labb 0.5.0 is out

Reactivity

Reference for labb’s reactive components, signal bindings, and server request helpers.

Use these components and attributes to keep browser state, component props, and Django views in sync. The c-lbr. components wrap Datastar, which carries signals with requests and applies HTML updates. Start with the Reactivity overview if you want the guided version.

c-lbr.signals

Declare the state a page needs and load the reactivity runtime. Put the declaration near the top of the page body.

<!-- Scalars are strings, except "true" and "false", which become booleans. -->
<c-lbr.signals $count="0" $filters.q="" $open="false" />

<!-- Use a : binding for a number, list, object, or context value. -->
<c-lbr.signals :$config="{'page': 1, 'sort': 'name'}" />

<!-- A typed schema instance supplied by the view. -->
<c-lbr.signals :schema=signals syncQuery />

Use a plain $name="..." prop for a string. For a number, list, or object, add : before the prop name. For example, $page="1" declares the string "1", while :$page="1" declares the number 1.

Add ifmissing when a $ declaration holds state the browser owns after the first render, so a later morph does not reset it. A :schema declaration does this on its own. See who owns a signal.

Add syncQuery when filters, sorting, or pagination should survive a refresh and appear in a shareable URL. ReactivityMiddleware restores those values before the view reads its signals. See URL state for the pattern and its trade-offs.

Property Type Default Required Description
schema string - No A Signals instance from the view. labb declares every field, then sends only the fields the view assigned this request. Bind a form component with :bind=schema.fields.name to reuse the field path from the schema.
syncQuery boolean False No Persist the declared signals in the page URL on load and after each signal change. Requires ReactivityMiddleware, which restores those values before the view reads request.signals.
ifmissing boolean False No Seed only signals the browser does not already have. Without it a $ declaration is re-applied by every morph, resetting state the user changed. It has no effect with :schema.
Slot Name Description
default Not used. The component renders a hidden signal declaration.

You can declare nested signals with dynamic props such as $filters.q="". Give each declaration an id when a page has more than one <c-lbr.signals> element.

Reactive props

Prefix a component prop with a signal name to update it in the browser. The value after the colon supplies the first server-rendered value.

<c-lb.badge variant="$status:success" size="lg">Active</c-lb.badge>

labb recalculates the prop when $status changes. A reactive prop loads the runtime even without a separate action. labb also includes styles for the prop’s possible component variants in the generated CSS.

Client-side attributes

These Datastar attributes handle browser-only interactions. They work after a c-lbr. component or reactive prop loads the runtime. Expressions receive the event as evt and the element as el.

Attribute Use
bind="$filters.q" Keep a form component and a signal in sync. Accepts a $ path, a bare path such as filters.q, or schema.fields.q.
data-on:click="$open = !$open" Run an expression for a DOM event. Add __debounce.300ms or __throttle.50ms when the event fires often.
data-show="$open" Show or hide an element.
data-text="$label" Replace an element’s text.
data-class="{'active': $open}" Add or remove classes. A key can contain several space-separated classes.
data-attr:href="$url" Set an attribute from a signal.

c-lbr.get

Send a GET request when the wrapped element fires its event.

<c-lbr.get to="todos:detail" pk=todo.pk>
    <c-lb.button>Open</c-lb.button>
</c-lbr.get>
Property Type Default Required Description
to string - Yes Django URL name such as "todos:detail", or a direct URL such as "/todos/1/". Paths and absolute URLs pass through unchanged. labb resolves other values with Django reverse().
pk string - No Adds a pk URL kwarg when kwargs does not supply one. Has no effect for a direct URL.
kwargs string - No URL kwargs as a dictionary from context or a JSON string. Has no effect for a direct URL.
on string click No Browser event that sends the request, such as "click" or "input__debounce.300ms". Use "init" to send it when the page loads.
tag string div No HTML element that wraps the content.
before string - No Datastar expression to run before the request, for example a signal update.
options string - No Datastar options passed to @get(), such as "{openWhenHidden:true}".
replaceUrl boolean False No Replace the current history entry with the request URL after the event fires.
pushUrl boolean False No Add the request URL as a new browser history entry after the event fires.
preserveQuery boolean False No Keep the current query string when replaceUrl or pushUrl updates the history entry.
Slot Name Description
default The request trigger and any nested content.

c-lbr.post

Send a POST request. On a form, it runs on submit and sends the current signal bag as JSON, which you read from request.signals. labb handles CSRF protection. Pass options="{contentType: 'form'}" when a view expects a classic form body in request.POST.

<c-lbr.signals $text="" />

<c-lbr.post to="todos:create">
    <c-lb.input type="text" bind="$text" required />
    <c-lb.button type="submit" variant="primary">Add</c-lb.button>
</c-lbr.post>
Property Type Default Required Description
to string - Yes Django URL name such as "todos:detail", or a direct URL such as "/todos/1/". Paths and absolute URLs pass through unchanged. labb resolves other values with Django reverse().
pk string - No Adds a pk URL kwarg when kwargs does not supply one. Has no effect for a direct URL.
kwargs string - No URL kwargs as a dictionary from context or a JSON string. Has no effect for a direct URL.
on string submit No Browser event that sends the request. The default "submit" prevents the native form submission. Use "init" to send it when the page loads.
tag string form No HTML element that wraps the content. A form handles Enter and native form semantics, while the reactive request still sends signals as JSON.
before string - No Datastar expression to run before the request.
options string - No Datastar options passed to @post(). Use "{contentType: 'form'}" when the view reads a classic form body from request.POST instead of request.signals.
noCSRF boolean False No Skip labb’s CSRF header. Use this only for an endpoint that you explicitly exempt from Django CSRF protection.
Slot Name Description
default Form controls or other content inside the wrapper.

