# nicegui_wizard — a form that survives reloads and restarts ## What it does A multi-step onboarding wizard in the browser — account, profile, a verification code — whose state lives on the server, in a durable store, keyed to the browser session. Reload the page, or stop and restart the server, and the user is back on the step they had reached, with what they had typed. The machine refuses to advance on an empty field, and only completes when the typed code matches the one it sent. ## The machine ```{mermaid} stateDiagram-v2 [*] --> Account state "Verify" as Verify Verify : on enter#58; send_code state "Done" as Done Done : outcome#58; success Account --> Profile : Next
[email != '']
/ context.email = event.email Profile --> Account : Back Profile --> Verify : Next
[full_name != '']
/ context.full_name = event.full_name Verify --> Profile : Back Verify --> Done : Verified
[code == context.code] Done --> [*] ``` The machine owns the flow and its rules: each `Next` carries the field of the step it leaves, a named guard (`account_ok`, `profile_ok`) refuses an empty one, and `set` keeps it in the typed context. Entering `Verify` runs `send_code`, which puts a code in the context; `Verified` only completes `where event.code == context.code`. ## How it is built - `wizard.stm` — the machine. - `actions.py` — `send_code`, which simulates emailing a code (the demo shows it on the page). - `app.py` — the NiceGUI glue, about 40 lines: - `runner()` builds an `AsyncDurableRunner` over an `AsyncSqliteStore` (`wizard.db`) on first use, so the store opens on NiceGUI's own event loop; - `_execution()` loads this session's execution (its id is in `app.storage.user`) or creates one; - `wizard_ui()`, a refreshable, renders the step named by `exe.active_path`; - each button's handler turns the click into an `Event`, awaits `runner.process(...)`, and awaits the refresh. A click: ```{mermaid} sequenceDiagram actor B as Browser participant L as NiceGUI event loop participant R as AsyncDurableRunner participant S as AsyncSqliteStore participant P as thread pool B->>L: click Next (email) L->>R: await process(id, Next) R->>S: await load(id) Note over L: while the store works, the loop serves other browsers R->>R: Account to Profile, set context.email R->>S: await commit(exe v+1) R-->>L: exe (Profile) L->>B: re-render the Profile step B->>L: click Next (full name) L->>R: await process(id, Next) R->>P: send_code (a sync action) P-->>R: returns R->>S: await commit(exe: Verify, code in the context) L->>B: re-render the Verify step ``` ## How it runs - **Semantics: synchronous** — `AsyncDurableRunner`: a click must show the step it led to, so the handler waits for the event to be processed before it re-renders. - **Execution model: coroutines.** NiceGUI calls its page functions and handlers on its asyncio event loop, so the app uses harel's async API, awaited from `async def` handlers. Each store call is awaited, and while one session's click waits on it, the loop serves every other session; the sync `send_code` runs in a thread pool, so it doesn't stall the loop either. A sync `DurableRunner` would block the loop for the whole call — and harel refuses a sync call from inside a running loop for that reason. - **Durability**: every click is a committed checkpoint in `wizard.db`, which is why a restart loses nothing; delete it (and NiceGUI's `.nicegui/`) to start over. ## Run it ```text pip install -r examples/nicegui_wizard/requirements.txt python -m examples.nicegui_wizard.app # open http://localhost:8080 ```