Actions and reactivity
The behaviour side of the component DSL. Permissioned actions, the events components emit, action targets, and the watch-actions that make components react to each other from backend configuration.
A component description carries behaviour as well as props. Every builder declares named actions that become permissions, events the rendered component emits, action targets that say what a button does, and watch-actions that make one component react to another. The backend declares all of them as data.
The behaviour API applies to every component. Every @antelopejs/interface-dms/base builder (Form, TableView, Tab, and the others) returns a ComponentBuilder. CustomComponent(name) and new ComponentBuilder(componentName) return the same class.
This page covers the backend declarations. Component events covers the frontend side that runs them: the window event bus, useWatch, and the registration of your own function handlers.
The ComponentBuilder API
ComponentBuilder comes from @antelopejs/interface-dms/component. Its chainable methods fall into three groups:
| Group | Method | Purpose |
|---|---|---|
| Structure | .options(opts) | Set the component's props. |
.mergeOptions(partial) | Shallow-merge into the options already set. | |
.child(id, component, { slot?, ...metadata }) | Nest a child component, in a named slot when slot is set. Other keys pass through to the serialized child. | |
.meta({ name?, icon?, description? }) | Merge the metadata that titles the component's permission. | |
| Behaviour | .action(id, definition) | Register a permissioned action. |
.button(id, { permission?, permissionId? }) | Register a named button that is not an action, so a quick action can expose it and inherit its permission. | |
.watch(event, functionId, opts?) | React to the component's own events (watch-actions). | |
.watchOn(childId, event, functionId, opts?) | Attach a watch to a named child component. | |
.watchFilter(cb) | Filter the watch list per user at serialize time. | |
| Advanced | .onCreated(cb(page)) | Run a callback when the owning page registers (below). |
.transformOptions(cb(options)) | Rewrite the options once, when the page registers. | |
.onFilter(cb(permissions, options, permissionId, context)) | Rewrite the options per request at serialize time. | |
.navBadge({ page?, count }) | Publish a count as the navigation badge of a page. |
Pages and components covers the structure methods. The rest of this page covers the behaviour and advanced groups.
Actions and their permissions
.action(id, definition) declares a named capability on a component, gated by a permission. The definition shapes the permission that the DMS registers:
static report = CustomComponent("MyReportPanel")
.meta({ name: "Report panel" })
.action("regenerate", {
title: "$my_app.report.perm.regenerate", // i18n key, add it to every locale file
icon: "i-ph-arrows-clockwise",
description: "Regenerate the report from source data",
defaultGranted: false,
});
ActionDefinition has five fields, and only title is required. The example shows four. The fifth, dependencies, names other actions of the same component by id. The roles editor grants them with this action, and a saved role stores them with it. A table view's edit depends on its view this way, because its form loads the row through the view route.
The action's permission id is <componentPermissionId>.<actionId>. The component permission id is the page's permission id followed by every field and child id on the path to that component. The page's permission id is its fullId, unless the page declares permission.id. See Auth and permissions. The permission registers together with the page, when @RegisterPage runs.
Two places take an Action:
- Route gating.
@AuthUserWithPermission(...)accepts anAction, so the backend route that does the work checks the same permission the UI shows. - Page options. A page's
permissionoption accepts anActionas well as aPermission.
TableView uses the same mechanism. It registers list and select, then add, edit, view, delete, archive, restore and export when the table can perform them. Table permissions therefore read like pages.tasks.table.delete (see Tables).
Action targets
An ActionTarget (from @antelopejs/interface-dms/base/types/action-target) describes what a button does on a click. It is the payload of a table's customButtons and custom row actions, and the shape to know when you build clickable controls of your own:
type | Fields | Effect |
|---|---|---|
"drawer" | component, title?, description?, direction?, size?, rowHeader?, rowTitle?, positionLabel?, deepLink?, notFound? | Opens a drawer that renders the component. It slides in from the bottom, or from the direction edge. A "right" drawer is a side sheet, 600px wide by default, which the user resizes from lg. Use it for detail panels. On a row it takes the side panel's row options (see Row drawers). |
"modal" | component, size?, title?, description? | Opens a centred modal that renders the component. |
"sidePanel" | component, title?, rowTitle?, positionLabel?, deepLink?, notFound? | Opens the component on a row in a non-modal panel docked on the right of the page. The DMS draws its header with the title, the "x of y" position, previous and next buttons, and a close button (see Row drawers). It is meant for global panels that do not depend on the page. A detail panel is a drawer. |
"page" | url | Navigates to an internal route. |
"external" | url, newTab? | Navigates to an external URL, in a new tab when newTab is set. |
"download" | url, filename? | Fetches a file with the user's session and saves it. The name is filename, else the server's Content-Disposition name, else the URL's last segment. |
"quickAction" | id | Runs a quick action, by its category:id key or its bare id. The DMS leaves the button out for a user the quick action is not served to. |
"api" | url, method?, body?, copy?, successMessage | Calls an endpoint and shows a toast on success, or the response's warning when it carries one. copy names a response field to copy to the clipboard. |
"exportJob" | url, method?, statusUrl?, downloadUrl?, labels? | Starts an async export job, polls its status, then downloads the result. |
The confirmation belongs to the action, not to the target. A button or a row action takes confirm. Its value is a ConfirmDialog (title, description?, color?, icon?, confirmLabel?, impact?, blocked?, confirmText?, fields?, and more) or { from: url }, a dialog the server words for the row (see Tables).
The exportJob labels relabel the progress toast with title, exporting, downloading, successTitle, successMessage, errorTitle and retry. The reusable export engine usually builds the job endpoint itself (see Backend services).
The component of a drawer, modal or sidePanel target is any component builder, from the same vocabulary as the rest of the page.
The custom array below sits inside a TableView's rowActions (see Tables):
custom: [
{
label: "Mark complete",
icon: "i-ph-check-circle",
target: {
type: "api",
url: "/api/task/complete",
method: "POST",
successMessage: "Task completed",
},
confirm: { title: "Complete task?", description: "This cannot be undone." },
},
],
Quick actions use a smaller target union of their own: navigate, openForm, button and event. It is a different type from ActionTarget.
Events components emit
Rendered components broadcast their lifecycle and interactions as events on a window-level bus. Watch-actions subscribe to these events, and so can your own frontend code (Component events). Two events are universal. ComponentEvents.LOAD and ComponentEvents.UNLOAD (@antelopejs/interface-dms/component) fire on mount and unmount.
Every other event belongs to one component. Each builder exports its event names as a constants namespace next to it (FormEvents for Form, and so on), from the same subpath. The component's own page in the Components section lists its events.
Each event has its own payload. FormEvents.FIELD_CHANGE carries { fieldId, value, initial }, once per field with the values the form starts from and then on each change. TabEvents.TAB_CHANGE carries the selected tab. onParam conditions match against the payload (see below).
Watch-actions
A watch-action says "when event E fires and its payload matches, run function F". You declare it on the backend, and the frontend's useWatch engine runs it. Every component can carry the serialized shape through BaseComponentProps:
import type { WatchActionCondition } from "@antelopejs/interface-dms/base/types";
export interface WatchAction {
component: string; // which component's events to listen to
event: string; // the event name
functionId: string; // the registered frontend function to run
params?: Record<string, unknown>; // static arguments for the function
onParam?: WatchActionCondition | WatchActionCondition[]; // payload conditions
requirePermission?: string; // drop the watch for users without this permission
}
The builder methods produce that object, so you rarely write it by hand.
.watch() reacts to the component's own events
.watch(event, functionId, opts?) registers a watch with component: ComponentId.SELF ("$component:self"). The component reacts to the events it emits itself. A form uses it to disable one field when another changes:
import { Form, FormEvents, FormFunctions } from "@antelopejs/interface-dms/base/form";
static form = Form({
fields: [
{ id: "isOwner", label: "Is Owner", type: new DefaultDataTypes.BooleanType() },
{ id: "displayName", label: "Display Name", type: new DefaultDataTypes.StringType() },
],
}).watch(FormEvents.FIELD_CHANGE, FormFunctions.SET_FIELD_DISABLED, {
params: { targetField: "displayName", setDisabled: true },
onParam: [
{ key: "fieldId", value: "isOwner" },
{ key: "value", value: true },
],
});
.watchOn() attaches a watch to a child
.watchOn(childId, event, functionId, opts?) targets a named child instead. At serialize time the builder splices the watch into that child's options, still with component: SELF, so the child reacts to its own events. Use it when a parent container composes children it does not build itself and wires their behaviour in one place.
Two rules apply:
- Call
.child(childId, ...)first. An unknownchildIdlogs the warningwatchOn: unknown child id "<id>" on component "<name>". Did you call .child() first?, and the watch never reaches a component. - Target the direct child that emits the event.
SELFresolves to that child, not to components nested deeper.
static panel = VStack({ spacing: "1rem" })
.child("form", Form({ fields, submitUrl: "/api/workspace" }))
.watchOn("form", FormEvents.SUBMIT_SUCCESS, "app.workspace.OnSaved");
Watch options
.watch() and .watchOn() take the same WatchOptions object:
paramsholds the static arguments passed to the function.WatchFunctionParamMapis a declaration-merged map, so a built-in id such asFormFunctions.SET_FIELD_DISABLEDgets its exact params type checked at compile time. Other ids fall back toRecord<string, unknown> | undefined.onParamis one condition or an array of conditions that must all match. Each pairs akeyand avalue. Thekeyis a dotted path into the event's payload, and the value must match strictly. OneFIELD_CHANGEstream can drive a different reaction per field this way.requirePermissionis a permission id. The DMS strips the watch from the serialized page of a user who does not hold it.
.watchFilter() filters per user
For logic beyond one permission id, .watchFilter((permissions, watches, permissionId) => watches) runs at serialize time and returns the watch list to keep for that user. Combine it with .onFilter() when the options must change too.
Built-in function ids
Some components ship function ids with registered frontend handlers, in a <Component>Functions namespace exported next to the builder. The component's page lists its ids and their params.
BlockFunctions.REFRESH_PAGE (@antelopejs/interface-dms/base/display) works on any component. It asks every block of the page that reads its data from a route to read it again without remounting: the stat groups, key-value lists, cards, charts, a tab set's badgesUrl, and the table views. A form that changes the record a page shows declares it on its own success:
static notes = Form({ fields, fetchUrl: "/api/customers/{{params.id}}", submitUrl: "/api/customers/{{params.id}}" })
.watch(FormEvents.SUBMIT_SUCCESS, BlockFunctions.REFRESH_PAGE);
You define every other functionId. Register a handler under the same string from a plugin in your frontend layer. Component events gives the full pattern, with the payload shapes and the engine that runs the handler.
watchActions inside .options({...}) has no effect. The builder strips the key from the options you pass, without a warning. Declare watches with .watch() and .watchOn().
Advanced hooks
Four builder methods cover a component that takes part in page registration or in per-request serialization:
.onCreated(callback)runs when the owning page registers. It receives the page as aCreatedPage(targetandpageInfo, which holdsfullIdandfullSlug). The built-in builders use it for side effects scoped to the page. Charts register theirrealtimeTopics as page topics, andTableViewregisters its hidden new, edit and view sub-pages under the key it is mounted under..transformOptions(callback)finalizes the options once, duringserialize(), when the page that carries the component registers. Use it for work that does not depend on the request, such as signing an upload token. Transforms run in registration order.serializeSync()skips them, because the host's own transform finalizes every component it embeds..onFilter(callback)is(permissions, options, permissionId, context) => options. It runs per request when the DMS serves the page layout, and rewrites the component's options from what the caller may see.contextis aComponentFilterContextwithtenantId,user, and, while a page layout is served, the page URL'squeryandparamsand the page'srecord. Return the options, changed or not..navBadge({ page?, count })publishes a count as the navigation badge of the component's page, or ofpage.count(ctx, user)returns a number or{ count, tone }. A count that throws or is zero shows no badge, and a page's staticMenuOptions.badgewins over it.
Page-level reactivity
Reactivity can also live on the page instead of a component. The page option setupId names a registered frontend function that runs when the page mounts, with an event-bus context that holds on and emit. Use it to coordinate several components at once. See Pages and components for the option and Page context for the function contract.