{"id":41339634,"url":"https://github.com/djust-org/djust","last_synced_at":"2026-06-14T04:02:24.121Z","repository":{"id":332969831,"uuid":"1135756180","full_name":"djust-org/djust","owner":"djust-org","description":"Phoenix LiveView-style reactive server-side rendering for Django with Rust-powered performance","archived":false,"fork":false,"pushed_at":"2026-06-01T00:54:57.000Z","size":32625,"stargazers_count":51,"open_issues_count":10,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-01T03:04:03.935Z","etag":null,"topics":["django","htmx-alternative","liveview","pwa","python","reactive","real-time","rust","server-side-rendering","websocket"],"latest_commit_sha":null,"homepage":"https://djust.org","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/djust-org.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY_AUDIT_TEMPLATE.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-16T14:49:01.000Z","updated_at":"2026-06-01T00:54:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"a0bb0949-a9c2-4fcf-8fe4-3a5bf375ad47","html_url":"https://github.com/djust-org/djust","commit_stats":null,"previous_names":["djust-org/djust"],"tags_count":144,"template":false,"template_full_name":null,"purl":"pkg:github/djust-org/djust","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djust-org%2Fdjust","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djust-org%2Fdjust/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djust-org%2Fdjust/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djust-org%2Fdjust/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/djust-org","download_url":"https://codeload.github.com/djust-org/djust/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djust-org%2Fdjust/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33969883,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","htmx-alternative","liveview","pwa","python","reactive","real-time","rust","server-side-rendering","websocket"],"created_at":"2026-01-23T06:42:28.531Z","updated_at":"2026-06-14T04:02:24.114Z","avatar_url":"https://github.com/djust-org.png","language":"Python","funding_links":["https://github.com/sponsors/djust-org"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"branding/logo/djust-wordmark-dark.png\" alt=\"djust\" width=\"300\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cstrong\u003eReactive server-side rendering for Django, powered by Rust\u003c/strong\u003e\u003c/p\u003e\n\ndjust brings Phoenix LiveView-style reactive components to Django. You write\nserver-side Python; the client updates automatically over a WebSocket. There is\nno JavaScript to write, no bundler, and no build step in your project.\n\n**[djust.org](https://djust.org)** · **[Documentation](https://docs.djust.org)** · **[Quick Start](https://docs.djust.org/getting-started/)** · **[Examples](https://djust.org/examples/)**\n\n[![PyPI version](https://img.shields.io/pypi/v/djust.svg)](https://pypi.org/project/djust/)\n[![CI](https://github.com/djust-org/djust/actions/workflows/test.yml/badge.svg)](https://github.com/djust-org/djust/actions/workflows/test.yml)\n[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)\n[![Django 4.2+](https://img.shields.io/badge/django-4.2+-green.svg)](https://www.djangoproject.com/)\n[![PyPI Downloads](https://img.shields.io/pypi/dm/djust.svg)](https://pypi.org/project/djust/)\n\n## Features\n\n- **Fast** — Rust-powered template engine and virtual DOM diffing (10–100x faster than plain Django rendering; see [Performance](#performance))\n- **Reactive components** — Phoenix LiveView-style server-side reactivity\n- **Django compatible** — works with existing Django templates and components\n- **No build step** — ~55 KB gzipped client JavaScript, no bundling required\n- **WebSocket updates** — real-time DOM patches over WebSocket, with HTTP fallback\n- **Minimal payloads** — diffing sends only what changed\n- **Rust core** — performance-critical paths (templates, VDOM, parsing) are written in Rust\n- **Debug panel** — interactive debugging with event history and VDOM inspection\n- **Lazy hydration** — defer WebSocket connections for below-the-fold content to reduce memory\n- **TurboNav compatible** — works with Turbo-style client-side navigation\n- **PWA support** — offline-first Progressive Web Apps with automatic sync\n- **Multi-tenant** — tenant isolation for SaaS architectures\n- **Auth** — view-level and handler-level authorization via Django permissions\n\n## Quick Example\n\n```python\nfrom djust import LiveView, event_handler\n\nclass CounterView(LiveView):\n    template_string = \"\"\"\n    \u003cdiv\u003e\n        \u003ch1\u003eCount: {{ count }}\u003c/h1\u003e\n        \u003cbutton dj-click=\"increment\"\u003e+\u003c/button\u003e\n        \u003cbutton dj-click=\"decrement\"\u003e-\u003c/button\u003e\n    \u003c/div\u003e\n    \"\"\"\n\n    def mount(self, request, **kwargs):\n        self.count = 0\n\n    @event_handler\n    def increment(self):\n        self.count += 1  # Automatically updates client\n\n    @event_handler\n    def decrement(self):\n        self.count -= 1\n```\n\nNo JavaScript needed. State changes trigger minimal DOM updates automatically.\n\n## How Reactivity Works\n\ndjust uses a Rust-powered virtual DOM (VDOM) to diff server-rendered HTML and\nsend only the changed patches over WebSocket. A few core attributes make\neverything click.\n\n### Template Anatomy\n\n```html\n{% load djust_tags %}\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    {% djust_scripts %}              {# Loads the client runtime #}\n\u003c/head\u003e\n\u003cbody dj-view=\"{{ dj_view_id }}\"\u003e   {# Identifies the WebSocket session #}\n    \u003cdiv dj-root\u003e                    {# Reactive boundary — only this is diffed #}\n        \u003ch1\u003eCount: {{ count }}\u003c/h1\u003e\n        \u003cbutton dj-click=\"increment\"\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n    {# Static content outside dj-root is never touched by VDOM patching #}\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n| Attribute | Where | Purpose |\n|---|---|---|\n| `{% djust_scripts %}` | `\u003chead\u003e` | Injects client JavaScript |\n| `dj-view=\"{{ dj_view_id }}\"` | `\u003cbody\u003e` | Connects page to WebSocket session |\n| `dj-root` | Inner `\u003cdiv\u003e` | Marks the reactive region; only HTML inside is diffed and patched |\n\n### Stable List Identity\n\nFor lists that can reorder or have items inserted/deleted, add `data-key` or\n`dj-key` on each item. djust uses this to emit `MoveChild` patches instead of\nremove-then-insert pairs, preserving DOM state (focus, scroll position,\nanimations):\n\n```html\n{% for item in items %}\n\u003cdiv data-key=\"{{ item.id }}\"\u003e\n    {{ item.name }}\n    \u003cbutton dj-click=\"delete\" data-item-id=\"{{ item.id }}\"\u003eDelete\u003c/button\u003e\n\u003c/div\u003e\n{% endfor %}\n```\n\nWithout a key, djust diffs by position — correct, but it produces more DOM\nmutations for reorders.\n\n### Common Pitfall: One-Sided `{% if %}` in Class Attributes\n\nUsing `{% if %}` without `{% else %}` inside an HTML attribute value can cause\nVDOM patching misalignment, because of djust's branch-aware div-depth counting:\n\n```html\n{# WRONG: one-sided if inside class attribute #}\n\u003cdiv class=\"card {% if active %}active{% endif %}\"\u003e\n\n{# CORRECT: use full if/else #}\n\u003cdiv class=\"card {% if active %}active{% else %}{% endif %}\"\u003e\n\n{# ALSO CORRECT: move conditional outside the tag #}\n{% if active %}\n\u003cdiv class=\"card active\"\u003e\n{% else %}\n\u003cdiv class=\"card\"\u003e\n{% endif %}\n    ...\n\u003c/div\u003e\n```\n\nThis applies only to attribute values — `{% if %}` blocks in element content\nwork fine.\n\nSee the [VDOM Architecture guide](docs/website/advanced/vdom-architecture.md)\nand [Template Cheat Sheet](docs/website/guides/template-cheatsheet.md) for full\ndetails.\n\n## Getting Started\n\nA complete walkthrough from zero to a working reactive counter in five steps.\n\n### Step 1 — Install\n\n```bash\npip install djust django-channels\n```\n\n### Step 2 — Add to `INSTALLED_APPS` and configure settings\n\nIn `myproject/settings.py`:\n\n```python\nINSTALLED_APPS = [\n    # ... your existing apps ...\n    'channels',   # WebSocket support\n    'djust',\n]\n\nASGI_APPLICATION = 'myproject.asgi.application'\n\nCHANNEL_LAYERS = {\n    'default': {\n        'BACKEND': 'channels.layers.InMemoryChannelLayer',\n    }\n}\n```\n\n### Step 3 — Configure `asgi.py`\n\nReplace `myproject/asgi.py` with:\n\n```python\nimport os\nfrom django.core.asgi import get_asgi_application\nfrom channels.routing import ProtocolTypeRouter, URLRouter\nfrom channels.auth import AuthMiddlewareStack\nfrom djust.websocket import LiveViewConsumer\nfrom django.urls import path\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')\n\napplication = ProtocolTypeRouter({\n    \"http\": get_asgi_application(),\n    \"websocket\": AuthMiddlewareStack(\n        URLRouter([\n            path('ws/live/', LiveViewConsumer.as_asgi()),\n        ])\n    ),\n})\n```\n\n### Step 4 — Add the URL route\n\nIn `myproject/urls.py`:\n\n```python\nfrom django.urls import path\nfrom myapp.views import CounterView\n\nurlpatterns = [\n    path('counter/', CounterView.as_view(), name='counter'),\n]\n```\n\n### Step 5 — Write the view and template\n\n`myapp/views.py`:\n\n```python\nfrom djust import LiveView, event_handler\n\nclass CounterView(LiveView):\n    template_name = 'counter.html'\n\n    def mount(self, request, **kwargs):\n        self.count = 0\n\n    @event_handler\n    def increment(self):\n        self.count += 1\n\n    @event_handler\n    def decrement(self):\n        self.count -= 1\n```\n\n`myapp/templates/counter.html`:\n\n```html\n{% load djust_tags %}\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    \u003ctitle\u003eCounter\u003c/title\u003e\n    {% djust_scripts %}\n\u003c/head\u003e\n\u003cbody dj-view=\"{{ dj_view_id }}\"\u003e\n    \u003cdiv dj-root\u003e\n        \u003ch1\u003eCount: {{ count }}\u003c/h1\u003e\n        \u003cbutton dj-click=\"increment\"\u003e+\u003c/button\u003e\n        \u003cbutton dj-click=\"decrement\"\u003e-\u003c/button\u003e\n    \u003c/div\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nRun with `uvicorn myproject.asgi:application --reload` and open `/counter/`.\nClicking the buttons updates the count without a page reload — no JavaScript\nwritten, no build step.\n\n**Next steps:**\n- [Template Cheat Sheet](docs/website/guides/template-cheatsheet.md) — all directives and filters at a glance\n- [Components Guide](docs/website/guides/components.md) — build reusable components with theming\n- [CSS Framework Guide](docs/website/guides/css-frameworks.md) — Tailwind and Bootstrap integration\n- [Deployment Guide](docs/website/guides/deployment.md) — production deployment with uvicorn, Redis, and Nginx\n\n---\n\n## Performance\n\nBenchmarked on an M1 MacBook Pro (2021):\n\n| Operation | Django | djust | Speedup |\n|-----------|---------|-------|---------|\n| Template rendering (100 items) | 2.5 ms | 0.15 ms | **16.7x** |\n| Large list (10k items) | 450 ms | 12 ms | **37.5x** |\n| Virtual DOM diff | N/A | 0.08 ms | **sub-ms** |\n| Round-trip update | 50 ms | 5 ms | **10x** |\n\nRun the benchmarks yourself:\n\n```bash\ncd benchmarks\npython benchmark.py\n```\n\n## Installation\n\n### Prerequisites\n\n- Python 3.10+\n- Django 4.2+\n- Rust 1.70+ (only required when building from source)\n\n### Install from PyPI\n\n```bash\npip install djust\n```\n\n### Build from Source\n\n#### Using Make (recommended for development)\n\n```bash\n# Clone the repository\ngit clone https://github.com/djust-org/djust.git\ncd djust\n\n# Install Rust (if needed)\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n# Install everything and build\nmake install\n\n# Start the development server\nmake start\n\n# See all available commands\nmake help\n```\n\nCommon Make commands:\n\n- `make start` — start development server with hot reload\n- `make stop` — stop the development server\n- `make status` — check if the server is running\n- `make test` — run all tests\n- `make clean` — clean build artifacts\n- `make help` — show all available commands\n\n#### Using uv\n\n```bash\n# Clone the repository\ngit clone https://github.com/djust-org/djust.git\ncd djust\n\n# Install Rust (if needed)\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n# Install uv (if needed)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Create virtual environment and install dependencies\nuv venv\nsource .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\n\n# Install maturin and build\nuv pip install maturin\nmaturin develop --release\n```\n\n#### Using pip\n\n```bash\n# Clone the repository\ngit clone https://github.com/djust-org/djust.git\ncd djust\n\n# Install Rust (if needed)\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n\n# Install maturin\npip install maturin\n\n# Build and install\nmaturin develop --release\n\n# Or build a wheel\nmaturin build --release\npip install target/wheels/djust-*.whl\n```\n\n## Documentation\n\nThe full documentation lives at [docs.djust.org](https://docs.djust.org). The\nsections below cover the core API; see [Getting Started](#getting-started) above\nfor first-time setup.\n\n### Creating LiveViews\n\n#### Class-Based LiveView\n\n```python\nfrom djust import LiveView, event_handler\n\nclass TodoListView(LiveView):\n    template_name = 'todos.html'  # Or use template_string\n\n    def mount(self, request, **kwargs):\n        \"\"\"Called when view is first loaded\"\"\"\n        self.todos = []\n\n    @event_handler\n    def add_todo(self, text):\n        \"\"\"Event handler — called from client\"\"\"\n        self.todos.append({'text': text, 'done': False})\n\n    @event_handler\n    def toggle_todo(self, index):\n        self.todos[index]['done'] = not self.todos[index]['done']\n```\n\n#### Function-Based LiveView\n\n```python\nfrom djust import live_view\n\n@live_view(template_name='counter.html')\ndef counter_view(request):\n    count = 0\n\n    def increment():\n        nonlocal count\n        count += 1\n\n    return locals()  # Returns all local variables as context\n```\n\n### Template Syntax\n\ndjust supports Django template syntax with event binding:\n\n```html\n\u003c!-- Variables --\u003e\n\u003ch1\u003e{{ title }}\u003c/h1\u003e\n\n\u003c!-- Filters (all 57 Django built-in filters supported) --\u003e\n\u003cp\u003e{{ text|upper }}\u003c/p\u003e\n\u003cp\u003e{{ description|truncatewords:20 }}\u003c/p\u003e\n\u003ca href=\"?q={{ query|urlencode }}\"\u003eSearch\u003c/a\u003e\n{{ body|urlize }}  {# No |safe needed — djust auto-marks urlize output as safe (see note below) #}\n\n\u003c!-- Control flow --\u003e\n{% if show %}\n    \u003cdiv\u003eVisible\u003c/div\u003e\n{% endif %}\n\n{% if count \u003e 10 %}\n    \u003cdiv\u003eMany items!\u003c/div\u003e\n{% endif %}\n\n{% for item in items %}\n    \u003cli\u003e{{ item }}\u003c/li\u003e\n{% endfor %}\n\n\u003c!-- URL resolution --\u003e\n\u003ca href=\"{% url 'myapp:detail' pk=item.id %}\"\u003eView\u003c/a\u003e\n\n\u003c!-- Template includes --\u003e\n{% include \"partials/header.html\" %}\n\n\u003c!-- Event binding --\u003e\n\u003cbutton dj-click=\"increment\"\u003eClick me\u003c/button\u003e\n\u003cinput dj-input=\"on_search\" type=\"text\" /\u003e\n\u003cform dj-submit=\"submit_form\"\u003e\n    \u003cinput name=\"email\" /\u003e\n    \u003cbutton type=\"submit\"\u003eSubmit\u003c/button\u003e\n\u003c/form\u003e\n```\n\n\u003e **Django migration note:** In standard Django, `urlize` requires `|safe` to\n\u003e render its HTML output. djust's Rust template engine automatically marks\n\u003e `urlize`, `urlizetrunc`, and `unordered_list` as safe (via\n\u003e `safe_output_filters` in the renderer), because these filters handle their own\n\u003e HTML escaping internally. Adding `|safe` after them is unnecessary.\n\n### Supported Events\n\n- `dj-click` — click events\n- `dj-input` — input events (passes `value`)\n- `dj-change` — change events (passes `value`)\n- `dj-submit` — form submission (passes form data as a dict)\n\n### Reusable Components\n\ndjust includes a component system with automatic state management and stable\ncomponent IDs.\n\n#### Basic Component Example\n\n```python\nfrom djust.components import AlertComponent\n\nclass MyView(LiveView):\n    def mount(self, request):\n        # Components get automatic IDs based on attribute names\n        self.alert_success = AlertComponent(\n            message=\"Operation successful!\",\n            type=\"success\",\n            dismissible=True\n        )\n        # component_id automatically becomes \"alert_success\"\n```\n\n#### Component ID Management\n\nComponents automatically receive a stable `component_id` based on their\n**attribute name** in your view, which eliminates manual ID management:\n\n```python\n# When you write:\nself.alert_success = AlertComponent(message=\"Success!\")\n\n# The framework automatically:\n# 1. Sets component.component_id = \"alert_success\"\n# 2. Persists this ID across renders and events\n# 3. Uses it in HTML: data-component-id=\"alert_success\"\n# 4. Routes events back to the correct component\n```\n\nWhy it works:\n\n- The attribute name (`alert_success`) is already unique within your view\n- It's stable across re-renders and WebSocket reconnections\n- Event handlers can reference components by their attribute names\n- No manual ID strings to keep in sync\n\nEvent routing example:\n\n```python\nclass MyView(LiveView):\n    def mount(self, request):\n        self.alert_warning = AlertComponent(\n            message=\"Warning message\",\n            dismissible=True\n        )\n\n    @event_handler\n    def dismiss(self, component_id: str = None):\n        \"\"\"Handle dismissal — automatically routes to correct component\"\"\"\n        if component_id and hasattr(self, component_id):\n            component = getattr(self, component_id)\n            if hasattr(component, 'dismiss'):\n                component.dismiss()  # component_id=\"alert_warning\"\n```\n\nWhen the dismiss button is clicked, the client sends `component_id=\"alert_warning\"`,\nand the handler uses `getattr(self, \"alert_warning\")` to find the component.\n\n#### Creating Custom Components\n\n```python\nfrom djust import LiveComponent, event_handler\nfrom djust.components import register_component\n\nclass ButtonComponent(LiveComponent):\n    template = '\u003cbutton dj-click=\"on_click\" data-component-id=\"{{ component_id }}\"\u003e{{ label }}\u003c/button\u003e'\n\n    def mount(self, **kwargs):\n        self.label = kwargs.get(\"label\", \"Click\")\n        self.clicks = 0\n\n    @event_handler()\n    def on_click(self, **kwargs):\n        self.clicks += 1\n        self.trigger_update()\n\n    def get_context_data(self):\n        return {\"label\": self.label, \"clicks\": self.clicks}\n\n# register_component accepts LiveComponent subclasses (stateful, event-driven)\nregister_component('my-button', ButtonComponent)\n```\n\n### Decorators\n\n```python\nfrom djust import LiveView, event_handler, reactive\n\nclass MyView(LiveView):\n    @event_handler\n    def handle_click(self):\n        \"\"\"Marks method as event handler\"\"\"\n        pass\n\n    @reactive\n    def count(self):\n        \"\"\"Reactive property — auto-triggers updates\"\"\"\n        return self._count\n\n    @count.setter\n    def count(self, value):\n        self._count = value\n```\n\n### Configuration\n\nConfigure djust in your Django `settings.py`:\n\n```python\nLIVEVIEW_CONFIG = {\n    # Transport mode\n    'use_websocket': True,  # Set to False for HTTP-only mode (no WebSocket dependency)\n\n    # Debug settings\n    'debug_vdom': False,  # Enable detailed VDOM patch logging (for troubleshooting)\n\n    # Serialization (issue #292)\n    'strict_serialization': False,  # Raise TypeError for non-serializable state values (recommended in development)\n\n    # CSS Framework\n    'css_framework': 'bootstrap5',  # Options: 'bootstrap5', 'tailwind', None\n}\n```\n\nCommon configuration options:\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `use_websocket` | `True` | Use WebSocket transport (requires Django Channels) |\n| `debug_vdom` | `False` | Enable detailed VDOM debugging logs |\n| `strict_serialization` | `False` | Raise TypeError for non-serializable state (recommended in dev) |\n| `css_framework` | `'bootstrap5'` | CSS framework for components |\n\nCSS framework setup. For Tailwind CSS, use the one-command setup:\n\n```bash\npython manage.py djust_setup_css tailwind\n```\n\nThis auto-detects template directories, creates config files, and builds your\nCSS. For production:\n\n```bash\npython manage.py djust_setup_css tailwind --minify\n```\n\nSee the [CSS Framework Guide](docs/website/guides/css-frameworks.md) for detailed\nsetup instructions, Bootstrap configuration, and CI/CD integration.\n\nDebug mode. When troubleshooting VDOM issues, enable debug logging:\n\n```python\n# In settings.py\nLIVEVIEW_CONFIG = {\n    'debug_vdom': True,\n}\n\n# Or programmatically\nfrom djust.config import config\nconfig.set('debug_vdom', True)\n```\n\nThis logs:\n\n- Server-side: patch generation details (stderr)\n- Client-side: patch application and DOM traversal (browser console)\n\n### State Management\n\ndjust provides Python-only state management decorators that remove the need for\nmanual JavaScript.\n\n#### Quick Start\n\nBuild a debounced search in eight lines of Python (no JavaScript):\n\n```python\nfrom djust import LiveView\nfrom djust.decorators import debounce\n\nclass ProductSearchView(LiveView):\n    template_string = \"\"\"\n    \u003cinput dj-input=\"search\" placeholder=\"Search products...\" /\u003e\n    \u003cdiv\u003e{% for p in results %}\u003cdiv\u003e{{ p.name }}\u003c/div\u003e{% endfor %}\u003c/div\u003e\n    \"\"\"\n\n    def mount(self, request):\n        self.results = []\n\n    @debounce(wait=0.5)  # Wait 500ms after typing stops\n    def search(self, query: str = \"\", **kwargs):\n        self.results = Product.objects.filter(name__icontains=query)[:10]\n```\n\nThe server only queries after you stop typing. Add `@optimistic` for instant UI\nupdates, or `@cache(ttl=300)` to cache responses for five minutes.\n\nSee the [State Management Quick Start](docs/STATE_MANAGEMENT_QUICKSTART.md).\n\n#### Available Decorators\n\n| Decorator | Use When | Example |\n|-----------|----------|---------|\n| `@debounce(wait)` | User is typing | Search, autosave |\n| `@throttle(interval)` | Rapid events | Scroll, resize |\n| `@optimistic` | Instant feedback | Counter, toggle |\n| `@cache(ttl, key_params)` | Repeated queries | Autocomplete |\n| `@client_state(keys)` | Multi-component | Dashboard filters |\n| `@background` | Long operations | AI generation, file processing |\n| `DraftModeMixin` | Auto-save forms | Contact form |\n\nQuick decision guide:\n\n- Typing in an input? → `@debounce(0.5)`\n- Scrolling/resizing? → `@throttle(0.1)`\n- Need an instant UI update? → `@optimistic`\n- Same query multiple times? → `@cache(ttl)`\n- Multiple components? → `@client_state([keys])`\n- Long-running work? → `@background` or `self.start_async(callback)`\n- Auto-save forms? → `DraftModeMixin`\n\nMore documentation:\n\n- [Quick Start](docs/STATE_MANAGEMENT_QUICKSTART.md) — get productive fast\n- [Full Tutorial](docs/STATE_MANAGEMENT_TUTORIAL.md) — step-by-step product search\n- [API Reference](docs/STATE_MANAGEMENT_API.md) — complete decorator docs and cheat sheet\n- [Examples](docs/STATE_MANAGEMENT_EXAMPLES.md) — copy-paste-ready code\n- [Migration Guide](docs/STATE_MANAGEMENT_MIGRATION.md) — convert JavaScript to Python\n- [Framework Comparison](docs/STATE_MANAGEMENT_COMPARISON.md) — vs Phoenix LiveView and Laravel Livewire\n\n### Navigation Patterns\n\ndjust provides three navigation mechanisms for building multi-view applications\nwithout full page reloads:\n\n#### When to Use What\n\n| Scenario | Use | Why |\n|----------|-----|-----|\n| Filter/sort/paginate within same view | `dj-patch` / `live_patch()` | No remount, URL stays bookmarkable |\n| Navigate to a different LiveView | `dj-navigate` / `live_redirect()` | Same WebSocket, no page reload |\n| Link to non-LiveView page | Standard `\u003ca href\u003e` | Full page load needed |\n\n#### Quick Decision Tree\n\n```\nIs this a direct user click on a link?\n├─ Yes → Is it the same view (filter/sort)?\n│   ├─ Yes → Use dj-patch\n│   └─ No → Use dj-navigate\n│\n└─ No → Is navigation conditional on server logic?\n    ├─ Yes → Use live_redirect() in @event_handler\n    │   Examples: form validation, auth checks, async operations\n    └─ No → You probably need dj-navigate (see anti-pattern below)\n```\n\n#### Anti-Pattern: Don't Use `dj-click` for Navigation\n\nThis is the most common mistake when building multi-view djust apps. Using\n`dj-click` to trigger a handler that immediately calls `live_redirect()` creates\nan unnecessary round-trip.\n\nWrong — using `dj-click` to trigger a handler that calls `live_redirect()`:\n\n```python\n# Anti-pattern: handler does nothing but navigate\n@event_handler()\ndef go_to_item(self, item_id, **kwargs):\n    self.live_redirect(f\"/items/{item_id}/\")  # Wasteful round-trip\n```\n\n```html\n\u003c!-- Wrong: forces a WebSocket round-trip just to navigate --\u003e\n\u003cbutton dj-click=\"go_to_item\" dj-value-item_id=\"{{ item.id }}\"\u003eView\u003c/button\u003e\n```\n\nWhat actually happens:\n\n1. User clicks button → client sends WebSocket message (50–100ms)\n2. Server receives message, processes handler (10–50ms)\n3. Server responds with `live_redirect` command (50–100ms)\n4. Client finally navigates to the new view\n\nTotal: 110–250ms, plus handler processing time.\n\nRight — using `dj-navigate` directly:\n\n```html\n\u003c!-- Right: client navigates immediately, no server round-trip --\u003e\n\u003ca dj-navigate=\"/items/{{ item.id }}/\"\u003eView Item\u003c/a\u003e\n```\n\nWhat happens:\n\n1. User clicks link → client navigates directly\n\nTotal: ~10ms (just DOM updates).\n\nWhy it matters:\n\n- Performance: 10–20x faster navigation\n- Network efficiency: saves WebSocket bandwidth\n- User experience: instant response, no loading indicators needed\n- Simplicity: less code, fewer moving parts\n\n#### When to Use `live_redirect()` in Handlers\n\nUse handlers for navigation only when navigation depends on server-side logic or\nvalidation.\n\nConditional navigation after form validation:\n\n```python\n@event_handler()\ndef submit_form(self, **kwargs):\n    if self.form.is_valid():\n        self.form.save()\n        self.live_redirect(\"/success/\")  # OK: conditional on validation\n    else:\n        # Stay on form to show errors\n        pass\n```\n\nNavigation based on auth/permissions:\n\n```python\n@event_handler()\ndef view_sensitive_data(self, **kwargs):\n    if not self.request.user.has_perm('app.view_sensitive'):\n        self.live_redirect(\"/access-denied/\")  # OK: auth check required\n        return\n    self.show_sensitive = True\n```\n\nNavigation after async operations:\n\n```python\n@event_handler()\nasync def create_and_view_item(self, name, **kwargs):\n    item = await Item.objects.acreate(name=name, owner=self.request.user)\n    self.live_redirect(f\"/items/{item.id}/\")  # OK: navigate to newly created item\n```\n\nMulti-step wizard logic:\n\n```python\n@event_handler()\ndef next_step(self, **kwargs):\n    if self.current_step == \"payment\" and not self.payment_valid:\n        # Stay on payment step if invalid\n        return\n    self.current_step = self.get_next_step()\n    self.live_patch(params={\"step\": self.current_step})  # OK: conditional flow\n```\n\nThe common theme: the handler does meaningful work before navigating. If your\nhandler only calls `live_redirect()`, use `dj-navigate` instead.\n\n#### Quick Example: Multi-View App\n\n```python\nfrom djust import LiveView\nfrom djust.mixins.navigation import NavigationMixin\nfrom djust.decorators import event_handler\n\nclass ProductListView(NavigationMixin, LiveView):\n    template_string = \"\"\"\n    \u003c!-- Filter within same view: use dj-patch --\u003e\n    \u003ca dj-patch=\"?category=electronics\"\u003eElectronics\u003c/a\u003e\n    \u003ca dj-patch=\"?category=books\"\u003eBooks\u003c/a\u003e\n\n    \u003cdiv\u003e\n        {% for product in products %}\n            \u003c!-- Navigate to different view: use dj-navigate --\u003e\n            \u003ca dj-navigate=\"/products/{{ product.id }}/\"\u003e{{ product.name }}\u003c/a\u003e\n        {% endfor %}\n    \u003c/div\u003e\n    \"\"\"\n\n    def mount(self, request, **kwargs):\n        self.category = \"all\"\n        self.products = []\n\n    def handle_params(self, params, uri):\n        \"\"\"Called when URL changes via dj-patch or browser back/forward\"\"\"\n        self.category = params.get(\"category\", \"all\")\n        self.products = Product.objects.filter(category=self.category)\n```\n\nSee the [Navigation Guide](docs/guides/navigation.md) for the complete API\nreference (`live_patch()`, `live_redirect()`, `handle_params()`).\n\n### Developer Tooling\n\n#### Debug Panel\n\nInteractive debugging tool for LiveView development (DEBUG mode only):\n\n```python\n# In settings.py\nDEBUG = True  # Debug panel automatically enabled\n```\n\nOpen it with `Ctrl+Shift+D` (Windows/Linux) or `Cmd+Shift+D` (Mac), or click the\nfloating debug button.\n\nFeatures:\n\n- **Event handlers** — discover all handlers with parameters, types, and descriptions\n- **Event history** — real-time log with timing metrics (e.g., `search • 45.2ms`)\n- **VDOM patches** — monitor DOM updates with sub-millisecond precision\n- **Variables** — inspect current view state\n\nSee the [Debug Panel Guide](docs/DEBUG_PANEL.md) and\n[Event Handler Best Practices](docs/EVENT_HANDLERS.md).\n\n#### Event Handlers\n\nAlways use the `@event_handler` decorator for auto-discovery and validation:\n\n```python\nfrom djust.decorators import event_handler\n\n@event_handler()\ndef search(self, value: str = \"\", **kwargs):\n    \"\"\"Search handler — description shown in debug panel\"\"\"\n    self.search_query = value\n```\n\nParameter convention: use `value` for form inputs (`dj-input`, `dj-change`\nevents):\n\n```python\n# Correct — matches what form events send\n@event_handler()\ndef search(self, value: str = \"\", **kwargs):\n    self.search_query = value\n\n# Wrong — won't receive input value\n@event_handler()\ndef search(self, query: str = \"\", **kwargs):\n    self.search_query = query  # Always \"\" (default)\n```\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────┐\n│  Browser                                    │\n│  ├── client.js (~55 KB gz) — events \u0026 DOM  │\n│  └── WebSocket connection                   │\n└─────────────────────────────────────────────┘\n           ↕ WebSocket (Binary/JSON)\n┌─────────────────────────────────────────────┐\n│  Django + Channels (Python)                 │\n│  ├── LiveView classes                       │\n│  ├── Event handlers                         │\n│  └── State management                       │\n└─────────────────────────────────────────────┘\n           ↕ Python/Rust FFI (PyO3)\n┌─────────────────────────────────────────────┐\n│  Rust core (native speed)                   │\n│  ├── Template engine (\u003c1ms)                │\n│  ├── Virtual DOM diffing (\u003c100μs)          │\n│  ├── HTML parser                            │\n│  └── Binary serialization (MessagePack)    │\n└─────────────────────────────────────────────┘\n```\n\n## Examples\n\nSee the [examples/demo_project](examples/demo_project) directory for complete\nworking examples:\n\n- **Counter** — simple reactive counter\n- **Todo List** — CRUD operations with lists\n- **Chat** — real-time messaging\n\nRun the demo:\n\n```bash\ncd examples/demo_project\npip install -r requirements.txt\npython manage.py migrate\npython manage.py runserver\n```\n\nVisit http://localhost:8000.\n\n## Development\n\n### Project Structure\n\n```\ndjust/\n├── crates/\n│   ├── djust_core/        # Core types \u0026 utilities\n│   ├── djust_templates/   # Template engine\n│   ├── djust_vdom/        # Virtual DOM \u0026 diffing\n│   ├── djust_components/  # Reusable component library\n│   └── djust_live/        # Main PyO3 bindings\n├── python/\n│   └── djust/             # Python package\n│       ├── live_view.py         # LiveView base class\n│       ├── component.py         # Component system\n│       ├── websocket.py         # WebSocket consumer\n│       └── static/\n│           └── client.js        # Client runtime\n├── branding/                    # Logo and brand assets\n├── examples/                    # Example projects\n├── benchmarks/                  # Performance benchmarks\n└── tests/                       # Tests\n```\n\n### Running Tests\n\n```bash\n# All tests (Python + Rust + JavaScript)\nmake test\n\n# Individual test suites\nmake test-python       # Python tests\nmake test-rust         # Rust tests\nmake test-js           # JavaScript tests\n\n# Specific tests\npytest tests/unit/test_live_view.py\ncargo test --workspace --exclude djust_live\n```\n\nFor comprehensive testing documentation, see the\n[Testing Guide](docs/TESTING.md).\n\n### Building Documentation\n\n```bash\ncargo doc --open\n```\n\n## Roadmap\n\ndjust 1.0 is released and stable. Active planning lives in\n[the issue tracker](https://github.com/djust-org/djust/issues). One notable\nitem still open:\n\n- React/Vue component compatibility\n\n## Security\n\n- CSRF protection via Django middleware\n- XSS protection via automatic template escaping (the Rust engine escapes all variables by default)\n- HTML-producing filters (`urlize`, `urlizetrunc`, `unordered_list`) handle their own escaping internally; the Rust engine's `safe_output_filters` whitelist prevents double-escaping, so `|safe` is never needed with these filters\n- WebSocket authentication via Django sessions\n- WebSocket origin validation and HMAC message signing\n- Per-view and global rate limiting\n- Configurable allowed origins for WebSocket connections\n- View-level auth enforcement (`login_required`, `permission_required`) before `mount()`\n- Handler-level `@permission_required` for protecting individual event handlers\n- `djust_audit` command and `djust.S005` system check for auth-posture visibility\n\nReport security issues to security@djust.org.\n\n## Contributing\n\nContributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.\n\nAreas where help is especially useful:\n\n- More example applications\n- Performance optimizations\n- Documentation improvements\n- Browser compatibility testing\n\n## Supporting djust\n\ndjust is open source (MIT licensed) and free. If you use djust in production or\nwant to support development:\n\n- Star this repository to help others discover it\n- [Sponsor on GitHub](https://github.com/sponsors/djust-org) — from $5/month\n\n## License\n\nMIT License — see the [LICENSE](LICENSE) file for details.\n\n## Acknowledgments\n\n- Inspired by [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/)\n- Built with [PyO3](https://pyo3.rs/) for Python/Rust interop\n- Uses [html5ever](https://github.com/servo/html5ever) for HTML parsing\n- Built on the Rust and Django communities\n\n## Community \u0026 Support\n\n- [djust.org](https://djust.org) — official website\n- [Documentation](https://docs.djust.org) — guides and API reference\n- [Examples](https://djust.org/examples/) — live code examples\n- [Issues](https://github.com/djust-org/djust/issues) — bug reports and feature requests\n- Email: support@djust.org\n\n---\n\nMaintained by the djust community.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdjust-org%2Fdjust","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdjust-org%2Fdjust","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdjust-org%2Fdjust/lists"}