Eventos e o SDK
Os eventos dos widgets e do ecrã, o editor de código, as acções pré-definidas e o SDK keplin em TypeScript — dados, widgets, navegação, sessão, modais e workflows.
Há muito ecrã que se faz sem escrever uma linha: ligar um datastore, arrastar widgets, apontar um botão a outro ecrã. Mas mais cedo ou mais tarde aparece o "quando isto acontecer, faz aquilo" — gravar e voltar atrás, recarregar uma tabela depois de um filtro, confirmar antes de apagar, abrir um modal e usar o que ele devolveu.
É para isso que servem os eventos: pontos do ecrã onde corre código teu,
escrito em TypeScript, com um SDK — o objecto keplin — que dá acesso a
tudo o que o ecrã tem.
Onde estão os eventos
No inspector, a última categoria de um widget (e do próprio ecrã) chama-se Eventos. Tem uma linha por evento disponível, e em cada linha:
- um ponto à esquerda: cheio quando aquele evento já tem código, vazio quando não tem;
- um botão … à direita, que abre o editor.

Os nomes dos eventos não são traduzidos — são os mesmos em qualquer língua
(onClick, onRowClick, onLoad), porque são também os nomes que aparecem
no código e nos registos do Radar.
Os eventos do ecrã
Clica numa zona vazia do canvas para o inspector mostrar o ecrã. A categoria Eventos tem três:
| Evento | Quando dispara | Para quê |
|---|---|---|
onLoad |
Uma vez, quando o ecrã abre. | Preparar estado, carregar coisas que os datastores não carregam, dar as boas-vindas. |
onParamsChange |
Sempre que os parâmetros da rota mudam — e não na primeira abertura. | Reagir a uma mudança de registo sem reabrir o ecrã. |
onUnload |
Quando o ecrã sai. | Limpar estado, guardar rascunhos. |

Os eventos de cada widget
Cada tipo de widget declara os seus. Além do nome, interessa o payload —
os dados que o evento traz consigo, e que o código lê em keplin.event.
Campos de formulário
| Widget | Eventos | keplin.event |
|---|---|---|
| Caixa de texto, Área de texto, Número, Sim/Não, Lista, Data, Cor | onChange |
{ value } |
| Ficheiro | onChange, onUpload |
onUpload: { file, name } |
Acções e navegação
| Widget | Eventos | keplin.event |
|---|---|---|
| Botão | onClick |
{} |
| Botão com menu | onClick, onMenuItem |
onMenuItem: { id, label } |
| Link | onClick |
{} |
| Exportar | onExport, onDataLoaded |
onExport: { rows, filename } |
Estrutura e conteúdo
| Widget | Eventos | keplin.event |
|---|---|---|
| Abas | onTabChange |
{ tab } |
| Relatório | onLoad |
{ report } |
Widgets de dados
| Widget | Eventos | keplin.event |
|---|---|---|
| Tabela | onRowClick, onRowDoubleClick, onSelectionChange, onDataLoaded |
{ row, index } · onSelectionChange: { row, rows } |
| Lista, Cartões | onRowClick, onDataLoaded |
{ row, index } |
| Gráfico | onClick |
{ name, seriesName, value, dataIndex } |
| KPI | onClick, onDataLoaded |
{ value, indicatorId } |
Quadros e planeamento
| Widget | Eventos | keplin.event |
|---|---|---|
| Kanban | onCardClick, onCardCreate, onCardMoved, onDataLoaded |
{ row } · { column } · { row, from, to, index } |
| Calendário | onEventClick, onDayClick, onRangeSelect, onRangeChange, onDataLoaded |
{ row } · { date } · { start, end } · { start, end, view } |
| Gantt | onBarClick, onEmptyClick, onDataLoaded |
{ row } · { date } |
Processos
| Widget | Eventos | keplin.event |
|---|---|---|
| Estado do processo | onDecide |
{ task, outcome } |
| As minhas tarefas | onOpen, onDecide |
{ task, screenId } |
Nota
Widgets programados por ti (os que aparecem na palette em Custom) declaram os seus próprios eventos, e aparecem aqui como quaisquer outros.
O editor de código
O botão … de um evento abre o editor num modal. O título diz onde estás:
o id do widget (ou o nome do ecrã) e o nome do evento — w_fic_sav1 · onClick.

