Authoring patterns example¶
A single page that exercises the idioms from the Authoring patterns guide:
- Composition through
children(aCardcomponent built onParentPropsthat forwards anidwithomit). - State with
create_signaland derived values withcreate_memo. - Reactive list rendering with
Forover a draft-first store. - A mutation wrapped in
actionwith a pending indicator. - Cleanup with
on_settledandon_cleanup(a tickingTimer).
Full listing¶
from wybthon import (
For,
ParentProps,
Prop,
Show,
action,
button,
component,
create_memo,
create_signal,
create_store,
div,
h3,
input_,
li,
omit,
on_cleanup,
on_settled,
p,
prop,
render,
section,
span,
ul,
)
class CardProps(ParentProps):
title: Prop[str] = prop(default="")
id: Prop[str | None] = prop(default=None)
@component
def Card(props: CardProps):
return section(h3(props.title), props.children, class_="card", **omit(props, "title", "children"))
@component
def NamesList():
store, set_store = create_store({"names": []})
draft, set_draft = create_signal("")
total = create_memo(lambda: len(store.names))
starts_with_a = create_memo(lambda: sum(1 for n in store.names if n["text"].lower().startswith("a")))
@action
async def add(text: str):
# Simulate a slow save; the button is disabled while ``add.pending()``.
import asyncio
await asyncio.sleep(0.3)
set_store(lambda s: s.names.append({"id": len(s.names) + 1, "text": text}))
set_draft("")
def clear():
set_store(lambda s: s.names.clear())
return div(
p(t"Total: {total} | Starts with A: {starts_with_a}"),
div(
input_(value=draft, on_input=lambda e: set_draft(e.target.value), placeholder="Name"),
button("Add", on_click=lambda: add(draft.peek()), disabled=add.pending),
button("Clear", on_click=clear),
),
Show(add.pending, lambda: p("Saving...")),
ul(
For(
lambda: store.names,
lambda item, index: li(lambda: f"{index() + 1}. {item()['text']}"),
fallback=li("No names yet."),
keyed=lambda n: n["id"],
)
),
)
@component
def Timer():
seconds, set_seconds = create_signal(0)
def start():
from js import clearInterval, setInterval
from pyodide.ffi import create_proxy
proxy = create_proxy(lambda: set_seconds(lambda s: s + 1))
handle = setInterval(proxy, 1000)
def stop():
clearInterval(handle)
proxy.destroy()
return stop # cleanup runs on unmount
on_settled(start)
on_cleanup(lambda: print("Timer unmounted"))
return div(span(t"Seconds: {seconds}"), class_="timer")
@component
def Page():
show_timer, set_show_timer = create_signal(True)
return div(
Card(title="State and derived values", id="names")[NamesList()],
Card(title="Cleanup")[
button("Toggle timer", on_click=lambda: set_show_timer(lambda v: not v)),
Show(show_timer, lambda: Timer()),
],
)
render(Page(), "#app")
What to notice¶
CardsubclassesParentProps, so it accepts children, and declares the props it handles. It forwardsidby spreadingomit(props, "title", "children")onto the<section>, so the parent'sid="names"lands there. ANonevalue removes the attribute.Pagepasses each card's children with item syntax:Card(title=...)[...].NamesListkeeps its list in a store.set_store(lambda s: s.names.append(...))mutates a draft; only the leaf signals that changed notify, andFormatches rows byidso existing<li>elements are kept.- With a key function,
Forhands the row callback accessors for both the item and the index, so the row text readsitem()andindex()inside a hole. - The summary line is a t-string:
totalandstarts_with_aare memos, read inside one binding. addis anaction. While it's in flight,add.pending()isTrue, which disables the button and shows the "Saving..." line.- Handlers that don't need the event take no arguments (
on_click=clear);on_inputtakes the event to reade.target.value. Timerstarts its interval inon_settledand returns a cleanup from it, so the interval stops whenShowunmounts the timer.on_cleanupin the body runs at the same time.
Next steps¶
- Read Components and Lifecycle and Ownership.
- Browse the Counter example for a smaller starting point.
- See Stores for
reconcile,snapshot, and derived stores.