KEPLIN Docs

Events and the SDK

The widgets' and the screen's events, the code editor, the pre-defined actions and the keplin SDK in TypeScript — data, widgets, navigation, session, modals and workflows.

Plenty of screen gets built without writing a line: binding a datastore, dragging widgets, pointing a button at another screen. But sooner or later the "when this happens, do that" shows up — save and go back, reload a table after a filter, confirm before deleting, open a modal and use what it returned.

That is what events are for: points of the screen where code of yours runs, written in TypeScript, with an SDK — the keplin object — that gives access to everything the screen has.

Where the events are

In the inspector, the last category of a widget (and of the screen itself) is called Events. It has one row per available event, and on each row:

  • a dot on the left: filled when that event already has code, empty when it does not;
  • a button on the right, which opens the editor.

The Events category of a Button: a filled dot marks the events that already have code; the … opens the editor.
The Events category of a Button: a filled dot marks the events that already have code; the … opens the editor.

The events' names are not translated — they are the same in any language (onClick, onRowClick, onLoad), because they are also the names that appear in the code and in the Radar's records.

The screen's events

Click an empty area of the canvas so the inspector shows the screen. The Events category has three:

Event When it fires What for
onLoad Once, when the screen opens. Preparing state, loading things the datastores do not load, saying welcome.
onParamsChange Whenever the route's parameters change — and not on the first opening. Reacting to a record change without reopening the screen.
onUnload When the screen leaves. Cleaning up state, saving drafts.

The screen's own events — onLoad, onParamsChange and onUnload — in the inspector with nothing selected.
The screen's own events — onLoad, onParamsChange and onUnload — in the inspector with nothing selected.

Each widget's events

Each widget type declares its own. Besides the name, what matters is the payload — the data the event carries with it, which the code reads in keplin.event.

Form fields

Widget Events keplin.event
Textbox, Text area, Number, Yes/No, Dropdown, Date, Color onChange { value }
File upload onChange, onUpload onUpload: { file, name }

Actions and navigation

Widget Events keplin.event
Button onClick {}
Menu button onClick, onMenuItem onMenuItem: { id, label }
Link onClick {}
Export onExport, onDataLoaded onExport: { rows, filename }

Structure and content

Widget Events keplin.event
Tabs onTabChange { tab }
Report onLoad { report }

Data widgets

Widget Events keplin.event
Table onRowClick, onRowDoubleClick, onSelectionChange, onDataLoaded { row, index } · onSelectionChange: { row, rows }
List, Cards onRowClick, onDataLoaded { row, index }
Chart onClick { name, seriesName, value, dataIndex }
KPI onClick, onDataLoaded { value, indicatorId }

Boards and planning

Widget Events keplin.event
Kanban onCardClick, onCardCreate, onCardMoved, onDataLoaded { row } · { column } · { row, from, to, index }
Calendar onEventClick, onDayClick, onRangeSelect, onRangeChange, onDataLoaded { row } · { date } · { start, end } · { start, end, view }
Gantt onBarClick, onEmptyClick, onDataLoaded { row } · { date }

Processes

Widget Events keplin.event
Process status onDecide { task, outcome }
My tasks onOpen, onDecide { task, screenId }

Nota

Widgets programmed by you (the ones that appear on the palette under Custom) declare their own events, and they appear here like any others.

The code editor

