Skip to content

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

  1. selected_id_from_query — keep the selected row’s id in the URL for deep links.
  2. save_intent_counter — monotonic counter so each save starts fresh async work under a new logical key.
  3. match_task_many — declarative UI for all handles done, any error, or any cancelled when several submit calls 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()))

CLI shell

streamtree init ./myapp --template crud --name "My app"

See also