| Botão | O que faz |
|---|---|
| Inserir acção | Escreve por ti o código de uma tarefa comum (a seguir). |
| Remover handler | Apaga o código deste evento. O ponto volta a vazio. |
| Cancelar | Fecha sem gravar. |
| Gravar | Verifica e grava. |
O editor tem sugestões enquanto escreves (Ctrl+Espaço): o keplin
inteiro está declarado, com os tipos certos — e, melhor ainda, os ids dos
widgets deste ecrã estão lá dentro. Escrever keplin.widgets.get(" mostra a
lista dos widgets do ecrã, e um id que não existe é assinalado como erro antes
de gravares.
Atenção
Ao gravar, o código é compilado. Se não for executável, a plataforma recusa-o — O evento não foi guardado: o código não é executável — e o modal fica aberto para corrigires. Um ecrã nunca fica com código partido lá dentro.
As acções pré-definidas
Inserir acção abre uma lista das tarefas mais comuns. Escolhes uma e o código é escrito no fim do que já lá está, já com os nomes reais do teu ecrã — o primeiro datastore de registo, a primeira tabela, a primeira caixa de texto.

| Acção | O que escreve |
|---|---|
| Gravar datastore | Valida e grava o registo, com aviso de sucesso. |
| Recarregar datastore | Volta a ler os dados de um datastore. |
| Filtrar datastore (por texto) | Lê o texto de uma caixa e aplica-o como filtro. |
| Navegar para um ecrã | Salta para outra rota da app. |
| Recarregar uma tabela | Refresca os dados de um widget de tabela. |
| Filtrar tabela pelo texto de um campo | O filtro interactivo clássico. |
| Mostrar/esconder um widget | Alterna a visibilidade de um widget. |
| Confirmar e mostrar toast | Pergunta antes de agir e avisa no fim. |
| Arrancar um workflow | Põe um processo a andar sobre o registo actual. |
| Ver e concluir tarefas | Lista as tarefas de quem está a usar e decide uma. |
| Mandar um sinal a um workflow | Acorda processos que estavam à espera. |
| Terminar sessão | Sai da app. |
O código inserido é um ponto de partida: fica teu, e é para editares. Não volta a ser gerado.
Como o código corre
Cada evento é uma função assíncrona que recebe uma coisa só: o keplin.
Daí saem três consequências práticas:
awaitfunciona no topo do código. Não é preciso embrulhar nada.returnsai do evento. É a forma normal de desistir a meio (por exemplo, quando uma confirmação foi recusada).- Não há parâmetros. O contexto vem dentro do próprio
keplin:keplin.eventtraz o payload ekeplin.ctxdiz onde estás (ctx.widgeté o widget que disparou —nullnos eventos de ecrã —,ctx.eventé o nome do evento ectx.screeno ecrã).
Enquanto o código de um botão não termina, o botão mostra três pontos animados: quem está a usar percebe que a app está a trabalhar. Se o código rebentar, o ecrã não parte: aparece um aviso e o erro fica registado no Radar, com o ecrã, o widget e o evento onde aconteceu.
O SDK keplin
Tudo o que o código pode fazer está debaixo de keplin. Estas são as áreas:
| Área | Para quê |
|---|---|
keplin.event / keplin.ctx |
O payload do evento e o contexto onde está a correr. |
keplin.widgets |
Falar com os widgets do ecrã. |
keplin.data |
Os datastores: ler, escrever, filtrar, gravar. |
keplin.nav |
Navegar e ler os parâmetros da rota. |
keplin.ui |
Avisos, confirmações e modais. |
keplin.state |
Estado partilhado entre ecrãs. |
keplin.session |
Quem está a usar a app, e o que pode fazer. |
keplin.auth |
Login, registo e recuperação de palavra-passe (ecrãs de sistema). |
keplin.i18n |
Frases traduzidas. |
keplin.storage |
Preferências guardadas no dispositivo. |
keplin.api |
Chamar as APIs da app directamente. |
keplin.reports |
Abrir e descarregar relatórios. |
keplin.workflow |
Arrancar processos, listar e concluir tarefas. |
Os widgets
keplin.widgets.get("id") devolve o handle de um widget. Todos os handles
têm o mesmo básico:
const w = keplin.widgets.get("w_fic_tel1");
w.show(); // mostrar
w.hide(); // esconder
w.setEnabled(false); // desactivar
w.set("label", "Telemóvel"); // mudar qualquer propriedade do inspector
w.get("label"); // ler o valor efectivo
w.reset(); // esquecer as alterações feitas em execução
E depois cada família acrescenta o que lhe é próprio:
| Família | O que acrescenta |
|---|---|
| Campos de formulário | getValue(), setValue(v), validate(), error |
| Widgets de dados (Tabela, Lista, Cartões, Gráfico, KPI, Kanban, Calendário, Gantt) | rows, total, refresh(), setFilter(where), setSort(sort) |
| Tabela | selectedRow, selectedRows, clearSelection() |
| KPI | value, values, valueOf(indicadorId) |
| Kanban | columns, moveCard(id, coluna, índice?) |
| Calendário | view, start, end, goTo(data), setView(vista) |
| Gantt | zoom, setZoom(z) |
| Abas | activeTab, tab("id") — e, na aba, activate(), show(), hide(), setEnabled() |
| Etiqueta / Botão / Link / Breadcrumb | setText(t) / setLabel(t) |
| Markdown | setContent(md) |
| Página externa | setUrl(url), reload() |
| Exportar | export() |
| Relatório | url, download() |
Nota
As sugestões do editor oferecem todos os verbos de todas as famílias,
porque o editor não sabe de antemão que widget é aquele id. Em execução só
existem os do tipo real — moveCard num Botão não faz nada de útil.
Os dados
keplin.data.store("nome") devolve um datastore do ecrã pelo nome (ver
Datastores e dados).
Num datastore de registo:
const conta = keplin.data.store("conta");
conta.get("nome"); // ler um campo
conta.set("estado", "ativo"); // escrever um campo (fica por gravar)
conta.record(); // o registo inteiro
conta.isDirty(); // há alterações por gravar?
conta.reset(); // deitar fora as alterações
const ok = await conta.save(); // valida e grava; true se gravou
Num datastore de lista:
const contas = keplin.data.store("contas");
contas.rows(); // as linhas carregadas
contas.total(); // o total (quando o servidor o dá)
contas.reload(); // voltar a ler
contas.setWhere({ estado: { eq: "ativo" } }); // filtro extra; null limpa
contas.setSort([{ field: "nome", direction: "ASC" }]);
contas.goToPage(2);
Em ambos, status() diz em que pé está o carregamento (idle, loading,
ready, error).
Navegação
keplin.nav.go("/ficha-de-conta/17"); // ir para uma rota (com parâmetros)
keplin.nav.back(); // voltar atrás
keplin.nav.params; // os parâmetros do ecrã actual, por nome
Avisos, confirmações e modais
keplin.ui.toast("Gravado.", "success"); // "success" | "error" | "info"
const ok = await keplin.ui.confirm("Apagar o registo?");
if (!ok) return;
A confirmação é um diálogo com o tema da app — nunca a caixa cinzenta do browser.
Estado, sessão e preferências
keplin.state.set("filtroContas", "activas"); // vive enquanto a aba estiver aberta
keplin.state.get("filtroContas");
keplin.state.remove("filtroContas");
keplin.session.user; // { id, username, name } — null em ecrãs públicos
keplin.session.roles; // os papéis de quem está a usar
keplin.session.can("contas.editar"); // tem esta acção? (Definições ▸ Permissões)
await keplin.session.logout();
keplin.storage.set("colunasContas", ["nome", "cidade"]); // fica no dispositivo
keplin.storage.get("colunasContas");
Dica
Para decidir o que alguém pode fazer, pergunta keplin.session.can("...") e
não hasRole("gestor"). As acções são declaradas em Definições ▸
Permissões e sobrevivem a reorganizações de papéis; o nome de um papel,
não.
As APIs e os relatórios
const linhas = await keplin.api.query("contas", { estado: "ativo" }, ["id", "nome"]);
await keplin.api.mutate("criarConta", { nome: "Nova" }, ["id"]);
keplin.reports.open("Contactos da conta", { contaId: 17 });
keplin.reports.download("Contactos da conta", { contaId: 17 }, "xlsx");
Numa consulta, a lista de campos é obrigatória — é ela que diz o que queres trazer.
Atenção
keplin.reports.open abre um separador novo e por isso não pode ficar
atrás de um await: fora do gesto do utilizador, o browser bloqueia a
janela. Abre primeiro, faz o resto depois.
Workflows
const registo = keplin.data.store("oportunidade").get("id");
await keplin.workflow.start("wf_aprovacao", registo);
const tarefas = await keplin.workflow.tasks();
await keplin.workflow.complete(tarefas[0].id, "aprovar");
const { woken } = await keplin.workflow.signal("documento-recebido", registo);
Frases traduzidas
keplin.i18n.t("{n} contas activas", { n: linhas.length });
keplin.i18n.locale;
Ecrãs modais
Um ecrã do Keplin não é modal porque foi aberto de certa maneira — é modal porque foi configurado assim. A decisão está no inspector do ecrã, na categoria Apresentação:

| Opção | O que faz |
|---|---|
| Modo | Ecrã (uma página normal), Modal (centro) ou Painel lateral (direita). |
| Largura (px) / Altura (px) | O tamanho do modal. O painel lateral usa a altura toda. |
| Botão de fechar | Mostra o × no canto. |
| Clique fora fecha / Esc fecha | As duas saídas habituais. |
| Refrescar o ecrã de trás ao fechar | Ao fechar, os datastores do ecrã que o chamou voltam a ler. |
A dica da própria secção resume: Abre POR CIMA do ecrã que o chama (Link,
eventos ou keplin.ui.openModal). Sai da navegação directa.
Abrir e fechar por código
const resultado = await keplin.ui.openModal("/nova-conta", { setor: "banca" });
if (resultado) {
keplin.data.store("contas").reload();
}
openModalrecebe a rota (ou o id) do ecrã e, opcionalmente, os parâmetros.- A promessa só resolve quando o modal fecha, e traz o valor que o modal devolveu.
- Dentro do modal,
keplin.ui.closeModal(valor)fecha e devolve esse valor. - Os modais empilham: um modal pode abrir outro.
Nota
keplin.nav.go("/rota") para um ecrã configurado como Modal (centro) ou
Painel lateral (direita) abre-o como modal em vez de navegar. É de
propósito: um ecrã modal não tem endereço próprio na navegação.
Receitas
Gravar e voltar (o onClick do botão Guardar da Ficha de Conta):
const ok = await keplin.data.store("conta").save();
if (ok) {
keplin.ui.toast("Conta guardada");
keplin.nav.go("/contas");
}
Abrir a ficha da linha clicada (onRowClick de uma Tabela):
keplin.nav.go(`/ficha-de-conta/${keplin.event.row["id"]}`);
Filtrar uma tabela por uma caixa de texto (onChange da caixa —
substitui os ids pelos do teu ecrã):
const texto = keplin.widgets.get("w_pesquisa").getValue();
keplin.widgets.get("w_cta_tab1").setFilter(texto ? { nome: { contains: texto } } : null);
Confirmar antes de uma acção destrutiva (onClick de um botão):
if (!(await keplin.ui.confirm("Apagar esta conta?"))) return;
Esconder um botão a quem não pode (onLoad do ecrã):
if (!keplin.session.can("contas.eliminar")) {
keplin.widgets.get("w_apagar").hide();
}
Porque não…?
- Porque não me deixa gravar o evento? O código não compila. A mensagem é O evento não foi guardado: o código não é executável — corrige e grava.
- Porque é que
keplin.widgets.get("...")dá erro? O id não existe neste ecrã. Confirma-o no topo do inspector, com o widget seleccionado; e lembra-te que cada dispositivo é uma árvore própria (ver Layouts e desenho por dispositivo). - Porque é que o
onParamsChangenão disparou ao abrir? É de propósito: ele só dispara em mudanças. Para o arranque, usa oonLoad. - Porque é que o modal não devolve nada? Ou o ecrã de destino não existe, ou quem está a usar não tem permissão para o abrir — nos dois casos a promessa resolve sem valor. Confirma a rota e as permissões.
- Porque não abre a janela do relatório? Puseste o
opendepois de umawait. Abre primeiro. - Porque é que o meu evento parece não correr? Vê o Radar: os erros do código dos eventos ficam lá, com ecrã, widget e evento — e o Radar leva-te directamente ao editor daquele evento.