Component events
Components on a DMS page are mounted independently, yet they often need to react to one another — a form disables a field when a checkbox flips, a table reloads after a form submits. The DMS wires this without prop-drilling or a store, through three cooperating pieces:
- An event bus —
useComponentEventdispatchesCustomEvents onwindow. Any component can broadcast; any component can listen. - A function registry —
useDefinedFunctionsmaps a string id to a handler, registered once frontend-side. - A watch engine —
useWatchreads thewatchActionsa component carries (declared on the backend), subscribes to the named events, and on a match calls the registered handler.
The result: the backend declares "when event E fires on component C and its payload matches, run function F" as pure data, and the frontend executes it — no frontend code per page.
.watch() / .watchOn() builder API and the events each component emits — is Actions & reactivity.The event bus
useComponentEvent returns sendComponentEvent(name, componentId, data?), which dispatches a CustomEvent on window. The event name is what listeners subscribe to, the componentId identifies the emitter (listeners filter on it), and data is the payload — the detail is { component, data }.
const { sendComponentEvent } = useComponentEvent();
sendComponentEvent("CmsComponent.Form.FieldChange", componentId, {
fieldId: "isOwner",
value: true,
});
Pass a componentId to useComponentEvent(componentId) and it also emits two lifecycle events for you — Events.LOAD ("CmsComponent.Load") on mount and Events.UNLOAD on unmount — so other components can react to yours appearing or disappearing. Call it with no argument when you only want to send.
The function registry
useDefinedFunctions is a single shared registry:
const { registerFunction, getFunction, unregisterFunction } = useDefinedFunctions();
registerFunction("app.form.HighlightOnChange", (action, eventData, componentState) => {
/* ... */
});
Ids are plain strings: the backend names a function in a watch-action, the frontend registers a handler under the same string — they only have to agree on the literal. Register your own from a plugin in your frontend layer.
Reacting with useWatch
A component that wants to react calls useWatch(actions, componentId?, defaultState?). It returns isLoading and state, subscribes to window for every distinct action.event, and on each event runs the actions that match. A WatchAction is the data the backend produced:
interface WatchAction {
component: string;
event: string;
functionId: string;
params?: Record<string, unknown>;
onParam?: WatchActionCondition | WatchActionCondition[];
}
An action matches when all of these hold:
action.componentequals the emitter's id.ComponentId.SELF("$component:self") resolves to the watcher's owncomponentId, so a component can watch its own events.action.eventequals theCustomEventtype.- the
onParamconditions pass (below).
On a match, useWatch resolves getFunction(action.functionId) and calls it as fn(action, event.detail, state), where state is a ref seeded from defaultState. The handler mutates state; the component reads it reactively.
Two execution rules to know: an action that throws is swallowed — isLoading never gets stuck — but it aborts the remaining matched actions for that event.
onParam matching
onParam filters on the event's payload by dotted path: each condition pairs a key (walked through event.detail.data) with a value (strict equality), and an array of conditions is AND-ed:
onParam: [
{ key: "fieldId", value: "isOwner" },
{ key: "value", value: true },
];
This is how one FIELD_CHANGE stream drives different reactions for different fields and values.
useEventedAction
The producer half of the loop: useEventedAction wraps an async operation so it toggles a loading flag and broadcasts start / success / error component events around it — other components watch that lifecycle.
const { execute } = useEventedAction<FormSubmitResponse>({
componentId: props.componentId,
events: {
start: FormEvents.SUBMIT,
success: FormEvents.SUBMIT_SUCCESS,
error: FormEvents.SUBMIT_ERROR,
},
});
execute(asyncFn, opts?) sets isLoading, emits start, awaits asyncFn, then emits success — or error and re-throws — and clears isLoading in a finally. Optional startPayload, successPayload(result), and errorPayload(error) map the operation's data onto the emitted events. Events are only emitted when both an event name and a componentId are configured.
End-to-end example
A form field's change drives custom behavior — say, writing a derived flag into the watching component's state. Two pieces, one shared id.
1. Register the handler (frontend, in a frontend-layer plugin):
export default defineNuxtPlugin(() => {
const { registerFunction } = useDefinedFunctions();
registerFunction("app.form.MarkOwner", (action, eventData, componentState) => {
// eventData is { component, data }
const payload = eventData.data as { fieldId: string; value: unknown };
componentState.value.isOwnerSelected = payload.value === true;
});
});
2. Declare the watch-action (backend, on the component builder). .watch(event, functionId, opts) records an action with component: ComponentId.SELF, so the form listens to the events it emits itself:
import { Form, FormEvents } from "@antelopejs-private/cms/interfaces/cms-base/form";
static form = Form({
title: "Profile",
fields: [
{ id: "isOwner", label: "Is Owner", type: new DefaultDataTypes.BooleanType() },
{ id: "displayName", label: "Display Name", type: new DefaultDataTypes.StringType() },
],
})
.watch(FormEvents.FIELD_CHANGE, "app.form.MarkOwner", {
onParam: { key: "fieldId", value: "isOwner" },
});
When isOwner changes, the form emits FIELD_CHANGE; useWatch sees the onParam match, resolves app.form.MarkOwner from the registry, and runs it — flipping state.isOwnerSelected, which the component reads reactively. The same shape with a built-in functionId is how conditional fields work out of the box; your custom function only adds a handler under a new id.
TableView displays
Register an alternate TableView body renderer — grid, kanban, cards, calendar — that inherits the table's search, filters, sort, tabs, pagination, selection, and row actions.
Period filtering
The producer / consumer / registry that links a PeriodSelector to data widgets by a shared string scope id.