Components
Overview
A component is a serializable description of a piece of UI, not a live object. ComponentBuilder collects a component name, an options object, children, watches and actions; serialize() turns that into plain JSON-safe data; the page layout route returns the result. Nothing in this interface renders anything.
Everything on this page lives in @antelopejs-private/cms/interfaces/cms/component. The cms-base interface ships the concrete builders (Form, TableView, Grid, the chart family) built on top of these types; use this page when you write a builder of your own or when you need to know what a serialized component looks like.
The Serialize-Then-Render Contract
Two shapes describe the same component on the two sides of serialization:
interface ComponentInfo<T = unknown, C = Component<T>, ChildType = ComponentChild<T, C>> {
componentName: string;
options?: T;
children?: ChildType[];
}
type ComponentInfoSerialized<T = unknown> = Omit<ComponentInfo<T>, "children"> & {
children?: ChildSerialized[];
};
ComponentInfo is what a builder holds: children carry live Component instances. ComponentInfoSerialized is what the page layout carries: every child has been replaced by its own serialized description.
| Type | Shape |
|---|---|
ComponentChild<T, C> | { id: string; component: C; slot?: string; [key: string]: unknown } |
BaseComponentChild | ComponentChild with component fixed to Component |
ChildSerialized | { id: string; component: ComponentInfoSerialized; slot?: string; [key: string]: unknown } |
ComponentInfoPromise<T> | MaybePromise<ComponentInfo<T, Component<T>, BaseComponentChild>> |
ComponentMetadata | { name: string; description?: string; icon?: string } |
The extra index signature on a child is the escape hatch for per-child props a container needs (a grid column span, a tab label): whatever you pass in the child metadata survives serialization next to id, component and slot.
Build a Component
ComponentBuilder<T> is the class you instantiate with the frontend component name. Every method returns this, so calls chain.
import { ComponentBuilder } from "@antelopejs-private/cms/interfaces/cms/component";
interface CounterOptions {
title: string;
fetchUrl: string;
}
export function Counter(options: CounterOptions) {
return new ComponentBuilder<CounterOptions>("acme-counter")
.options(options)
.meta({ name: options.title, icon: "i-ph-hash" });
}
Assign the result to a static field of a page class and @RegisterPage() picks it up (Pages and Modules).
| Method | Description |
|---|---|
options(opts?) | Replace the options object. A watchActions key on the input is stripped: watches are declared with watch/watchOn only. |
mergeOptions(opts) | Shallow-merge into the existing options. Logs a warning and does nothing when no options were set. |
child(id, component, metadata?) | Append a child with its own positional permission. metadata may carry slot plus any additional props. A duplicate id logs a warning. |
meta(meta) | Merge into the component metadata (name, description, icon). |
onCreated(callback) | Run callback(pageMetadata) when the component's page registers. |
onFilter(callback) | Transform the options per caller, from their permission set. |
watch(event, functionId, opts?) | Declare a watch on this component. |
watchOn(childId, event, functionId, opts?) | Declare a watch spliced into a child's options at serialization time. |
watchFilter(callback) | Filter the accumulated watches per caller. |
action(id, definition) | Declare an Action on the component. |
before(anchor) | Place the component above anchor when injecting it into another page. |
after(anchor) | Place it below anchor. |
order(value) | Break ties between components landing at the same spot. Defaults to 0. |
Placement
The last three methods only mean something on a component declared inside a @RegisterPageExtension class; on a component declared on its own page they are ignored. Together they build a ComponentPlacement:
type PlacementSide = "before" | "after" | "end";
interface ComponentPlacement {
side: PlacementSide;
anchor?: Component;
order: number;
}
const DEFAULT_PLACEMENT: ComponentPlacement = { side: "end", order: 0 };
anchor is a static component field of the target page class, e.g. .before(TeamPage.table). A component that declares no placement gets DEFAULT_PLACEMENT — appended after every component of the target page, with no ordering hint. Read the current value off component.placement, which is undefined until one of the three methods is called.
Lower order comes first, and equal orders fall back to the extension class name and then to declaration order, so the result never depends on module start order. Pages and Modules covers the extension mechanism these describe placement for.
Children and Slots
import { ComponentBuilder } from "@antelopejs-private/cms/interfaces/cms/component";
import { Placeholder } from "@antelopejs-private/cms/interfaces/cms-base";
const panel = new ComponentBuilder<{ title: string }>("acme-panel")
.options({ title: "Overview" })
.child("header", Placeholder({ label: "Header" }), { slot: "header" })
.child("body", Placeholder({ label: "Body" }), { slot: "default", grow: true });
The grow: true above is an arbitrary extra prop: it lands on the serialized child alongside id, component and slot.
The Component Class
ComponentBuilder extends Component<T>, which is also usable directly when you already hold a ComponentInfo:
new Component<T>(
componentInfo: ComponentInfoPromise<T>,
metadata: ComponentMetadata,
onPageCreated?: (page: PageMetadata) => void,
)
PageMetadata is the page record described in Pages and Modules.
| Member | Description |
|---|---|
componentInfo | The ComponentInfo, possibly a promise. |
metadata | The ComponentMetadata. |
actions | Record<string, Action> of the declared actions. |
getAction(id) | One action, or undefined. |
onPageCreated | The callback run when the owning page registers. |
onFilterCallback | The per-caller options filter, when one is installed. |
placement | The ComponentPlacement, or undefined when none was declared. |
serialize() | Promise<ComponentInfoSerialized<T>> — awaits the component info and serializes every child. |
serializeSync() | The same, for a component whose info is not a promise. |
Both serialize methods normalize children to [] when the component has none.
Because componentInfo may be a promise, a builder can resolve data at registration time. Use serialize() unless you know the info is synchronous.
Actions
An action is a named capability of a component that carries its own permission — "add a row", "export", "restore". Declare one on the builder:
import { ComponentBuilder } from "@antelopejs-private/cms/interfaces/cms/component";
const table = new ComponentBuilder<{ fetchUrl: string }>("acme-table")
.options({ fetchUrl: "/api/things" })
.action("export", {
title: "$acme.table.action_export",
icon: "i-ph-download-simple",
description: "$acme.table.action_export_description",
});
const exportAction = table.getAction("export");
ActionDefinition carries title (required), and optional icon, description and defaultGranted.
An Action instance exposes:
| Member | Description |
|---|---|
id | The action id, as passed to action(). |
definition | The ActionDefinition. |
permissionId | The component's permission id with .<id> appended, or undefined while the component has no permission id yet. |
toPermission() | A Permission built from the definition, or undefined when permissionId is. |
The component's permission id only exists once the owning page has registered, so read permissionId from onCreated or later — not while the builder is still being assembled.
An Action is accepted anywhere a permission is: as the permission option of a page (Pages and Modules) and as the target of AuthUserWithPermission (Permissions).
getPermissionIdRef is the seam that lets Action resolve its component's permission id without importing the page module: it is an object with a single get(component) method, replaced by cms/page when that module loads. Consumers read permissionId; there is no reason to call or reassign the ref.
Per-Caller Filtering
onFilter rewrites a component's options for the caller who requested the page layout. It runs during layout resolution, after the component itself passed its permission check.
import { ComponentBuilder } from "@antelopejs-private/cms/interfaces/cms/component";
interface ListOptions {
fetchUrl: string;
allowBulkDelete: boolean;
}
const list = new ComponentBuilder<ListOptions>("acme-list")
.options({ fetchUrl: "/api/things", allowBulkDelete: true })
.onFilter((permissions, options) => ({
...options,
allowBulkDelete: permissions.has("things.delete"),
}));
The callback receives (permissions: Set<string>, options: T, permissionId: string) and returns the options — synchronously or as a promise. The third argument is the component position's own permission id, including the full parent path for a child, so a filter can derive the ids of the actions it needs to test.
Watches
A watch binds an event on a component to a registered function. The backend only describes the binding.
watch<F extends string>(event: string, functionId: F, opts?: WatchOptions<F>): this
watchOn<F extends string>(childId: string, event: string, functionId: F, opts?: WatchOptions<F>): this
WatchOptions field | Type | Description |
|---|---|---|
params | WatchFunctionParams<F> | Arguments handed to the function. Typed per functionId when the function declares its parameter map. |
onParam | WatchActionCondition | WatchActionCondition[] | Conditions on the event payload, each { key, value }. |
requirePermission | string | Permission id the caller must hold; the watch is dropped from the layout otherwise. |
watch targets the component itself; watchOn(childId, ...) targets a child added with child() and is spliced into that child's serialized options. Calling watchOn with an unknown child id logs a warning.
import {
Form,
FormEvents,
FormFunctions,
} from "@antelopejs-private/cms/interfaces/cms-base/form";
const form = Form({ title: "Profile", fields: [] }).watch(
FormEvents.FIELD_CHANGE,
FormFunctions.SET_FIELD_DISABLED,
{
params: { targetField: "adminNotes", setDisabled: true },
onParam: [
{ key: "fieldId", value: "isOwner" },
{ key: "value", value: false },
],
requirePermission: "pages.crm.contacts.form",
},
);
watchFilter(callback) narrows the watch list per caller: it receives (permissions, watches, permissionId) and returns the watches to keep. It runs right after onFilter in the same filtering pass, and the requirePermission narrowing is applied to whatever it returns.
Component Events
ComponentEvents holds the lifecycle event names:
| Constant | Value |
|---|---|
ComponentEvents.LOAD | "CmsComponent.Load" |
ComponentEvents.UNLOAD | "CmsComponent.Unload" |
Use them as the event argument of watch or watchOn, or as the event name in a page setup function's on() subscription (Pages and Modules).
Next Steps
Permissions covers the ids these components and actions register, and the guards that check them.