Lista de tarefas — input, lista virtualizada e checkboxes ✅¶
Modos: A/B — usa widgets com valor e o formato Python de evento (event.value).
Depois do Contador, este é o próximo passo natural: um app que acumula uma lista. Você digita uma tarefa, aperta Add, e ela aparece numa lista com um checkbox por item. 🚀
O que muda em relação ao Contador
O Contador tem um único inteiro no estado. Aqui o estado carrega uma lista de
objetos (list[TodoItem]) e um rascunho de texto — e a UI usa um
LazyColumn virtualizado, que materializa na IR só a janela visível.
O que este exemplo mostra¶
Inputcontrolado — o valor vem do estado (app.state.draft) e volta pelo handleron_change, lendoevent.valuedoTextChangeEvent.LazyColumnvirtualizado — declaraitem_counte umitem_builder; só a janela visível é renderizada.Checkboxpor linha — alternadoneno item pelo índice.- Mutações de lista com
set_state— funções internas mutam o estado no lugar.
Rodando ▶¶
tempestweb dev --mode wasm --path examples/todo # Python no browser (Pyodide)
tempestweb dev --mode server --path examples/todo # Python no servidor (FastAPI + WS)
O código¶
"""Todo list — exercises input, list and toggle widgets."""
from __future__ import annotations
from dataclasses import dataclass, field
from tempest_core import App, Style, Widget
from tempest_core import Edge
from tempest_core import (
Button,
Checkbox,
Column,
Input,
LazyColumn,
Row,
Text,
)
from tempest_core import TextChangeEvent
@dataclass
class TodoItem:
"""A single todo entry.
Attributes:
title: The text of the item.
done: Whether the item has been completed.
"""
title: str
done: bool = False
@dataclass
class TodoState:
"""State for the todo-list app.
Attributes:
draft: The text currently typed into the new-item field.
items: The todo items, in insertion order.
"""
draft: str = ""
items: list[TodoItem] = field(default_factory=list)
def make_state() -> TodoState:
"""Build the initial state with two seed items.
Returns:
A fresh :class:`TodoState` pre-populated with sample items.
"""
return TodoState(
items=[
TodoItem(title="Read docs/plan.md", done=True),
TodoItem(title="Write the todo example", done=False),
]
)
def view(app: App[TodoState]) -> Widget:
"""Render the todo UI from the current state.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
The widget tree for the current state.
"""
def edit_draft(event: TextChangeEvent) -> None:
app.set_state(lambda s: setattr(s, "draft", event.value))
def add_item() -> None:
def mutate(s: TodoState) -> None:
title = s.draft.strip()
if title:
s.items.append(TodoItem(title=title))
s.draft = ""
app.set_state(mutate)
def toggle(index: int) -> None:
def mutate(s: TodoState) -> None:
s.items[index].done = not s.items[index].done
app.set_state(mutate)
def build_row(index: int) -> Widget:
item = app.state.items[index]
return Checkbox(
label=item.title,
checked=item.done,
on_change=lambda _event, i=index: toggle(i),
)
return Column(
style=Style(gap=12.0, padding=Edge.all(16)),
children=[
Text(content="Todo", key="title"),
Row(
style=Style(gap=8.0),
children=[
Input(
value=app.state.draft,
placeholder="What needs doing?",
on_change=edit_draft,
key="draft",
),
Button(label="Add", on_click=add_item, key="add"),
],
),
LazyColumn(
item_count=len(app.state.items),
item_builder=build_row,
key="items",
),
],
)
Peça por peça¶
O input controlado¶
def edit_draft(event: TextChangeEvent) -> None:
app.set_state(lambda s: setattr(s, "draft", event.value))
O Input recebe value=app.state.draft e on_change=edit_draft. Cada tecla
dispara um TextChangeEvent; lemos event.value e guardamos no estado. Isso é um
controlled input: a única fonte de verdade é o estado.
Adicionar sem itens vazios¶
def add_item() -> None:
def mutate(s: TodoState) -> None:
title = s.draft.strip()
if title:
s.items.append(TodoItem(title=title))
s.draft = ""
app.set_state(mutate)
add_item faz strip() e só adiciona se sobrou texto — depois limpa o rascunho.
A lista virtualizada¶
LazyColumn não recebe uma lista de widgets pronta: recebe uma contagem e um
builder chamado por índice. Só a janela visível vira IR — a lista pode ter
milhares de itens sem custo.
Por que i=index no lambda?
index como argumento default congela o valor no momento da criação
do lambda — o clássico truque de closures em loops do Python.
Recapitulando¶
Neste exemplo você viu:
- ✅ Um
Inputcontrolado lendoevent.valuedoTextChangeEvent - ✅ Mutações de lista com funções internas em
set_state - ✅ Um
LazyColumnvirtualizado dirigido poritem_count+item_builder - ✅ Um
Checkboxpor linha alternando estado por índice - ✅ O padrão rodando inalterado nos Modos A/B
Próximos passos¶
- 💡 Volte ao Contador para o padrão
set_statemais simples - 💡 Veja o Formulário para validação com
Form+FormField - 💡 O Chat UI usa
LazyColumncom muito mais itens