djust 0.4.2

StableReleased
Install
pip install djust==0.4.2

Fixed

  • Derived context vars synced when parent instance attr mutated in-place (#703)_sync_state_to_rust() now collects id()s of all sub-objects reachable from changed instance attrs and includes any derived context var whose id() appears in that set. Previously, context vars computed in get_context_data() that returned sub-objects of a mutated dict (e.g., wizard_step_data.get("person", {})) were skipped because their id() was unchanged, causing templates to render stale data. Depth-capped at 8 with cycle detection. 9 new regression tests.

  • as_live_field() now respects widget.input_type override for type attribute (#683 re-open) — The initial #683 fix merged widget.attrs but type was still ignored because Django moves type= from attrs into widget.input_type during widget __init__. _get_field_type() now checks widget.input_type against the widget class's default and uses the override when they differ (e.g. TextInput(attrs={"type": "tel"}) sets input_type="tel"). 4 new regression tests covering type="tel", type="url", type="search", and the default type="text" fallback.

Added

  • LiveComponent events now propagate to parent LiveView waiters (ADR-002 Phase 1b/1c follow-up) — Closes the "known limitation" documented in the v0.4.2 tutorials guide: await self.wait_for_event("foo") on a LiveView now resolves when the matching handler fires on an embedded LiveComponent, not just when it fires on the view itself. Without this, a TutorialStep(wait_for="submit", ...) where submit is a handler on a child FormComponent would silently stall forever — the parent view's waiter would never resolve and the tour would hang. The fix is in the WebSocket consumer's handle_event component-event branch: after the component handler runs, the consumer now calls self.view_instance._notify_waiters(event_name, notify_kwargs) with the handler's kwargs + an injected component_id key, mirroring the notification that already happened in the main LiveView branch from Phase 1b. The component_id injection means apps can use the waiter's predicate argument to disambiguate events fired from multiple component instances: wait_for_event("submit", predicate=lambda kw: kw.get("component_id") == "project_form"). A notification failure is caught and logged via the djust.websocket logger so a buggy waiter/predicate can't break the component handler's observable behavior — the component's state mutations always happen even if the waiter notification raises. 5 new regression tests in python/tests/test_waiter_component_propagation.py covering: component event resolves parent waiter, component_id is injected into notify kwargs so predicates can filter by source, multiple parent waiters for the same event all resolve (fan-out), the non-component branch still notifies parent waiters (regression guard for the Phase 1b path), and a raising _notify_waiters is logged-and-swallowed rather than propagating. docs/website/guides/tutorials.md Limitations section updated to document the new behavior with a component_id predicate example.

Documentation

  • Tutorial bubble must be placed outside dj-root (#699) — If the {% tutorial_bubble %} tag is placed inside the dj-root container, morphdom recovery (which replaces the entire dj-root content on patch failure) destroys the bubble mid-tour, causing it to silently disappear. The tutorials guide now has a dedicated "Bubble Placement" section explaining the requirement, why it exists, and correct/incorrect examples. The simplest-possible example at the top of the guide is updated to show the bubble outside dj-root. The tutorial_bubble template tag docstring is also updated with this requirement.

  • data-* attribute naming convention documented in Events guide (#623) — How data-foo-bar on an HTML element maps to foo_bar in the event handler's kwargs was undocumented. The Events guide now has a dedicated "Data Attribute Naming Convention" section covering: the dash-to-underscore rule, client-side type-hint suffixes (:int, :float, :bool, :json, :list), server-side Python type-hint coercion, the dj-value-* alternative, which internal data-* attributes are excluded, and a quick-reference table.

Changed

  • System checks T002, V008, C003 now suppressible via DJUST_CONFIG (#603) — These three informational checks fire on every manage.py invocation and are noisy for projects that deliberately don't use the checked features (daphne, explicit dj-root, non-primitive mount state). A new suppress_checks config key in DJUST_CONFIG (or LIVEVIEW_CONFIG) accepts a list of check IDs to silence: DJUST_CONFIG = {"suppress_checks": ["T002", "V008", "C003"]}. Both short IDs ("T002") and fully-qualified IDs ("djust.T002") are accepted, case-insensitive. Only the informational/advisory variants are suppressed — the C003 Warning (daphne misordered) still fires because it indicates a real misconfiguration. 7 new tests for the suppression mechanism.

  • release-drafter/release-drafter v6 → v7 + drop pull_request trigger — v7 validates target_commitish against the GitHub releases API and rejects refs/pull/<n>/merge refs, which is what github.ref resolves to under a pull_request trigger. v6 silently tolerated this; v7 does not, causing every PR to fail with Validation Failed: target_commitish invalid. The fix is to drop the pull_request trigger — release-drafter is designed to track changes that have landed on the release branch, not comment on in-flight PRs, so push: branches: [main] is the right fit. Aligns with how Phoenix, Elixir, GitHub CLI, and other major projects wire release-drafter. Resolves the v7 bump that was deferred out of the v0.4.2 dependabot batch (#680).

  • Dependency batch carry-over (v0.4.2) — Drains the dependabot backlog that was held behind the v0.4.1 release. Single consolidated PR so one CI run catches any inter-dep interactions:

    • npm: vitest / @vitest/ui / @vitest/coverage-v8 4.0.18 → 4.1.4 (patches + new test runner features), jsdom 29.0.1 → 29.0.2, happy-dom 20.8.4 → 20.8.9. Full JS suite remains green (1111 tests).
    • Cargo: tokio 1.50 → 1.51 (workspace), uuid 1.22 → 1.23, proptest 1.10 → 1.11 (djust_vdom), indexmap 2.13.0 → 2.14.0 (transitive pickup via cargo update). cargo check --workspace clean; cargo test -p djust_vdom passes all 42 proptest-driven tests on the new 1.11 runtime.
    • GitHub Actions: actions/github-script v8 → v9 (two workflows), astral-sh/setup-uv v6 → v7 (test workflow). Workflow syntax unchanged.
    • Intentionally deferred: html5ever 0.36 → 0.39 is a 3-minor-version jump that requires a matching markup5ever_rcdom 0.39 release which has not yet been published to crates.io (only git snapshots exist in the html5ever workspace). Using git deps in our published workspace would break cargo publish and leak unreleased upstream state, so this stays deferred until upstream publishes. release-drafter/release-drafter v6 → v7 was also deferred out of this chore batch because of a target_commitish validation incompatibility — shipped as a separate follow-up PR alongside this one.

    Closes 13 open dependabot PRs as superseded (#581, #582, #604, #606, #607, #609, #615, #616, #644, #645, #646, #647, #648).

Fixed

  • @background natively supports async def handlers (#697) — The @background decorator now detects asyncio.iscoroutinefunction and creates a native async closure so _run_async_work can await it directly on the event loop instead of routing through sync_to_async. The fragile inspect.iscoroutine(result) workaround from #692 is kept as a legacy fallback. 5 new regression tests.

  • flush_push_events() resolves callback dynamically on WS reconnect (#698)PushEventMixin.flush_push_events() now resolves the flush callback via self._ws_consumer._flush_push_events at call time instead of relying on a stored _push_events_flush_callback that was only wired during initial mount. After a WebSocket reconnect the view instance is restored from session but the stored callback was stale. The dynamic lookup always finds the current consumer. Legacy stored callback kept as fallback. 7 new tests.

  • push_commands-only handlers auto-skip VDOM re-render (#700) — Handlers that only call push_commands() / push_event() without changing public state no longer trigger a VDOM re-render. The _snapshot_assigns deep-copy comparison could report false positives for views with non-copyable public attributes (querysets, file handles) because sentinel objects never compare equal. A new identity-based check (id() comparison before/after) detects whether any public attribute was actually rebound and auto-sets _skip_render = True when push events are pending but no state changed. 5 new tests.

  • System check V010 detects wrong TutorialMixin MRO ordering at startup (#691) — Django's View.__init__ does not call super().__init__(), so writing class MyView(LiveView, TutorialMixin) silently skips TutorialMixin's initialisation. A new djust.V010 system check scans all LiveView subclasses at startup and emits an Error with a clear fix hint when TutorialMixin appears after a View-derived base in the class declaration. Suppressible via DJUST_CONFIG = {"suppress_checks": ["V010"]}. 5 new tests. Tutorials guide updated with correct ordering.

  • @background async def handlers now execute correctly (#692)@background wraps handlers in a sync closure; when the handler is async def, the closure returned an unawaited coroutine and the handler body never ran. The fix in _run_async_work (already on main via workaround) detects coroutine returns and awaits them. 11 new regression tests in test_background_async.py verify both sync and async handlers execute their bodies.

  • push_commands in @background tasks now flush mid-execution (#693) — Push events queued by push_commands inside a @background handler only reached the client when the entire task completed. The _flush_pending_push_events callback mechanism (already on main) lets TutorialMixin and other background handlers flush events immediately. A new public await self.flush_push_events() method on PushEventMixin provides the same capability to any @background handler. 7 new tests in test_push_flush_background.py.

  • get_context_data no longer includes non-serializable class attributes (#694) — The MRO walker in ContextMixin.get_context_data() added class-level attributes (like tutorial_steps) to the template context. Non-JSON-serializable values were silently converted to their str() repr, corrupting state on subsequent events. The fix skips class-level attributes that fail a JSON serialisability probe. Additionally, TutorialMixin now stores steps as _tutorial_steps (private) with a read-only tutorial_steps property, so they are excluded by both the _ prefix convention and the serialisability check. 14 new tests.

  • Debug panel SVG attributes no longer double-escaped (#613) — SVG attributes like viewBox and path d in the debug toolbar were rendered garbled because the Rust VDOM's to_html() method HTML-escaped text content inside <script> and <style> elements. Per the HTML spec, these are "raw text elements" whose content must be emitted verbatim — escaping & to &amp; or < to &lt; corrupts JavaScript/CSS code and causes double-escaping when the HTML is round-tripped through the VDOM pipeline (parse with html5ever which decodes entities, then re-serialize with to_html() which re-encodes them). The fix adds an in_raw_text flag to the internal _to_html() serializer that propagates through <script>/<style> children, skipping html_escape() for their text nodes. SVG attribute values in templates (which don't contain HTML special characters) were already correct but now have explicit regression tests. 4 new Rust unit tests, 3 new Rust integration tests (script/style/SVG roundtrip), 3 new Python regression tests (JS source validation, JSON injection check, VDOM roundtrip), and 3 new JS tests (tab icon SVGs, path d attributes, header button SVGs all verified in DOM).

  • form.cleaned_data Python types no longer serialize to null (#628)datetime.date, datetime.datetime, datetime.time, Decimal, and UUID values in form.cleaned_data stored in public view state are now properly serialized to their JSON representations (ISO strings, floats, strings) instead of silently becoming null. Both the DjangoJSONEncoder and normalize_django_value() already handled these types; 10 new regression tests confirm the behavior.

  • set() is now JSON-serializable as public state (#626) — Storing a Python set() or frozenset() in public view state no longer crashes json.dumps. Sets are serialized as sorted lists (falling back to unsorted when elements aren't comparable). Both DjangoJSONEncoder.default() and normalize_django_value() now handle set/frozenset. 11 new regression tests.

  • dict state no longer corrupted to list after Rust state sync (#612) — Round-tripping state through the Rust MessagePack serialization boundary could corrupt dict values into list because #[serde(untagged)] on the Value enum let rmp_serde match a msgpack map against the List variant before trying Object. The fix replaces the derived Deserialize with a custom visitor-based implementation that uses the deserializer's type hints (visit_map vs visit_seq) to correctly distinguish maps from arrays. 4 new Rust regression tests + 1 Python end-to-end msgpack round-trip test.

  • as_live_field() now merges widget.attrs into rendered HTML (#683) — The as_live_field() method (and {% live_field %} tag) dropped any attributes defined on a Django widget's attrs dict — type="email", placeholder, pattern, min/max, custom data-*, and any other HTML attributes were silently lost. The fix adds _merge_widget_attrs() to BaseAdapter, called from _render_input, _render_checkbox, and _render_radio, which merges field.widget.attrs into the output attributes with djust-specific keys (dj-change, name, class, etc.) taking precedence over widget defaults. Boolean False/None values in widget attrs are filtered out to avoid rendering disabled="False". 17 new regression tests in python/tests/test_live_field_widget_attrs.py covering: EmailInput placeholder/type, pattern/min/max/step/title, djust attrs override clashing widget attrs, empty widget attrs, textarea rows/cols, checkbox data-attrs, radio data-attrs on each option, select data-attrs, and boolean True/False handling.

  • VDOM patcher guards against text nodes for 5 patch types (#622) — The VDOM diff patcher called setAttribute(), removeAttribute(), appendChild(), removeChild(), and replaceChild() on #text nodes, which don't implement these methods. This crashed conditional rendering whenever a text node sat where the patcher expected an element (common in {% if %} blocks that switch between text and element content). The fix adds an isElement(node) guard at the top of each of the five patch-type branches in 12-vdom-patch.js — when the target is a non-element node (text, comment, CDATA), the patch is skipped gracefully instead of throwing. 4 new JS tests in tests/js/vdom_patch_errors.test.js covering setAttribute, removeAttribute, appendChild, and replaceChild on text nodes.

  • Autofocus handling on dynamically inserted elements (#617) — Dynamically inserted <input autofocus> elements didn't receive focus after a VDOM patch because the browser only honours the autofocus attribute on initial page load. The patcher now detects autofocus on newly inserted elements after each patch cycle and calls .focus() explicitly. 4 new JS tests in tests/js/vdom-autofocus.test.js covering single autofocus, multiple elements (last wins), elements without autofocus ignored, and no-op when no autofocus elements are present.

  • Private _ attributes preserved across events and reconnects (#627, #611) — Two related state-management bugs caused any attribute starting with _ (the documented convention for private/internal state) to be silently wiped. The root cause was that session save used the output of get_context_data(), which by design strips _-prefixed attributes. For #627, every WebSocket event round-trip lost private state because _save_state_to_session() persisted only public context. For #611, the pre-rendered WS reconnect path restored session state but never included private attributes set during the HTTP GET mount. The fix adds two helpers — _get_private_state() (collects all _-prefixed instance attrs that aren't dunder or in the base-class exclusion set) and _restore_private_state(state_dict) — and wires them into _save_state_to_session() (now persists private state under a _private_state session key) and _load_state_from_session() / the reconnect path in RequestMixin._restore_session_state() (restores private attrs before the view resumes). 20 new regression tests in python/tests/test_private_attr_preservation.py covering: private attrs survive event dispatch, survive reconnect, survive multiple sequential events, coexist with public attrs, handle None/complex/nested values, are excluded for dunder attrs, are excluded for base-class internals, and round-trip through session save/load.

  • Layout flash on pre-rendered mount: defer reinitAfterDOMUpdate via requestAnimationFrame (#619, fixes #618) — Carry-over bugfix from v0.4.1. When a page is pre-rendered via HTTP GET, the WebSocket mount used to call reinitAfterDOMUpdate() synchronously right after stamping dj-id attributes onto the existing DOM. That synchronous call triggered a full DOM traversal for event binding, which forced the browser to recalculate layout mid-paint — and on pages with large pre-rendered elements (e.g. big dashboard stat values) the elements briefly rendered at the wrong size before settling, producing a visible layout-flash on every initial load. The fix moves the post-mount block (reinit + _mountReady flag + form recovery + auto-recover) into a runPostMount closure and schedules it via requestAnimationFrame(runPostMount) when available, falling back to a synchronous call when requestAnimationFrame is unavailable (JSDOM tests, exotic non-browser environments). Event binding now happens after the browser finishes its current paint, eliminating the flash entirely. The ordering invariant (reinit → _mountReady → form recovery) is preserved inside the closure so dj-mounted handlers and recovered form inputs still see bound event listeners. The non-prerendered data.html innerHTML-replace branch is unchanged — it already invalidates layout via the full DOM swap so there's no pre-paint to protect. 8 new regression tests in tests/js/mount-deferred-reinit.test.js asserting: the rAF wrapper is present, the synchronous fallback is preserved, the closure is named runPostMount for stable debugging, reinitAfterDOMUpdate() runs before _mountReady inside the closure, _mountReady is set inside the closure (not synchronously), form recovery runs only on reconnect inside the closure, the non-prerendered branch calls reinit synchronously, and exactly one call-site of reinitAfterDOMUpdate() exists in the skipMountHtml branch (so a refactor that reintroduces the sync call would immediately flip red). Closes #619 as superseded and closes the original #618 bug report.

  • Scaffolded projects now default DEBUG=False and generate .env.example (#637) — Carry-over bugfix from v0.4.1. Previously, python -m djust startproject mysite and python -m djust new mysite both generated a settings.py with DEBUG = True and ALLOWED_HOSTS = ["*"] as hardcoded literals. A developer who deployed the scaffolded output without remembering to flip those values ran production with full stack traces, the django-insecure-<random> default SECRET_KEY, and a wildcard host allowlist — the exact footgun that A001 (DEBUG enabled) and A014 (ALLOWED_HOSTS too permissive) flag in djust_audit. Now both scaffold paths (cli.py's cmd_startproject and the higher-level djust.scaffolding.generator.generate_project) emit DEBUG = os.environ.get("DEBUG", "False").lower() in ("true", "1", "yes") and ALLOWED_HOSTS = [host.strip() for host in os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") if host.strip()] — unconfigured deployments fail safe. The scaffold also writes a .env.example template alongside .gitignore (which already ignores .env) so local development picks up developer-friendly values via cp .env.example .env + whatever .env loader the developer uses. The .env.example includes DEBUG=True, a freshly-generated SECRET_KEY token (via secrets.token_urlsafe(50)), and ALLOWED_HOSTS=localhost,127.0.0.1 so the local experience hasn't changed. 4 new regression tests in python/tests/test_cli_scaffold.py asserting: DEBUG = True is no longer literal, DEBUG reads from env with "False" fallback, ALLOWED_HOSTS = ["*"] is no longer literal, narrow localhost,127.0.0.1 env default, .env.example exists with the three documented vars and a real (not template-placeholder) secret key, .env remains in .gitignore while .env.example does not. Closes #637.

Added

  • TutorialMixin + TutorialStep + {% tutorial_bubble %} — declarative guided tours (ADR-002 Phase 1c) — Capstone of ADR-002 Phase 1: a one-import, zero-JavaScript way for any djust app to ship a real guided tour, onboarding flow, or wizard. Apps declare the tour as a list of TutorialStep dataclasses on a LiveView that mixes in TutorialMixin; the framework runs the state machine as a @background task, pushing a highlight + narrate + focus chain at each step's target via push_commands (Phase 1a), then either asyncio.sleep'ing for auto-advance steps or awaiting wait_for_event (Phase 1b) until the user actually fires the matching @event_handler. Four event handlers come for free — start_tutorial, skip_tutorial, cancel_tutorial, restart_tutorial — along with three instance attributes (tutorial_running, tutorial_current_step, tutorial_total_steps) for progress display in the view state. TutorialStep supports per-step target (CSS selector, required), message (narration text), position (top/bottom/left/right bubble hint), wait_for (handler name to suspend on), timeout (seconds — pairs with wait_for for bounded waits or used alone for auto-advance), on_enter/on_exit (optional extra JSChain pushes for per-step setup/teardown beyond the default highlight + narrate + focus), and highlight_class/narrate_event (override per-step CSS class and CustomEvent name when you need different visual treatment). Skip and cancel signals are raced against the wait via asyncio.wait(..., return_when=FIRST_COMPLETED) so either unblocks the current step immediately; WebSocket disconnect cancels the background task automatically so there's no lingering work, no leaked waiters, no stuck highlights. A new {% tutorial_bubble %} template tag renders a floating narration bubble that listens for tour:narrate CustomEvents at document level (dispatched at the step's target with bubbles: true), positions itself next to the target per the step's position hint, displays step N / total progress, and includes "Skip" and "Close" buttons pre-bound to the mixin's event handlers — the default bubble is marked dj-update="ignore" so morphdom doesn't clobber it during VDOM patches. The new client-side src/28-tutorial-bubble.js module (~140 lines, brings client.js to 30 modules) registers its listeners unconditionally at IIFE time, reads detail.text/target/position/step/total from the event, and updates the bubble's text + progress + position + visibility. The framework ships no CSS — apps style the bubble and highlight class themselves (the guide includes a minimal starter block). 26 new Python tests for the mixin covering TutorialStep dataclass (minimal, custom position, invalid position, empty target, empty message, wait_for+timeout, on_enter/on_exit), lifecycle (initial state, empty-steps no-op, single step, setup+cleanup chain order, multi-step order, idempotent start-while-running), wait_for_event integration (step suspends on user action, timeout advances silently, indefinite wait), skip/cancel paths (advance past current, abort loop, no-op when not running), on_enter/on_exit pushes, per-step highlight class override, and per-step narrate event override. 9 new Python tests for the tutorial_bubble template tag covering defaults, custom css_class/event/position, invalid-position fallback to "bottom", skip+cancel button bindings, text/progress element classes, and XSS escaping of hostile css_class and event kwargs. 12 new JS tests in tests/js/tutorial-bubble.test.js covering listener registration, text content updates, progress text updates, show/hide via data-visible, default/custom position application, missing-target graceful handling, missing-bubble graceful handling, tour:hide event, and repeated updates on subsequent events. Zero new runtime dependencies — stdlib asyncio + dataclasses + Django's format_html. Full documentation in the new docs/website/guides/tutorials.md guide with the simplest-possible example, state-machine description, TutorialStep reference, wait_for/timeout combinations table, on_enter/on_exit patterns, the bubble template tag docs, a starter CSS block, four usage patterns (auto-advance walkthrough, interactive onboarding, mixed, branching with custom handlers), skip/cancel UX, disconnect cleanup, debugging tips, and honest limitations (LiveComponent events don't propagate to parent waiters yet, actor-mode views bypass the dispatch hook, handler validation failures prevent the waiter from resolving except via timeout, single-user only — multi-user broadcast is Phase 4 in v0.5.x).

  • await self.wait_for_event(name, timeout=None, predicate=None) async primitive (ADR-002 Phase 1b) — Second half of the backend-driven UI Phase 1 primitives. Adds a new WaiterMixin (automatically included in LiveView) that lets a @background handler suspend until a specific @event_handler is called by the user, optionally filtered by a predicate, optionally bounded by a timeout. The returned dict is the kwargs that were passed to the matching handler. This is the primitive that makes "highlight this button, wait for the user to actually click it, then advance to the next step" work declaratively — required by TutorialMixin (Phase 1c) and by any server-driven flow that needs to pause mid-plan until real user input arrives. Implementation: ~180 lines in python/djust/mixins/waiters.py, a ~15-line hook in python/djust/websocket.py that calls _notify_waiters after every successful handler invocation, a ~10-line cleanup hook in the WebSocket disconnect path that cancels all pending waiters when the view tears down (so @background tasks unblock with CancelledError instead of leaking), and proper integration into LiveView's MRO via python/djust/mixins/__init__.py. The notify pass runs AFTER the handler completes so waiters created during a handler call aren't self-notified (prevents re-entrancy surprises where wait_for_event("X") inside an X handler would resolve against itself). Multiple concurrent waiters for the same event name all resolve with the same kwargs dict when that event fires — fan-out patterns work without manual coordination. Waiters for different event names are fully independent. A predicate that raises is treated as "no match" and logged via the djust.waiters logger, so a buggy predicate can't crash the event pipeline or deadlock a background task. 18 new Python tests covering: basic resolution, kwargs copy semantics, no-op on unmatched names, predicate filtering, predicate-that-raises treated as False with warning log, predicate=None matches any kwargs, timeout raises asyncio.TimeoutError, expired waiters removed from registry, indefinite waits without timeout, concurrent waiters for same event all resolve, waiters for different events are independent, partial resolution (some predicates match, others don't), _cancel_all_waiters unblocks pending futures with CancelledError and clears the registry, task cancellation removes the waiter, and stability under mid-iteration waiter-list mutation. Full documentation in the existing docs/website/guides/server-driven-ui.md guide with signature, predicate examples, concurrency semantics, timeouts and cleanup, composition with push_commands, and honest limitations (no component-event support yet, actor mode bypasses the hook, validation failures prevent handler execution which means waiters never resolve except via timeout).

  • LiveView.push_commands(chain) + djust:exec client-side auto-executor (ADR-002 Phase 1a) — First half of the backend-driven UI primitives proposed in ADR-002. Adds a one-line server-side helper self.push_commands(chain) that takes a djust.js.JSChain (shipped in v0.4.1 as the JS Commands fluent API) and pushes it to the current session as a djust:exec push event carrying the chain's JSON-serialized ops list. The client half is a new framework-provided src/27-exec-listener.js module that listens for djust:push_event CustomEvents on window, filters for event === 'djust:exec', and runs the ops via window.djust.js._executeOps(ops, document.body) — the same function that runs inline dj-click="[[...]]" JSON chains and fluent-API .exec() calls from hook code. No hook registration, no template markup, no user setup required: the auto-executor ships bound with client.js and is active on every djust page automatically. The server-side helper is type-safe — it rejects anything that isn't a JSChain with a clear TypeError pointing at the JS.* factory methods, preventing raw ops-list smuggling through the push_event path. push_commands and push_event share the same queue and preserve ordering, so handlers can interleave "push a flash message, add a CSS class, fire analytics, run an animation" in one deterministic sequence. 23 new Python tests covering single-op chains, multi-op ordering, empty chains, JSON round-trip, immutability of chains after push, type validation against strings/dicts/lists/None, queue composition with push_event, and per-op factory parity across all 11 JS Commands. 13 new JS tests in tests/js/exec-listener.test.js covering listener registration, single-op execution, multi-op ordering, multiple-class add_class, focus, dispatch with detail, filtering for non-djust:exec events, malformed-payload rejection (missing ops, non-array ops, missing detail), error resilience (one bad op doesn't break the chain), multiple independent exec fires, and end-to-end integration with the fluent window.djust.js chain factory. Zero new runtime dependencies. Full documentation in docs/website/guides/server-driven-ui.md with patterns, debugging tips, and pointers to Phase 1b (wait_for_event) and Phase 1c (TutorialMixin) still to come in v0.4.2.

All releases · Atom feed