2. Live search¶
Goal: a search box that filters rows as you type — with zero network traffic.
Declare a client signal¶
<section class="tracker">
<header>
<input :bind="search" placeholder="Search title…" autocomplete="off">
</header>
<div data-region>
<p class="count">{{ issues | length }} shown</p>
<ul class="issue-list">
{% for t in issues %}
<li data-id="{{ t.id }}" data-title="{{ t.title }}"
:show="search.lower() in t.title.lower()">
<span>{{ t.title }}</span>
<span class="status {{ t.status }}">{{ t.status }}</span>
</li>
{% endfor %}
</ul>
</div>
</section>
search is Local: the browser owns it. Two binders wire it up:
:bind="search"on the input — two-way: typing writes the signal, and a signal write repaints the input (with a focus guard, so nothing clobbers you mid-keystroke).:show="search.lower() in t.title.lower()"on each row — a compiled predicate that re-evaluates wheneversearchchanges.incompiles to.includes,.lower()to.toLowerCase().
The row expression reads t.title — a server row's field, on the client. That works through the projection you wrote yourself: data-title="{{ t.title }}" puts the field on the row element, and the compiler reads t.title from the nearest data-id ancestor's dataset at runtime. You choose, field by field, what a row exposes to the browser.
Restart and type. Open the network tab first: filtering triggers zero requests — the predicate lives in the island.
Also worth noticing: the input sits in <header>, outside the region. Both roots are wired at boot, and the header survives server re-renders — which starts to matter in part 5.
Meet the seam¶
Try to shortcut the count. Replace the <p class="count">…</p> line with:
Restart, reload, and the compiler stops you:
CompileError: server-sited 'issues' cannot be read on the client;
DB-backed state stays on the server
issues lives on the server; a client binder cannot read it — by construction, with no exceptions to remember. Server data reaches the browser three ways only: rendered by Jinja, projected per row through data-* attributes (as t.title above), or pushed into a client signal by a command. Put the Jinja count back and move on.
If rows refuse to hide
:show sets the hidden attribute, and any author display: on the same element overrides the browser's default [hidden] rule. The stylesheet from the setup guards this with [hidden] { display: none !important; } — keep that habit in your own apps.
Next: Part 3 — shareable filters, a signal that lives in the address bar.