Partial rendering reference#
The patch verbs, the request and response headers, the data-next-* attributes, and the PARTIAL_BACKENDS settings, in tables.
For the narrative behind any of these, read the scenario that uses it in Partial rendering by scenario.
Patch verbs#
A patch is one addressed DOM operation with a verb, an optional target, optional HTML, and verb-specific extras.
The operations apply in list order.
The server is the only author of a target, the client never names one.
The envelope around the list always carries the assets and form keys, serialised as [] and null when empty, and the JSON examples in this section omit them.
Patches(request) opens the builder in a handler.
Patches.versioned(version) opens the same builder for code that holds no request, a test of a custom operation or a hand-assembled envelope.
Verb |
Builder method |
Semantics |
Default |
|---|---|---|---|
|
|
The default verb. Morph the target into the HTML. The target names a zone or a form by uid. |
|
|
|
Replace the node wholesale, no morph. |
none |
|
|
Replace only the contents, no morph. |
none |
|
|
Add children at the end, dedup by |
|
|
|
Add children at the start, dedup the same way. |
|
|
|
Remove the target. |
none |
|
|
Ask the client to re-fetch the zone with its own cookies. The safe default of an SSE fan-out. |
none |
|
|
Merge named serialize-provider values into |
none |
|
|
Dispatch a |
|
|
|
Show a toast, sugar over |
|
|
|
Open a layer from the server, optionally seeding a zone to fill later or an href whose zone loads into the modal. See Server-initiated layers. |
none |
|
|
Close the top layer with an accept result or a dismissal. |
accept, no result |
|
|
Push browser history. The href is validated by the server. The client also honours
|
|
|
|
A full client navigation to a server-authored href. |
none |
A target carries exactly one address key, and the client resolves zone, then form, then field, then css.
zone names a data-next-zone wrapper and form names an action uid.
field is a [uid, name] pair addressing one named input of a form by its uid.
css is a raw selector, the escape hatch a bare layer shell relies on.
A verb beyond this set is registered on both sides.
register_patch_op("confetti") on the server registers the name, which the next.E066 check validates at manage.py check, and earns the generic op() channel on the builder.
An unregistered name fails at runtime with UnknownPatchOpError.
Next.partial.defineOp("confetti", handler) on the client supplies the handler.
See Extending the protocol for the end-to-end recipe, the context and event seams, and the custom-verb exceptions.
An event name that starts with partial: or next:, or equals ready or context-updated, is reserved for the runtime lifecycle.
Patches.event() rejects such a name with ReservedEventNameError, symmetric to op() rejecting a built-in verb on the generic channel, so an application cannot forge a lifecycle event.
The $csrf and $dev keys of the init payload are reserved the same way.
Patches.context() rejects either name with ReservedContextKeyError, and the js-context delta of a zone render drops them before it becomes a context op.
The $ namespace therefore belongs to the framework on a patch exactly as it does on a full render.
A full render drops a page or component key of either name from the payload whether or not it has a framework value to write there, so no patch has a registered value to update.
Asset manifest#
The assets key of an envelope lists the co-located assets the rendered targets registered.
Each entry carries kind and url always, plus inline and load when they apply.
Field |
Value |
Semantics |
|---|---|---|
|
A registered asset kind |
The kind the asset was discovered under, |
|
A published URL |
The URL form of the asset. An inline asset carries an empty string here. |
|
An asset body |
The body of an inline asset, absent on a URL-form asset. |
|
|
The insertion verb, derived from the renderer registered for the kind. Absent when that renderer is a custom backend method, and absent on an inline body whose kind does not wrap it in the element the verb builds. |
The runtime inserts an asset by its verb rather than by its kind, so a custom kind registered with one of the three built-in renderers loads like the built-in kind that shares it.
The verb of an entry resolves in three steps.
A load field the server wrote wins.
A URL-form entry without that field falls back to the verb the name of a built-in kind implies, link for css, script for js, and module for module.
An envelope from a backend that spells no load field therefore still loads.
An entry carrying an inline body takes no such fallback, because the server spells the verb only when the kind wraps the body in the element the runtime builds.
A body with no explicit load is therefore dropped at the boundary rather than executed in an element a full page render prints verbatim.
An entry that resolves to no verb is skipped.
The next.W074 check reports a registered kind whose renderer implies no verb, and next.W076 a registered kind whose inline bodies lose the verb its URL form keeps.
Both checks walk the kinds registered in the running process, so an entry naming a kind no registration backs is skipped with no check to announce it.
See Asset kinds for the renderer-to-verb mapping.
Request headers#
Client to server. All values are ASCII, and zone names are ASCII slugs.
Header |
When |
Semantics |
|---|---|---|
|
Every intercepted request |
The partial switch. Without it the response is the full page, byte for byte. |
|
Every intercepted request |
|
|
Zone GET, refresh, filter |
The zones to render, comma-joined for a batch. |
|
Inline validation |
The field names to validate without saving. |
|
Pagination |
|
|
Every request once a version is learned |
The asset version the client holds. The first request of a page asserts none. |
|
Every mutation |
The ring id used to suppress an SSE echo. |
|
Every layer request, the open GET and the accept re-GET |
The path and query string of the page that hosts a layer, for a server-side morph of its zones. |
CSRF header |
Every unsafe method |
The name comes from |
Response headers#
Server to client.
Header |
When |
Semantics |
|---|---|---|
|
Every envelope |
|
|
Every partial-capable path |
|
|
Every envelope |
The current asset version. |
|
An invalid form |
The existing invalid-form contract. |
|
An invalid form |
The uid of the failed action. |
Status codes#
Status |
Semantics |
|---|---|
200 with an envelope |
Patches, including an invalid form. A validation error is state, not an HTTP failure. |
200 without an envelope |
The fetch passed through a redirect. The runtime performs a full navigation to |
204 |
A success with no patch to apply, for example a wizard advance with no redirect target. The runtime applies nothing. |
303 |
A mutation succeeded without the runtime, the existing full cycle. |
302 without an envelope |
A guard redirect served outside the shaping path. The runtime navigates fully to the final URL. |
403 without an envelope |
A guard denial or a CSRF failure served outside the shaping path. On a mutation the runtime
stays in place and fires |
400 |
An intent that did not validate: an unknown zone, a bad origin, a zone in a dynamic page body. |
404 |
An unknown form uid, the existing behaviour. |
409 |
A version mismatch on a safe method, with an empty body. The runtime fully visits the current URL. A mutation always runs, and a version mismatch surfaces in the envelope version, which the client reads to reload once into a full client visit. |
5xx |
No envelope. The runtime swaps nothing and fires |
A safe-method zone GET that answers with a non-envelope navigates once under a navigate-once flag, the same shape the version-mismatch reload uses.
A lazy="load" zone re-asks on the freshly loaded page, so an expired session, a WAF stub, or a maintenance page that keeps answering non-envelope would otherwise loop the navigation.
The second non-envelope while the flag stands degrades to a partial:error of kind network and leaves the page in place, and the flag clears the moment a correct envelope classifies.
Attributes#
The single namespace the runtime reads is data-next-*.
The form-behaviour attributes are written by the {% form %} tag from its parameters, not hand-authored as a string DSL.
Attribute |
On |
Semantics |
|---|---|---|
|
Zone wrapper |
The zone address, written by the |
|
Lazy zone wrapper, infinite-scroll sentinel |
|
|
Zone wrapper |
The poll interval in milliseconds, from the |
|
|
The action uid, written by |
|
|
Inline validation, source is the |
|
|
Route the response into a zone. On a GET filter it names the zone to morph,
and on a POST form it is written by the |
|
Filter |
The event that auto-submits a GET filter, |
|
With |
Debounce in milliseconds. |
|
Pagination link |
|
|
|
Open the href in a layer, the zone container is created before the request. |
|
The opening |
Re-fetch this zone on layer accept. |
|
Form, link |
A confirmation prompt before the request. |
|
Any element |
The morph leaves the node untouched, paired by id when present and by position otherwise. |
|
List rows, repeated |
The match key for the morph and for |
|
A container |
Subscribe to a patch stream at the URL. |
|
Layer opener, layer zone container |
Written during a layer open on the opener link and the layer’s zone
container, alongside |
|
Runtime |
Set by the runtime on every layer dialog, the styling hook for the modal shell. |
|
Runtime toast container |
The toast tray, created by the runtime on the first |
|
Runtime toast item |
One toast, the value is the variant, the styling hook for a single notification. |
Lifecycle events#
The runtime fires events on three channels, the element, the document, and the Next.on bus.
The next:* node events fire on the element as a bubbling CustomEvent caught with addEventListener.
The apply-stage partial:* events and next:toast fire on the document and the Next.on bus.
A partial:error of kind asset, raised when a co-located stylesheet fails to load or when the asset version still mismatches after the reload, reaches only the bus.
ready, context-updated, partial:before-request, and the fetch-stage partial:error reach only the bus.
The next:mounted, next:removed, and next:morph-* node events live only on document.addEventListener and never reach the bus, so Next.on("next:mounted") is a silent no-op.
Event |
Cancelable |
Detail |
|---|---|---|
|
No |
The existing core contract. |
|
No |
|
|
No |
|
|
Yes |
|
|
No |
|
|
No |
A discriminated union on |
|
No |
|
|
No |
|
|
No |
|
|
No |
Fired on each touched node, bubbles. The node is the event target. Pairs with |
|
No |
Fired on a node immediately before it detaches, bubbles, no detail. The unmount half of the island lifecycle, the place to tear down a mounted root or a timer. |
|
Yes |
Fired on the old node before a pair morphs. Detail |
|
Yes |
Fired on the old element before one attribute changes. Detail
|
|
No |
Detail |
The mount and morph events run during the patch apply, so a framework island can take over a node by vetoing its morph and managing its own subtree. The mounted and removed pair brackets the node’s life inside the document, the symmetry an adapter relies on to mount and unmount a root.
Client runtime#
The runtime exposes window.Next once the bundle loads.
The surface is small, and every entry mirrors a seam the runtime already uses internally.
Member |
Purpose |
|---|---|
|
Subscribe to a lifecycle event and receive a teardown function. A known event from the table above types its payload. A |
|
Run a plugin function with |
|
A frozen snapshot of the client context the server seeded and the |
|
Register a handler for a custom verb the server authors, dispatched through the same pipeline as the built-ins. |
|
A re-executable mount registry. The callback runs over the matching elements at load and over every matching element a later patch inserts. |
|
Register a parser that turns a foreign content type into an envelope before the apply pipeline, so a third party can emulate another wire format. |
|
Parse and apply a wire envelope directly, the entry a parse hook or a test feeds. |
|
Send one partial request through the wire’s queues and locks. |
|
The layer stack for driving modals from script: |
|
The stream registry: |
The runtime’s dev mode follows Django DEBUG.
Under the default auto script injection policy a full render seeds the $dev key of the init payload while DEBUG is on.
The runtime reads that key once at bootstrap, and every diagnostic this reference calls dev-only rides it.
In dev each applied patch shows up in the browser Performance panel as a next:apply:<label> measurement and prints the same span as a console.debug line.
The label is the zone the op addresses, read the way the verb itself reads it.
refresh prefers its top-level zone and falls back to the one in target, layer.open reads only its top-level zone, and every other verb reads target.zone.
An op that names no zone that way is labelled by its verb.
Dev also counts what the envelope boundary dropped, so a malformed op and a malformed asset each earn a console warning with the number dropped instead of vanishing.
An ops or assets value that is not an array is dropped whole and earns its own console warning naming the field, since the per-entry counts would otherwise report nothing wrong.
An asset whose insertion verb the envelope boundary cannot resolve is a console.debug skip naming its kind, because a kind with a custom renderer is a normal configuration rather than damage.
A production page carries no $dev key, so it carries neither the measurements nor any of the console lines.
Intercepting modals#
A data-next-layer link opens a modal over the current view and pushes the honest URL of the modal body.
The pushed URL is the real address of the body rather than a masked URL of the page beneath it.
A refresh or a shared link resolves that URL as its own standalone page through its own page.py, and Back closes the top layer.
There is no client router and no URL masking.
A single popstate handler closes the layer whose pushed URL the browser moved past.
data-next-confirm and data-next-layer combine on one link.
The confirm gate is a capture-phase click handler, the layer opener is a bubble-phase one, so the confirm runs first regardless of install order.
A cancelled confirm stops the click before it reaches the opener, so the layer never opens.
An accepted confirm lets the click through and the layer opens.
The same gate protects every click-driven trigger, so a prompt fronts a layer open the same way it fronts a pagination merge.
Server-initiated layers#
Patches.layer_open opens a layer from a handler, the server counterpart of the data-next-layer opener.
Its signature is layer_open(*, zone=None, href=None), and the two keywords select one of three forms.
A layer shows a zone of a page, uniformly.
There is no separate mechanism for a whole page in a modal.
A page that opens in a layer declares a zone with {% zone "name" %}, and that name travels to layer_open or to data-next-layer.
Call |
Effect |
|---|---|
|
Open a bare modal shell. Its container carries no zone name, so only a css-targeted patch can address it. Name a zone to fill the modal with zone patches. |
|
Open a layer whose zone container is named |
|
Fetch the |
A modal that shows a page’s content takes the third form.
def open_record(self, request: HttpRequest, record_id: int) -> HttpResponse:
"""Open the record's detail zone in a layer."""
return (
Patches(request)
.layer_open(href=f"/records/{record_id}/", zone="record")
.response()
)
An href without a zone raises LayerHrefWithoutZoneError.
A layer loads a zone, so an href that names no zone has nowhere to mount its content.
To open a page in a layer, wrap the page content in a zone and pass the zone name.
The href is validated same-site like every navigation sink, a cross-site value raises CrossSiteHrefError.
The client data-next-layer="record" opener and the server layer_open(href, zone) do the same work, both load a page zone into a layer.
Settings#
The partial subsystem reads PARTIAL_BACKENDS inside NEXT_FRAMEWORK.
The list holds the protocol backends, and only the first entry is active.
The rest are ignored, multi-backend selection is not supported, and a list with more than one entry earns the next.W071 warning at manage.py check.
"PARTIAL_BACKENDS": [
{
"BACKEND": "next.partial.PartialProtocolBackend",
"OPTIONS": {
"VERSION": "manifest",
"PUSH_WIZARD_STEPS": False,
"SSE": {
"HEARTBEAT_SECONDS": 25,
"RETRY_MS": 3000,
},
},
},
]
Key |
Default |
Meaning |
|---|---|---|
|
|
The source of |
|
|
The global default for pushing wizard steps to history. A wizard’s |
|
|
The heartbeat period for an async source. |
|
|
The |
See Settings for every key inside NEXT_FRAMEWORK.
Styling layers and toasts#
The runtime creates a bare <dialog data-next-dialog> for every layer and a <div data-next-toasts> container for toasts.
No framework CSS is applied.
The selectors are the hook.
[data-next-dialog] {
width: 100%;
max-width: 32rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
padding: 1.5rem;
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}
[data-next-dialog]::backdrop {
background-color: rgb(0 0 0 / 0.4);
}
[data-next-toasts] {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
[data-next-toast] { /* default variant */ }
[data-next-toast="success"] { /* success variant */ }
With Tailwind Play CDN @apply is available inside a <style type="text/tailwindcss"> block in the layout template.
<style type="text/tailwindcss">
[data-next-dialog] {
@apply w-full max-w-lg rounded-lg border border-border
bg-background text-foreground shadow-xl p-6;
}
[data-next-dialog]::backdrop {
@apply bg-black/40;
}
</style>
The next.dj examples use both patterns through the shared _shared/static/shared/css/base.css file.
See also#
See also
Partial rendering by scenario for each verb, header, and attribute in the context of a task. System checks for the zone and verb checks. Signals for the partial subsystem signals.