4. Selection & a detail pane¶
This part makes a row click show its detail and highlight the selection, all client-side.
A selection signal¶
search: Local[str] = ""
selected_id: Local[int] = 0 # which row's detail is open (0 = none)
filter: Url[str] = "all"
issues: Server[object]
Make each row clickable and mark the selected one:
<li data-row="IssueRow" data-id="{{ t.id }}" data-title="{{ t.title }}"
:show="search.lower() in t.title.lower()"
:class="selected: selected_id == t.id">
<button @click="selected_id = t.id">{{ t.title }}</button>
<span class="status {{ t.status }}">{{ t.status }}</span>
</li>
and add the detail pane below the list, still inside the region:
<div class="detail">
{% for t in issues %}
<div data-row="IssueRow" data-id="{{ t.id }}" :show="selected_id == t.id">
<h2>{{ t.title }}</h2>
<p>#{{ t.id }} · {{ t.status }}</p>
</div>
{% endfor %}
</div>
Three compiled pieces:
@click="selected_id = t.id": an event action assigning aLocalfrom the row's projected id. Click; zero network.:class="selected: selected_id == t.id": toggles one class on a condition; the highlight follows the selection.:show="selected_id == t.id"on each detail block: only the selected issue's detail is visible.
Why == just works¶
t.id comes from a data-* attribute, so it arrives as a string: data-id="2" is "2", never 2, while selected_id is an int. So selected_id = t.id looks like it should put a string into an int signal without being noticed: the classic bug of every stringly-typed boundary.
It doesn't, because you declared the row:
data-row="IssueRow" tells the compiler what those DOM strings are, so it inserts the decode at the read: t.id compiles to an int, once, wherever it appears. selected_id therefore actually holds an int, selected_id == t.id is an ordinary int comparison: the type you wrote is the type you get. You never debug "2" !== 2, and you never debug "2" + 1 === "21" either.
Declare id: str instead and the compiler is equally happy: it just won't decode, because a string already is one. It will not guess.
The render-all pattern and its ceiling
This detail pane server-renders every issue's detail and shows one. That is fine here, where a detail is two lines. For rows with heavy payloads (message bodies, documents) this pattern ships everything to be hidden; the right move is to load the open item's body on demand through a server read. The mail demo does exactly that, and the unified idiom turns the mistake into a compile error.
Restart: click rows, watch the highlight and the pane follow, and confirm in the network tab that none of it touches the server.
Next comes Part 5 — commands, where the database changes.