4. Chaves e identidade¶
Toda interação chega no core como chave + evento: o renderizador diz “o nó
quality-segments-item-1 recebeu um clique” e o runtime procura esse nó na
árvore para achar o handler. A chave é, portanto, a identidade do nó — para o
diff e para o roteamento de evento.
Por isso a regra: uma chave, um nó. Duas chaves iguais na mesma tela e o handler que responde é o do primeiro nó que a busca encontrar — não necessariamente aquele que o usuário tocou.
O problema, medido¶
Dois SegmentedControl na mesma tela: um para tema, um para qualidade.
from tempest_core import Column, Node, SegmentedControl, build
tema: list[int] = []
qualidade: list[int] = []
tela = Column(
key="root",
children=[
SegmentedControl(
key="theme-segments",
options=["Sistema", "Claro", "Escuro"],
on_select=tema.append,
),
SegmentedControl(
key="quality-segments",
options=["Baixa", "Média", "Alta"],
on_select=qualidade.append,
),
],
)
def chaves(node: Node) -> list[str]: # (1)!
"""Devolve as chaves do nó e de todos os descendentes."""
encontradas = [node.key] if node.key is not None else []
for filho in node.children:
encontradas.extend(chaves(filho))
return encontradas
print(chaves(build(tela)))
- Um walk simples da árvore construída, só para olhar as chaves emitidas.
['root',
'theme-segments', 'theme-segments-item-0', 'theme-segments-item-1', 'theme-segments-item-2',
'quality-segments', 'quality-segments-item-0', 'quality-segments-item-1', 'quality-segments-item-2']
Nenhuma repetida. Antes da versão 0.15.0 os dois controles emitiam seg-0,
seg-1, seg-2 cada um — clicar em “Claro” no controle de tema mudava a
qualidade, e o controle de tema ficava inerte.
As três peças¶
Todo componente ganhou uma identidade explícita:
| Peça | O que é |
|---|---|
default_key |
O nome do componente, usado quando quem chama não passa key ("segmented", "navbar", "card"…). |
base_key |
self.key or self.default_key — a chave da raiz que o componente emite. |
child_key(sufixo) |
f"{base_key}-{sufixo}" — a chave de cada nó interno. |
from tempest_core import SegmentedControl
controle = SegmentedControl(key="quality", options=[], on_select=lambda i: None)
controle.base_key # "quality"
controle.child_key("item-0") # "quality-item-0"
sem_chave = SegmentedControl(options=[], on_select=lambda i: None)
sem_chave.base_key # "segmented"
sem_chave.child_key("item-0") # "segmented-item-0"
Duas instâncias sem key ainda colidem
O default_key resolve o caso comum de um controle por tela e mantém a
árvore legível num dump. Ele não pode inventar identidade: duas instâncias
sem key do mesmo componente caem na mesma base. Numa tela com dois,
passe key — é exatamente o que key significa.
Escrevendo o seu componente¶
O mesmo contrato vale para componente próprio: declare o default_key e passe
todo nó interno por child_key.
from typing import ClassVar
from tempest_core import Button, Column, Component, Text, Widget, build
class Contador(Component):
"""Um rótulo com um botão de incremento."""
default_key: ClassVar[str] = "contador" # (1)!
valor: int = 0
def render(self) -> Widget:
"""Baixa o contador para primitivas."""
return Column(
key=self.base_key, # (2)!
children=[
Text(content=str(self.valor), key=self.child_key("valor")), # (3)!
Button(label="+1", on_click=lambda: None, key=self.child_key("mais")),
],
)
print([n.key for n in build(Contador(key="carrinho", valor=2)).children])
# -> ['carrinho-valor', 'carrinho-mais']
- O nome do componente. Sem ele, uma instância sem
keyherdaria"component"— e colidiria com qualquer outro componente igualmente distraído. - A raiz usa
base_key, nuncaself.key or "..."escrito à mão. - O sufixo descreve o papel do nó dentro do componente (
valor,mais,item-0), sem repetir o nome do componente — a base já o carrega.
O teste que segura isso
tests/test_child_keys.py monta duas instâncias de cada componente
interativo, afirma que a árvore não tem chave repetida e dispara o handler
pela chave — do jeito que o runtime faria. Um componente novo sem
default_key próprio falha o guard parametrizado.
Migrando de 0.14.x¶
As chaves internas mudaram de forma junto com o conserto: além do prefixo, o sufixo perdeu a repetição do nome do componente. Se o seu código (ou o seu teste, ou a sua fixture de renderizador) procura nó por chave literal, atualize:
| Componente | Antes | Agora |
|---|---|---|
SegmentedControl |
seg-1 |
<key>-item-1 |
RadioGroup |
radio-1 |
<key>-item-1 |
NavBar |
nav-1 |
<key>-item-1 |
Tabs |
tab-1 |
<key>-item-1 |
Breadcrumb |
crumb-1 / sep-1 |
<key>-item-1 / <key>-sep-1 |
Rating |
star-1 |
<key>-star-1 |
Card |
card-body / card-col |
<key>-body / <key>-col |
DataTable |
dt-next / dt-row-0 |
<key>-next / <key>-row-0 |
Stepper |
step-up / step-value |
<key>-up / <key>-value |
SearchBar |
search-input |
<key>-input |
EmailInput (e irmãos BR) |
email-field / field-label |
<key>-field / <key>-field-label |
Onde <key> é o key que você passou — ou o default_key do componente
(segmented, navbar, card, data-table, stepper, searchbar,
email-input…) quando você não passou nenhum. Para um componente sem key,
AppBar, Header, Card e companhia caem exatamente na chave antiga
(appbar-title, header-subtitle, card-body), porque o sufixo já repetia o
nome do componente.
Recapitulando¶
- Evento roteia por chave; chave repetida entrega o evento ao nó errado.
base_keyé a raiz do componente,child_key(sufixo)é cada nó interno.default_keynomeia o componente para a instância semkey— duas na mesma tela ainda precisam dekeyexplícita.- Componente próprio segue o mesmo contrato, e o guard parametrizado cobra isso.