Counter¶
The specification’s counter, plus the interaction patterns you use every day.
counter/
├── app.py # composition root
└── views/
├── components/ # counter.py (Counter), greeter.py (Greeter)
├── pages/ # home.py, on a Router
└── styles/
1. A component with props and state¶
class Counter(Component):
"""A labelled counter. ``label`` and ``step`` are props; ``count`` is per-instance state."""
label: str = "Clicks"
step: int = 1
count = State(0)
def increment(self):
self.count += self.step
def reset(self):
self.count = 0
def render(self):
return Card(
Text(self.label, muted=True, size="sm"),
Text(f"{self.count}", style=COUNT, aria_live="polite"),
Row(
Button(f"+{self.step}", on_click=self.increment),
Button("Reset", variant="outline", on_click=self.reset, disabled=self.count == 0),
),
)
label and step are props, set by whoever uses the component. count is state, owned
by each instance. disabled=self.count == 0 shows that rendering is plain Python: the button
is disabled whenever the condition holds.
2. Typing, debounced¶
class Greeter(Component):
"""Typing sends the (debounced) value to a typed handler; the greeting re-renders."""
name = State("")
def set_name(self, value: str):
self.name = value.strip()[:40]
def render(self):
return Card(
Input("Your name", placeholder="Ada", on_input=self.set_name, autocomplete="off"),
Text(f"Hello, {self.name or 'stranger'}!"),
)
on_input waits until typing pauses (150 ms), then sends the value. The value: str
annotation validates it before set_name runs.
3. The page¶
views/pages/home.py¶
from rational import Button, Container, Heading, Page, Router, Row, js
from views.components import Counter, Greeter
from views.styles import PAGE
router = Router()
@router.page("/")
class Home(Page):
title = "Counter · Rational Web"
def render(self):
return Container(
Row(
Heading("Counter", level=1),
Button("Toggle theme", variant="ghost", on_click=js.toggle_theme()), # runs in the browser
justify="between",
),
Row(
Counter(key="one"), # different keys: each counter keeps its own count
Counter(key="ten", label="Tens", step=10),
collapse="mobile",
align="stretch",
),
Greeter(),
tag="main",
max_width=720,
style=PAGE,
)
The two counters have different key= values, so each keeps its own count.
js.toggle_theme() runs in the browser: no request is sent.
4. The composition root¶
app.py¶
"""Counter: the composition root.
Run: python app.py --dev # hot reload, readable errors
python app.py --prod # production server
"""
from rational import App
from views.pages import router
from views.styles import THEME
app = App(title="Counter", theme=THEME)
app.include(router)
if __name__ == "__main__":
app.run()