6. The create dialog¶
This part adds a New-issue dialog that POSTs a command, clears itself, and closes. This part completes the app.
The final component¶
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING
from situ import Local, Server, Url
if TYPE_CHECKING:
from typing import Protocol
class IssuesFacade(Protocol):
"""The server-sited operations the handlers call (impl: IssueStore)."""
async def create(self, title: str) -> None: ...
async def set_status(self, issue_id: int, status: str) -> None: ...
reset: Callable[..., None] # injected at exec: clears local signals to their defaults
search: Local[str] = "" # the live-search box — client-only
selected_id: Local[int] = 0 # which row's detail is open (0 = none)
dialog_open: Local[bool] = False
new_title: Local[str] = ""
filter: Url[str] = "all" # all | open | closed — a shareable link
issues: Server[IssuesFacade] # the store: a facade in handlers, a row list in the template
async def create(new_title):
await issues.create(new_title)
reset("new_title", "dialog_open") # clear the input, close the dialog
async def close(id):
await issues.set_status(id, "closed")
async def reopen(id):
await issues.set_status(id, "open")
<section class="tracker">
<header>
<input :bind="search" placeholder="Search title…" autocomplete="off">
<button class="primary" @click="dialog_open = True">New issue</button>
<div class="dialog" :show="dialog_open">
<form @submit="create">
<input :bind="new_title" name="new_title" placeholder="Title" autocomplete="off">
<button class="primary">Create</button>
<button type="button" @click="dialog_open = False">Cancel</button>
</form>
</div>
</header>
<div data-region>
<nav>
<a href="?filter=all" class="{{ 'selected' if filter == 'all' else '' }}">all</a>
<a href="?filter=open" class="{{ 'selected' if filter == 'open' else '' }}">open</a>
<a href="?filter=closed" class="{{ 'selected' if filter == 'closed' else '' }}">closed</a>
<span class="count">{{ issues | length }} shown</span>
</nav>
<ul class="issue-list">
{% for t in issues %}
<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>
{% endfor %}
</ul>
<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>
{% if t.status == 'open' %}
<button @click="close(t.id)">Close</button>
{% else %}
<button @click="reopen(t.id)">Reopen</button>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>
The new pieces:
- The dialog lives in
<header>. A command's region swap would tear down and rebuild anything inside the region, an open dialog included. The header survives swaps, so overlays, search boxes, and toasts belong there. (This is the two-roots rule from the Quickstart.) @submit="create"on the<form>, with the input carryingname="new_title". A submitted form sends its fields, and the handler'snew_titleparameter binds from them. Notetype="button"on Cancel: buttons inside a form default totype=submit, so an untyped Cancel would submit the form.reset("new_title", "dialog_open")in the handler: the command's response carries a signal patch (theX-Siting-Signalsheader) that resets both signals to their defaults. The input clears and the dialog closes, with zero client code for either.
Restart: New issue → type a title → Create. One POST to /cmd/create, the list grows, the input is empty, the dialog is gone.

What you built, by the numbers¶
| Signal | Site | What it cost you |
|---|---|---|
search |
Local |
one declaration + two binders; filtering is compiled JS, zero network |
selected_id |
Local |
one declaration; selection survives command swaps for free |
dialog_open, new_title |
Local |
a dialog with self-clearing state |
filter |
Url |
three <a> tags; shareable, reload-surviving |
issues |
Server |
a store, a facade Protocol, a context(), and three handlers that became three POST routes |
Hand-written JavaScript: zero. The generated island for the finished app is ~6.8 KB; every line of it is readable at /island.js.
Where next¶
- Split it into components. The header, the list and the detail pane want to be separate files as this grows. Composition does that at compile time, leaving the island the same program.
- Make it multi-client.
mount_component(..., live=True)broadcasts mutations to every connected browser; see the wire protocol. - Style it with the kit. The component kit replaces the tutorial CSS with a design system, and
<Dialog>/<Combobox>/<DataGrid>replace hand-rolled widgets. - Generate it. declui produces this app's whole shape (list, detail, gated actions) from one typed model; the capstone example is this tracker, grown up.
- Read the flagship.
demos/issuesis this same app decomposed into a five-component tree with multi-select and bulk commands: run the demos.