c-lbr.delete

Send a DELETE request. Add confirm when the browser should ask before it sends the request.

<c-lbr.delete to="todos:delete" pk=todo.pk confirm="Delete this todo?">
    <c-lb.button variant="error">Delete</c-lb.button>
</c-lbr.delete>
Property Type Default Required Description
to string - Yes Django URL name such as "todos:detail", or a direct URL such as "/todos/1/". Paths and absolute URLs pass through unchanged. labb resolves other values with Django reverse().
pk string - No Adds a pk URL kwarg when kwargs does not supply one. Has no effect for a direct URL.
kwargs string - No URL kwargs as a dictionary from context or a JSON string. Has no effect for a direct URL.
on string click No Browser event that sends the request. Use "init" to send it when the page loads.
tag string span No HTML element that wraps the content.
before string - No Datastar expression to run before the request.
confirm string - No Message for the browser confirmation dialog. Canceling the dialog prevents the request.
options string - No Datastar options passed to @delete().
noCSRF boolean False No Skip labb’s CSRF header. Use this only for an endpoint that you explicitly exempt from Django CSRF protection.
Slot Name Description
default The delete trigger and any nested content.

c-lbr.target

Name a stable update region. In a Django response, @name resolves to this element.

<c-lbr.target name="results">
    ...
</c-lbr.target>
Property Type Default Required Description
name string - Yes Name of the target. labb renders it as data-lbr-target="name" so a response can address it with @name.
tag string div No HTML element that becomes the target region.
Slot Name Description
default Content inside the named update region.

c-lbr.replace-url

Replace the current address without navigating. Use it when an action changes a route or record URL. It does not serialize signal state. Use syncQuery on <c-lbr.signals> for filters, sorting, and pagination.

<c-lbr.replace-url to="/settings/billing/" />
Property Type Default Required Description
to string - Yes Django URL name such as "todos:detail", or a direct URL such as "/todos/1/". Paths and absolute URLs pass through unchanged. labb resolves other values with Django reverse().
pk string - No Adds a pk URL kwarg when kwargs does not supply one. Has no effect for a direct URL.
kwargs string - No URL kwargs as a dictionary from context or a JSON string. Has no effect for a direct URL.
push boolean False No Add a new history entry when the target pathname differs from the current one. Replaces the current entry when the pathname already matches.

In a Django view

Reactive requests include the current signal bag. Read it from request.signals.

def index(request):
    q = request.signals.get("filters", {}).get("q", "")
    return render(request, "todos/index.html", {"todos": search(q)})
Object Use
request.signals The current signals as a dictionary.
request.is_datastar True for a reactive request. Use it when a view needs an in-place response for Datastar and a full-page response otherwise.

ReactivityMiddleware reads the signals before the view runs. Add it to MIDDLEWARE:

settings.py
MIDDLEWARE = [
    # ...
    "labb.middleware.ReactivityMiddleware",
]

Typed signals

Define a Signals class when you want validated, typed fields instead of reading the dictionary by hand.

from labb.signals import Signals, Str, Int

class TodoSignals(Signals):
    q = Str(path="filters.q", default="")
    page = Int(default=1, min_value=1)

def index(request):
    signals = TodoSignals(request)
    return render(request, "todos/index.html", {"signals": signals, "todos": search(signals.q)})

Pass the instance to <c-lbr.signals :schema=signals /> to declare the same state in the browser.

Sending a change back

Assigning to a field is how a view changes a signal. labb sends that field back only when its value differs and leaves the other schema signals unchanged.

s = TodoSignals(request)
s.page = min(s.page, total_pages)   # sent only if the clamp did something
Method What it does
s.mark_changed("page") Send a field even though its value already matches what the browser sent
s.mark_changed() Send every field
s.changed Names of the fields selected to send back
s.changed_signals_dict() Those fields as a nested signal dict

mark_changed covers the two cases an assignment cannot express: overwriting a value the browser sent, and mutating a Dict or List field in place.

Sending a forced patch

An HTML morph reapplies a changed signal declaration only when its rendered attribute changes. When the server must apply the same value more than once, return a signal patch through SSEResponse instead:

from labb.reactivity import SSEResponse

def reset_page(request):
    signals = TodoSignals(request)
    signals.page = 1
    return SSEResponse([signals.patch("page")])

signals.patch() emits a Datastar signal event every time the response yields it. Pass field names to patch only those fields, or omit them to patch the whole schema.

Schema fields in templates

signals.q is the parsed value that your view uses. signals.fields.q is its SignalField descriptor. It carries the field’s declared path, so you can pass it directly to a form component with a Cotton binding.

<c-lbr.signals :schema=signals />

<c-lb.input type="search" :bind=signals.fields.q placeholder="Search" />

The input reads the descriptor’s filters.q path and renders data-bind:filters.q. That keeps the schema, template binding, and server-side value on the same name. Use a string path such as bind="$filters.q" for small page-local state.

A bind path may contain only word characters and dots. labb validates the rendered path, so a template-built path such as bind="$selected." works when it renders to a valid path.