Examples
State storm
About
StateStorm: a busy dashboard that puts the browser runtime under load.
quillo run storm.qwe --watch then open http://localhost:8080
Press "Storm" and every 16 ms something changes at random: items are added,
removed and shuffled (a keyed list), tabs switch (whole subtrees mount and
unmount), items open and close (each open one runs its own timer), text
changes. The parts use every kind of reactive state there is: ~state,
props and callbacks, every timers, url(), live() data and bound inputs.
"Reset" puts everything back, so a test can check that nothing that was
removed is still held on to (tests/browser/storm.js).
Run it
$ quillo run storm.qwe --watchSource
storm.qwe
let visits = 0// Live data: every page shows the same count, updated when it changes.server fn visit_count() = visitsserver fn add_visit() { visits += 1 visits}let tabs = ["list", "grid", "form"]fn initial_items() = range(0, 12) | map((i) => { id: i, label: `Item ${i}` })component Ticker(label: str, ms: int) { state n = 0 every(ms, () => { n += 1 }) <span.ticker>{label} <b>{n}</b></span>}component Counter(id: int, on_change) { state n = 0 <div.counter> <button @click={() => { n += 1 on_change(id, n) }}>+</button> <b>{n}</b> </div>}component Item(item: map, open: bool, on_toggle, on_remove) { state clicks = 0 <li.item class={open && "open"}> <button.link @click={() => { clicks += 1 on_toggle(item.id) }}>{item.label}</button> {open && <Ticker label="open for" ms={50}/>} <span.muted>{clicks} clicks</span> <button.remove @click={() => { on_remove(item.id) }}>×</button> </li>}component Where() { let u = url() <span.where>at {u.path}</span>}component Visits() { state count = live(visit_count) <div.visits> Visits: <b>{count ?? "…"}</b> <button @click={() => { add_visit() }}>Visit</button> </div>}view Panel(title: str, children) = <section.panel> <h3>{title}</h3> {children} </section>component Storm() { state items = initial_items() state next_id = 12 state open = [] state tab = "list" state storm = false state changes = 0 state text = "hello" state totals = {} let toggle = (id) => { open = any(open, (o) => o == id) ? open | filter((o) => o != id) : open + [id] } let remove = (id) => { items = items | filter((it) => it.id != id) open = open | filter((o) => o != id) } let counted = (id, n) => { totals = set(totals, `${id}`, n) } let shake = () => { let r = random_int(0, 6) if r == 0 { items = items + [{ id: next_id, label: `Item ${next_id}` }] next_id += 1 } if r == 1 && len(items) > 0 { remove(items[random_int(0, len(items) - 1)].id) } if r == 2 { items = items | sort_by((_) => random()) } if r == 3 { tab = tabs[random_int(0, 2)] } if r == 4 && len(items) > 0 { toggle(items[random_int(0, len(items) - 1)].id) } if r >= 5 { text = `change ${changes}` } changes += 1 } if storm { every(16, shake) } <div.storm> <header> <h1>StateStorm</h1> <button.primary @click={() => { storm = !storm }}>{storm ? "Calm" : "Storm"}</button> <button @click={() => { storm = false items = initial_items() next_id = 12 open = [] tab = "list" text = "hello" totals = {} }}>Reset</button> <span.stats><b.changes>{changes}</b> changes · <b.count>{len(items)}</b> items · <b.opened>{len(open)}</b> open</span> </header> <aside> {for t in tabs => <button key={t} class={t == tab && "active"} @click={() => { tab = t }}>{t}</button>} <Where/> </aside> <main> {match tab { "list" => <ul.items>{for item in items => <Item key={item.id} {item} open={any(open, (o) => o == item.id)} on_toggle={toggle} on_remove={remove}/>}</ul> "grid" => <div.grid>{for i in range(0, 9) => <Panel key={i} title={`Panel ${i}`}> <Ticker label="ticks" ms={40 + i * 10}/> <Counter id={i} on_change={counted}/> {i == 4 && <Visits/>} </Panel>} </div> _ => <form.form @submit={(e) => {}}> <label>Text <input bind:value={text}/></label> <p>{text} ({len(text)} characters)</p> <p>Totals: {len(keys(totals))} counters used</p> </form> }} </main> </div>}let style = css`* { box-sizing: border-box; }body { font: 14px/1.4 system-ui, sans-serif; margin: 0; color: #222; }.storm { display: grid; grid-template-columns: 9rem 1fr; grid-template-rows: auto 1fr; min-height: 100vh; }header { grid-column: 1 / -1; display: flex; gap: .75rem; align-items: center; padding: .6rem 1rem; border-bottom: 1px solid #e5e5e5; }header h1 { font-size: 1.1rem; margin: 0 1rem 0 0; }.stats { margin-left: auto; color: #666; font-variant-numeric: tabular-nums; }aside { display: flex; flex-direction: column; gap: .3rem; padding: 1rem; border-right: 1px solid #eee; }aside .where { margin-top: 1rem; color: #888; font-size: 12px; }main { padding: 1rem; overflow: auto; }button { font: inherit; padding: .25rem .7rem; border-radius: 6px; border: 1px solid #ccc; background: white; cursor: pointer; }button.primary { background: #222; color: white; border-color: #222; min-width: 5rem; }button.active { border-color: #222; font-weight: 600; }button.link { border: none; background: none; padding: 0; min-width: 5.5rem; text-align: left; }.items { list-style: none; padding: 0; margin: 0; columns: 2; }.item { display: flex; gap: .6rem; align-items: center; padding: .3rem .4rem; border-radius: 6px; break-inside: avoid; }.item.open { background: #eef6f0; }.item .remove { margin-left: auto; padding: 0 .45rem; }.muted, .ticker { color: #888; font-size: 12px; font-variant-numeric: tabular-nums; }.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: .75rem; }.panel { border: 1px solid #e5e5e5; border-radius: 8px; padding: .25rem .75rem .75rem; display: flex; flex-direction: column; gap: .4rem; }.panel h3 { font-size: .9rem; margin: .5rem 0 .25rem; color: #555; }.counter { display: flex; gap: .5rem; align-items: center; }.form { display: flex; flex-direction: column; gap: .5rem; max-width: 24rem; }@media (prefers-color-scheme: dark) { body { background: #161616; color: #ddd; } header, aside, .panel { border-color: #2a2a2a; } button { background: #222; color: #ddd; border-color: #444; } button.primary { background: #ddd; color: #111; } .item.open { background: #1d2a21; }}`route GET / = <html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>StateStorm</title> <style>{style}</style> </head> <body><Storm/></body> </html>Thread
Thread 0 posts
On State storm: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.