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()