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:
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.