Actions & reactivity
A component description is not just props. Every builder also carries behavior: named actions that become permissions, events the rendered component emits, action targets that describe what a button does, and watch-actions that make one component react to another — all declared on the backend, as data.
The behavioral API applies to every component: every cms-base builder (Form, TableView, Tree, …) returns a ComponentBuilder — the same class you get from CustomComponent(name) or ComponentBuilder(componentName) directly.
useWatch, and registering your own function handlers — is Component events.The ComponentBuilder API
ComponentBuilder comes from cms/component. Its fluent methods fall into three groups:
| Group | Method | Purpose |
|---|---|---|
| Structure | .options(opts) | Set the component's props. |
.mergeOptions(partial) | Shallow-merge into already-set options. | |
.child(id, component, { slot? }) | Nest a child component, optionally in a named slot. | |
.meta({ name, icon?, description? }) | Set the metadata that titles the component's permission. | |
| Behavior | .action(id, definition) | Register a permissioned action. |
.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). |
.onFilter(cb(permissions, options, permissionId)) | Rewrite the options per user at serialize time. |
Structure methods are covered in Pages & components. The rest of this page covers the behavior and advanced groups.
Actions and their permissions
.action(id, definition) declares a named, permission-gated capability on a component. The definition shapes the permission that gets registered:
static report = CustomComponent("MyReportPanel")
.meta({ name: "Report panel" })
.action("regenerate", {
title: "$my_app.report.perm.regenerate", // i18n key — add to every locale file
icon: "i-ph-arrows-clockwise",
description: "Regenerate the report from source data",
defaultGranted: false,
});
The example shows every ActionDefinition field — only title is required. The action's permission id derives from its component: <componentPermissionId>.<actionId>, where the component permission id is the page's fullId plus every field/child id in that component position's path (see Auth & permissions). The permission registers together with the page, when @RegisterPage runs.
Two places consume an Action:
- Route gating —
@AuthUserWithPermission(...)accepts anAction, so the backend route that performs the work checks the same permission the UI displays. - Page/permission options — a page's
permissionoption also accepts anAction.
TableView uses this exact mechanism internally: every table route registers its own action (list, add, edit, delete, …), which is why table permissions look like pages.tasks.table.delete (Tables).
Action targets
An ActionTarget (from cms-base/types/action-target) describes what a button does when clicked. It is the payload of a table's customButtons and custom row actions, and the shape to know when you build clickable affordances of your own:
type | Fields | Effect |
|---|---|---|
"drawer" | component, title?, description? | Opens a side drawer rendering the given component. |
"modal" | component, size?, title?, description? | Opens a centered modal rendering the component. |
"page" | url | Navigates to an internal route. |
"external" | url, newTab? | Navigates to an external URL, optionally in a new tab. |
"api" | url, method?, successMessage, confirm? | Calls an endpoint; shows a toast on success; optional confirm dialog first. |
"exportJob" | url, method?, statusUrl?, downloadUrl?, labels?, confirm? | Starts an async export job, polls its status, then downloads the result. |
A confirm object carries a title, a description, and an optional confirmColor (primary, error, or warning). The exportJob labels let you relabel the progress toast (title, exporting, downloading, successTitle, successMessage, errorTitle, retry); the job endpoint itself is usually built with the reusable export engine (Backend services).
The component of a drawer/modal target is any component builder — the same vocabulary as the rest of the page.
A target in the wild — the custom array below sits inside a TableView's rowActions (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." },
},
},
],
navigate / openForm / event) — don't confuse the two.Events components emit
Rendered components broadcast their lifecycle and interactions as events on a window-level bus. Watch-actions subscribe to these; so can your own frontend code (Component events). Two events are universal: ComponentEvents.LOAD / UNLOAD (cms/component), emitted automatically on mount and unmount.
Every other event belongs to a component. Each builder exports its event names as a constants namespace next to it (FormEvents for Form, and so on), importable from the same subpath — the component's own page in the Components section lists its events.
Event payloads are event-specific — FIELD_CHANGE carries the fieldId, value, and formValues, TAB_CHANGE the selected tab, and so on. The payload is what onParam conditions match against (below).
Watch-actions
A watch-action is the declarative form of "when event E fires and its payload matches, run function F". You declare it on the backend; the frontend's useWatch engine executes it. The serialized shape every component can carry (via BaseComponentProps):
interface WatchAction {
component: string; // which component's events to listen to
event: string; // the event name (see the table above)
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
}
You rarely write that object by hand — the builder methods produce it.
.watch() — react to your own events
.watch(event, functionId, opts?) registers a watch with component: ComponentId.SELF ("$component:self"), so the component reacts to the events it emits itself. This is how a form disables one field when another changes:
import { Form, FormEvents, FormFunctions } from "@antelopejs-private/cms/interfaces/cms-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() — attach a watch to a child
.watchOn(childId, event, functionId, opts?) targets a named child instead: at serialize time the watch is spliced 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 doesn't build itself and wants to wire their behavior in one place.
Two rules keep it working:
- Call
.child(childId, …)first — an unknownchildIdlogs a warning 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");
WatchOptions
Both methods take the same options object:
params— static arguments passed to the function. For built-in function ids the shape is typed:WatchFunctionParamMapis a declaration-merged map, so a built-in id likeFormFunctions.SET_FIELD_DISABLEDgets its exact params type-checked at compile time. Unknown ids fall back toRecord<string, unknown>.onParam— one condition or an ANDed array of them, each pairing akeyand avalue. Thekeyis a dotted path walked through the event's payload; the value must match strictly. This is how a singleFIELD_CHANGEstream drives different reactions per field.requirePermission— a permission id; users who don't hold it get the watch stripped from their serialized page.
.watchFilter() — per-user filtering
For logic beyond a single 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 themselves must also change.
Built-in function ids
Some components ship function ids with registered frontend handlers — a <Component>Functions namespace exported next to the builder; the component's page lists its ids and their params.
Any other functionId is yours to define: register a handler under the same string from a plugin in your frontend layer — the full pattern, including payload shapes and the executing engine, is in Component events.
watchActions inside .options({...}) has no effect — the builder silently strips the key from caller-supplied options. .watch() / .watchOn() are the supported way to declare watches.Advanced hooks
Two builder methods cover the rare cases where a component needs to participate in page registration or per-user serialization:
.onCreated(callback)— runs when the owning page registers, receiving the page'sPageMetadata. The built-in builders use it for page-scoped side effects: charts register theirrealtimeTopics as page topics, andTableViewregisters its hidden new/edit/view sub-pages..onFilter(callback)—(permissions, options, permissionId) => optionsruns per user when the page layout is served, letting you rewrite the component's options based on what the caller may see. Return the (possibly modified) options.
Page-level reactivity
Reactivity can also hang off the page rather than a component: the page option setupId names a registered frontend function that runs when the page mounts, with an event-bus on/emit context — useful for orchestrating several components at once. See Pages & components for the option and Page context for the function contract.
Navigation
How the dashboard sidebar is built — the Pages/Modules/Settings root groups, categories for your own screens, page URLs, and the command-palette quick actions.
Backend services
Cross-cutting DMS services a module can use — hooks, notifications, realtime publishing, job locks, HTML rendering, and the export-job engine.