An event's button opens the editor in a modal. The title says where you are: the widget's id (or the screen's name) and the event's name — w_fic_sav1 · onClick.

The editor of the Guardar button's onClick event: the code saves the datastore and, if it went well, notifies and goes back to the list.
The editor of the Guardar button's onClick event: the code saves the datastore and, if it went well, notifies and goes back to the list.

Button What it does
Insert action Writes the code of a common task for you (below).
Remove handler Deletes this event's code. The dot goes back to empty.
Cancel Closes without saving.
Save Checks and saves.

The editor has suggestions as you type (Ctrl+Space): the whole keplin is declared, with the right types — and, better still, the ids of this screen's widgets are in there. Typing keplin.widgets.get(" shows the list of the screen's widgets, and an id that does not exist is flagged as an error before you save.

Atenção

On save, the code is compiled. If it is not runnable, the platform refuses it — Event not saved: the code is not runnable — and the modal stays open for you to fix it. A screen is never left with broken code inside.

The pre-defined actions

Insert action opens a list of the most common tasks. You choose one and the code is written at the end of what is already there, already with your screen's real names — the first record datastore, the first table, the first textbox.

Insert action — the pre-defined actions that write the code for you, from datastores to workflows.
Insert action — the pre-defined actions that write the code for you, from datastores to workflows.

Action What it writes
Save datastore Validates and saves the record, with a success notice.
Reload datastore Reads a datastore's data again.
Filter datastore (by text) Reads a textbox's text and applies it as a filter.
Navigate to a screen Jumps to another route of the app.
Refresh a table Refreshes a table widget's data.
Filter table by a textbox value The classic interactive filter.
Show/hide a widget Toggles a widget's visibility.
Confirm and show a toast Asks before acting and notifies at the end.
Start a workflow Sets a process going over the current record.
List and complete tasks Lists the current user's tasks and decides one.
Send a signal to a workflow Wakes up processes that were waiting.
Sign out Leaves the app.

The inserted code is a starting point: it becomes yours, and it is there to be edited. It is never generated again.

How the code runs

Each event is an asynchronous function that receives one thing only: the keplin. Three practical consequences follow:

  • await works at the top of the code. No need to wrap anything.
  • return exits the event. It is the normal way of giving up halfway (for example, when a confirmation was refused).
  • There are no parameters. The context comes inside keplin itself: keplin.event carries the payload and keplin.ctx says where you are (ctx.widget is the widget that fired — null on screen events —, ctx.event is the event's name and ctx.screen the screen).

While a button's code has not finished, the button shows three animated dots: whoever is using it understands the app is working. If the code blows up, the screen does not break: a notice appears and the error is recorded in the Radar, with the screen, the widget and the event where it happened.

The keplin SDK

Everything the code can do sits under keplin. These are the areas:

Area What for
keplin.event / keplin.ctx The event's payload and the context it is running in.
keplin.widgets Talking to the screen's widgets.
keplin.data The datastores: reading, writing, filtering, saving.
keplin.nav Navigating and reading the route's parameters.
keplin.ui Notices, confirmations and modals.
keplin.state State shared between screens.
keplin.session Who is using the app, and what they can do.
keplin.auth Login, sign-up and password recovery (system screens).
keplin.i18n Translated phrases.
keplin.storage Preferences kept on the device.
keplin.api Calling the app's APIs directly.
keplin.reports Opening and downloading reports.
keplin.workflow Starting processes, listing and completing tasks.

The widgets

keplin.widgets.get("id") returns a widget's handle. All handles have the same basics:

const w = keplin.widgets.get("w_fic_tel1");
w.show();               // show
w.hide();               // hide
w.setEnabled(false);    // disable
w.set("label", "Telemóvel");   // change any inspector property
w.get("label");         // read the effective value
w.reset();              // forget the changes made at runtime

And then each family adds what is its own:

Family What it adds
Form fields getValue(), setValue(v), validate(), error
Data widgets (Table, List, Cards, Chart, KPI, Kanban, Calendar, Gantt) rows, total, refresh(), setFilter(where), setSort(sort)
Table selectedRow, selectedRows, clearSelection()
KPI value, values, valueOf(indicadorId)
Kanban columns, moveCard(id, coluna, índice?)
Calendar view, start, end, goTo(data), setView(vista)
Gantt zoom, setZoom(z)
Tabs activeTab, tab("id") — and, on the tab, activate(), show(), hide(), setEnabled()
Label / Button / Link / Breadcrumb setText(t) / setLabel(t)
Markdown setContent(md)
External page setUrl(url), reload()
Export export()
Report url, download()

Nota

The editor's suggestions offer all the verbs of all the families, because the editor does not know beforehand which widget that id is. At runtime only the ones of the real type exist — moveCard on a Button does nothing useful.

The data

keplin.data.store("nome") returns one of the screen's datastores by name (see Datastores and data).

On a record datastore:

const conta = keplin.data.store("conta");
conta.get("nome");                 // read a field
conta.set("estado", "ativo");      // write a field (left unsaved)
conta.record();                    // the whole record
conta.isDirty();                   // are there unsaved changes?
conta.reset();                     // throw the changes away
const ok = await conta.save();     // validates and saves; true if it saved

On a list datastore:

const contas = keplin.data.store("contas");
contas.rows();                     // the loaded rows
contas.total();                    // the total (when the server gives it)
contas.reload();                   // read again
contas.setWhere({ estado: { eq: "ativo" } });   // extra filter; null clears
contas.setSort([{ field: "nome", direction: "ASC" }]);
contas.goToPage(2);

On both, status() says where the loading stands (idle, loading, ready, error).

keplin.nav.go("/ficha-de-conta/17");   // go to a route (with parameters)
keplin.nav.back();                      // go back
keplin.nav.params;                      // the current screen's parameters, by name

Notices, confirmations and modals

keplin.ui.toast("Gravado.", "success");        // "success" | "error" | "info"
const ok = await keplin.ui.confirm("Apagar o registo?");
if (!ok) return;

The confirmation is a dialog with the app's theme — never the browser's grey box.

State, session and preferences

keplin.state.set("filtroContas", "activas");   // lives while the tab is open
keplin.state.get("filtroContas");
keplin.state.remove("filtroContas");

keplin.session.user;              // { id, username, name } — null on public screens
keplin.session.roles;             // the roles of whoever is using
keplin.session.can("contas.editar");   // has this action? (Settings ▸ Permissions)
await keplin.session.logout();

keplin.storage.set("colunasContas", ["nome", "cidade"]);   // stays on the device
keplin.storage.get("colunasContas");

Dica

To decide what someone can do, ask keplin.session.can("...") and not hasRole("gestor"). Actions are declared in Settings ▸ Permissions and survive role reorganisations; a role's name does not.

The APIs and the reports

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");

On a query, the list of fields is required — it is what says what you want to bring back.

Atenção

keplin.reports.open opens a new tab and therefore cannot sit behind an await: outside the user's gesture, the browser blocks the window. Open first, do the rest afterwards.

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

Translated phrases

keplin.i18n.t("{n} contas activas", { n: linhas.length });
keplin.i18n.locale;

A Keplin screen is not modal because it was opened a certain way — it is modal because it was configured that way. The decision sits in the screen's inspector, in the Presentation category:

The Presentation section of the screen: this is where a screen becomes a Modal (centered) or a Side panel (right).
The Presentation section of the screen: this is where a screen becomes a Modal (centered) or a Side panel (right).

Option What it does
Mode Screen (a normal page), Modal (centered) or Side panel (right).
Width (px) / Height (px) The modal's size. The side panel uses the full height.
Close button Shows the × in the corner.
Click outside closes / Esc closes The two usual ways out.
Refresh screen behind on close On closing, the datastores of the calling screen read again.

The section's own hint sums it up: Opens ON TOP of the calling screen (Link, events or keplin.ui.openModal). Excluded from direct navigation.

Opening and closing by code

const resultado = await keplin.ui.openModal("/nova-conta", { setor: "banca" });
if (resultado) {
  keplin.data.store("contas").reload();
}
  • openModal receives the screen's route (or id) and, optionally, the parameters.
  • The promise only resolves when the modal closes, and carries the value the modal returned.
  • Inside the modal, keplin.ui.closeModal(valor) closes and returns that value.
  • Modals stack: a modal can open another.

Nota

keplin.nav.go("/rota") to a screen configured as Modal (centered) or Side panel (right) opens it as a modal instead of navigating. It is on purpose: a modal screen has no address of its own in the navigation.

Recipes

Save and go back (the onClick of the Ficha de Conta's Save button):

const ok = await keplin.data.store("conta").save();
if (ok) {
  keplin.ui.toast("Conta guardada");
  keplin.nav.go("/contas");
}

Open the record card of the clicked row (onRowClick of a Table):

keplin.nav.go(`/ficha-de-conta/${keplin.event.row["id"]}`);

Filter a table by a textbox (onChange of the box — replace the ids with your screen's):

const texto = keplin.widgets.get("w_pesquisa").getValue();
keplin.widgets.get("w_cta_tab1").setFilter(texto ? { nome: { contains: texto } } : null);

Confirm before a destructive action (onClick of a button):

if (!(await keplin.ui.confirm("Apagar esta conta?"))) return;

Hide a button from whoever cannot use it (onLoad of the screen):

if (!keplin.session.can("contas.eliminar")) {
  keplin.widgets.get("w_apagar").hide();
}

Why doesn't…?

  • Why won't it let me save the event? The code does not compile. The message is Event not saved: the code is not runnable — fix it and save.
  • Why does keplin.widgets.get("...") give an error? The id does not exist on this screen. Confirm it at the top of the inspector, with the widget selected; and remember that each device is a tree of its own (see Layouts and per-device design).
  • Why didn't onParamsChange fire on opening? On purpose: it only fires on changes. For the start, use onLoad.
  • Why does the modal return nothing? Either the target screen does not exist, or whoever is using it has no permission to open it — in both cases the promise resolves with no value. Check the route and the permissions.
  • Why doesn't the report's window open? You put the open after an await. Open first.
  • Why does my event seem not to run? Check the Radar: the errors of the events' code end up there, with screen, widget and event — and the Radar takes you straight to that event's editor.