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¶
stateDiagram-v2
[*] --> Account
state "Verify" as Verify
Verify : on enter#58; send_code
state "Done" as Done
Done : outcome#58; success
Account --> Profile : Next<br/>[email != '']<br/>/ context.email = event.email
Profile --> Account : Back
Profile --> Verify : Next<br/>[full_name != '']<br/>/ context.full_name = event.full_name
Verify --> Profile : Back
Verify --> Done : Verified<br/>[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 anAsyncDurableRunnerover anAsyncSqliteStore(wizard.db) on first use, so the store opens on NiceGUI’s own event loop;_execution()loads this session’s execution (its id is inapp.storage.user) or creates one;wizard_ui(), a refreshable, renders the step named byexe.active_path;each button’s handler turns the click into an
Event, awaitsrunner.process(...), and awaits the refresh.
A click:
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 defhandlers. Each store call is awaited, and while one session’s click waits on it, the loop serves every other session; the syncsend_coderuns in a thread pool, so it doesn’t stall the loop either. A syncDurableRunnerwould 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¶
pip install -r examples/nicegui_wizard/requirements.txt
python -m examples.nicegui_wizard.app # open http://localhost:8080