List / detail / save (CRUD-shaped flows)
Goal
Ship list → detail → save flows with DataGrid, URL id, save_intent_counter,
and match_task_many without adopting a heavyweight admin framework.
Read the pattern doc first
Phase 3 CRUD patterns is the authoritative narrative. This recipe only orients you to the moving parts.
Moving parts
selected_id_from_query— keep the selected row’s id in the URL for deep links.save_intent_counter— monotonic counter so each save starts fresh async work under a new logical key.match_task_many— declarative UI for all handles done, any error, or any cancelled when severalsubmitcalls are in flight.
Full demos (embedded)
Pattern demo (in-memory list + parallel reference fetches):
"""In-memory CRUD + ``match_task_many`` / ``submit`` pattern (Phase 3 reference).
Run: ``streamlit run examples/crud_pattern_demo.py``
See ``docs/PHASE3_CRUD.md`` for how this maps to ``DataGrid``, URL filters, and optional extras.
"""
from __future__ import annotations
import time
from typing import Any
from streamtree import asyncio, component, render
from streamtree.core.element import Element
from streamtree.elements import Button, Form, Page, Text, TextInput, VStack
from streamtree.loading import match_task_many
from streamtree.state import state
def _rows() -> list[dict[str, Any]]:
return [{"id": 1, "name": "Alpha"}, {"id": 2, "name": "Beta"}]
@component
def CrudPatternDemo() -> Element:
rows = state(list(_rows()), key="crud_rows")
selected_id = state(1, key="crud_sel")
name_edit = state("Alpha", key="crud_name")
def sync_refs() -> tuple[int, int]:
time.sleep(0.08)
return (1, 2)
h1 = asyncio.submit(lambda: sync_refs()[0], key="crud_prefetch_a")
h2 = asyncio.submit(lambda: sync_refs()[1], key="crud_prefetch_b")
sync_banner = match_task_many(
(h1, h2),
loading=VStack(Text("Loading reference data…")),
ready=lambda _: VStack(Text("Reference checks complete.")),
error=VStack(Text("Reference load failed.")),
)
def select_row(rid: int, current_name: str) -> None:
selected_id.set(rid)
name_edit.set(current_name)
def apply_edit() -> None:
rid = int(selected_id())
name = str(name_edit()).strip()
if not name:
return
updated = [{**r, "name": name} if r["id"] == rid else r for r in rows()]
rows.set(updated)
def add_row() -> None:
name = str(name_edit()).strip() or "New"
nxt = max((r["id"] for r in rows()), default=0) + 1
rows.set([*rows(), {"id": nxt, "name": name}])
selected_id.set(nxt)
row_buttons: list[Element] = []
for r in rows():
rid, nm = r["id"], r["name"]
row_buttons.append(
Button(
f"{rid}: {nm}",
on_click=lambda rid=rid, nm=nm: select_row(rid, nm),
)
)
return VStack(
Text("## CRUD pattern (in-memory)"),
sync_banner,
Text("Select a row, edit the name, then **Apply**."),
*row_buttons,
Form(
TextInput("Name", value=name_edit),
Button("Apply edit", on_click=apply_edit, submit=True),
Button("Add row (uses name field)", on_click=add_row, submit=True),
form_key="crud_form",
),
)
if __name__ == "__main__":
render(Page(CrudPatternDemo()))
Automation demo (CRUD helpers beside normal state):
"""CRUD helpers: URL id + save-intent counter (Phase 3).
Run: ``streamlit run examples/crud_automation_demo.py``
Uses :mod:`streamtree.crud` alongside normal ``state`` for row data. See ``docs/PHASE3_CRUD.md``.
"""
from __future__ import annotations
from streamtree import component, render
from streamtree.core.element import Element
from streamtree.crud import save_intent_counter, selected_id_from_query
from streamtree.elements import Button, Page, Text, TextInput, VStack
from streamtree.routing import set_query_value
from streamtree.state import state
@component
def CrudAutomationDemo() -> Element:
qid = selected_id_from_query(param="id", default="1")
name = state("Alpha", key="crud_auto_name")
save_count, bump_save = save_intent_counter(key="crud_auto_save")
def pick(id_str: str, nm: str) -> None:
set_query_value(id_str, param="id")
name.set(nm)
return Page(
VStack(
Text("## CRUD automation helpers"),
Text(f"Query id: {qid!r} — bump save intent: {save_count()}"),
Button("Pick id=1 / Alpha", on_click=lambda: pick("1", "Alpha")),
Button("Pick id=2 / Beta", on_click=lambda: pick("2", "Beta")),
TextInput("Name", value=name),
Button("Simulate save click", on_click=bump_save),
)
)
if __name__ == "__main__":
render(Page(CrudAutomationDemo()))