Forms
Form is the component builder for data-entry screens. You declare a list of fields — each labelled and typed by a DataType (Data types) — plus where the form loads its values from and submits them to. The shared Nuxt frontend turns that into rendered inputs, client-side validation, and the submit flow. You write no Vue.
Use a Form for create/edit screens, settings panels, and standalone data entry. For paginated, sortable, CRUD-backed lists whose add/edit forms are generated from a Data controller, use a TableView (Tables) instead.
Form is a page component: assign the builder to a static field on a PageController (see Pages & components). The field key becomes the component key and part of its permission id.
What it renders
The frontend renders a titled card. From the top:
- An optional title and description header.
- The fields, each a labelled input chosen from its DataType (text box, select, date picker, file upload, rich text, …), with the field's
descriptionas helper text. Required fields are marked; invalid fields show an inline message and an error state. - Field groups, whose members render together under a shared label, laid out horizontally or vertically (
orientation). - A submit button at the bottom. On submit the form validates against its schema; on success it shows
successMessage(and optionally navigates viaredirectOnSuccess), on failureerrorMessage.
When fetchUrl is set the form first loads existing values and pre-fills the inputs (an edit form); without it the form starts empty (a create form).
Usage
Assign the builder to a static field on the page.
import {
PageController,
RegisterPage,
pagesCategory,
} from "@antelopejs-private/cms/interfaces/cms/page";
import { Form } from "@antelopejs-private/cms/interfaces/cms-base/form";
import { DefaultDataTypes } from "@antelopejs-private/cms/interfaces/cms-base/data-types";
import { HttpMethod } from "@antelopejs-private/cms/interfaces/cms-base/types";
@RegisterPage()
export class ContactPage extends PageController("contact", {
displayName: "Contact",
icon: "i-ph-note-pencil",
category: pagesCategory,
}) {
static form = Form({
title: "Contact Us",
description: "Send us a message and we'll get back to you",
fields: [
{
id: "name",
label: "Full Name",
type: new DefaultDataTypes.StringType({ placeholder: "Jane Doe", maxLength: 100 }),
required: true,
},
{
id: "email",
label: "Email",
type: new DefaultDataTypes.EmailType({ placeholder: "[email protected]" }),
required: true,
},
{
id: "message",
label: "Message",
type: new DefaultDataTypes.StringType({ textarea: true, rows: 6, maxLength: 1000 }),
},
],
submitUrl: "/api/contact",
submitUrlMethod: HttpMethod.post,
successMessage: "Thanks — we'll be in touch.",
});
}
The routes behind submitUrl (and fetchUrl) are normal API controllers — see Pages & components for wiring. Return _id, never id, for created rows.
Options
Form(options) takes a single options object (FormProps):
| Option | Type | Default | Purpose |
|---|---|---|---|
fields | (FormField | FieldGroup)[] | (required) | Fields and field groups to render. See Fields. |
title | string | — | Heading above the fields (no heading is rendered when unset); the component's display name falls back to "Form". |
description | string | — | Sub-heading text under the title. |
fetchUrl | string | — | URL to load existing values from (pre-fills the form). Omit for a create form. |
fetchUrlMethod | HttpMethod | GET | HTTP method for the fetch request. |
submitUrl | string | — | URL the form submits to. |
submitUrlMethod | HttpMethod | PUT | HTTP method for the submit request. |
submitLabel | string | — | Label of the submit button (i18n key or literal). Defaults to the generic "Save changes". |
successMessage | string | — | Toast shown after a successful submit. |
errorMessage | string | — | Toast shown when a submit fails. |
fieldsOrientation | "horizontal" | "vertical" | — | Overall layout direction of the field list. |
redirectOnSuccess | string | — | Path to navigate to after a successful submit. Supports token substitution. |
submitDefaults | Record<string, unknown> | — | Extra values merged into the submit payload. String values support token substitution. |
submitUrl, redirectOnSuccess, and submitDefaults string values support token substitution, resolved against the form's route at submit time:
{{params.X}}— a route path parameter.{{query.X}}— a query-string parameter.{{response.X}}— (redirectOnSuccessonly) a field of the submit's JSON response body, e.g./items/{{response._id}}/editto redirect to a freshly created entity.
submitDefaults entries whose tokens can't be resolved are dropped — this is how a page-rendered "new" form inherits context such as a parent id.
HttpMethod is a string enum (HttpMethod.get, HttpMethod.post, HttpMethod.put, HttpMethod.delete, HttpMethod.patch, …). The options require the enum member — a bare string literal such as "POST" does not typecheck.submitUrlMethod is omitted, the frontend submits with PUT — a creation form must set submitUrlMethod: HttpMethod.post explicitly.Fields
Each entry in fields is either a single field (FormField) or a field group (FieldGroup).
Field
{
id: "email", // payload key — required
label: "Email Address", // visible label
description: "We never share it", // helper text under the label
type: new DefaultDataTypes.EmailType({ ... }), // a DataType instance — required
required: true, // marks the field and tightens validation
defaultValue: "[email protected]", // initial value
localized: true, // store one value per locale
disabled: true, // render read-only
inputComponent: FormComponents.InputText(), // override the DataType's default input
}
| Property | Type | Purpose |
|---|---|---|
id | string | The field's key in the payload. Required. |
type | DataType | The DataType instance driving the input, parsing, and validation. Required. |
label | string | Field label. Use a $-prefixed i18n key in real modules. |
description | string | Helper text under the label. |
required | boolean | Marks the field required and tightens validation (see Validation). |
defaultValue | DefaultValue | Initial value (string | number | boolean | Date | null | array). |
localized | boolean | Store the value as a per-locale record instead of a single scalar. |
disabled | boolean | Render the input non-editable. |
inputComponent | ComponentInfoSerialized | Override the input the DataType would otherwise pick (see below). |
The type is always a DataType instance — StringType, EmailType, SelectType, DateType, FileType, RichTextType, AddressType, and so on. Each carries its own options (placeholders, min/max, items, constraints) and supplies both the validation rule and the default input. See Data types.
Field group
A group renders related fields together under a shared label:
{
id: "contactGroup",
label: "Contact Information",
description: "Your email and phone number",
orientation: "vertical", // or "horizontal"
order: 1, // sort order among items
fields: [
{ id: "email", type: new DefaultDataTypes.EmailType(), required: true },
{ id: "phone", type: new DefaultDataTypes.PhoneType() },
],
}
Grouped fields still submit as flat top-level keys (email, phone) — the group only affects layout. Members may omit label; the group's own label heads the cluster.
Overriding the input component
Each field renders the input its DataType chooses. Set inputComponent to override it with a factory from the FormComponents namespace (same import as Form), each taking its own options object:
- Text & numbers —
InputText,InputTextarea,InputEmail,InputPassword,InputPhone,InputNumber,InputPercentage,InputSlider,RichText. - Choices —
InputSelect,InputRadioGroup,InputSwitch,InputCheckbox,InputCascader,InputTree,InputRelation. - Dates & times —
DatePicker,DatePickerRange,Calendar,InputTime. - Media & specialized —
File,Image,InputColor,InputAddress,PermissionsTree.
Prefer configuring the DataType; reach for inputComponent only when you need a different control than the type's default.
Features
Validation
The form validates on the client before submit, against rules derived from the fields: each field's DataType supplies its rule, then:
- Required string and array fields must be non-empty (min length 1); other required fields must be non-null. Non-required fields are nullable and optional.
- Localized fields validate as a record of locale → value, so each provided translation is checked against the field's rule.
required flag.Localized fields
Set localized: true to store one value per locale: the value becomes a record keyed by locale code (e.g. { en: "Title", fr: "Titre" }) and the input renders a per-language editor. This is for content translated per locale — distinct from $-prefixed i18n message keys used for static labels (Localization).
Watches and reactivity
.watch() below is the form-flavored use of a generic builder API — the full reference (including .watchOn(), watchFilter, and the events every component emits) is Actions & reactivity; the frontend engine and custom function handlers are in Component events.A form reacts to its own events through .watch() on the builder. A watch listens for a form event and runs a form function, optionally gated by a condition or a permission:
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(), defaultValue: false },
{ 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 },
],
});
- Events (
FormEvents):SUBMIT,SUBMIT_SUCCESS,SUBMIT_ERROR,FIELD_CHANGE,RESET. - Functions (
FormFunctions):SET_FIELD_DISABLED({ targetField, setDisabled }),SET_FIELD_HIDDEN({ targetField, setHidden }), andSET_FIELD_REQUIRED({ targetField, setRequired }) — the last one toggles a field's required state in the client-side validation (a hidden or disabled field is never required). onParam— one condition or an array of conditions ({ key, value }) that must all match the event payload for the function to run. ForFIELD_CHANGEthe payload carriesfieldIdandvalue, so the example above disablesdisplayNameonly whenisOwnerchanges totrue.requirePermission— only apply this watch if the calling user holds the given permission id (see Permissions)..watchFilter((permissions, watches, permissionId) => watches)— a final hook to filter the watch list per user at serialize time.
watchActions inside the options object is silently stripped by the builder — those watches never reach the frontend. Use .watch() on the builder.Form modes and readonly behavior
A standalone Form has exactly two modes, decided by fetchUrl: present means edit, absent means create. The new / edit / view form modes and the per-mode readonly behavior (disabled / hidden / default) belong to the data-controller-generated add/edit forms behind a TableView (Tables) — they are not props on the standalone Form builder. For per-field create/edit/view behavior, model the data with a DataController (Data controller).
Permissions
A Form is a page component: its permission id is the page's fullId (category chain + page id) joined with every field/child id in its position — e.g. a form field on the page whose fullId is pages.form.form-grouped resolves to pages.form.form-grouped.form, while a form nested as details under panel resolves to pages.form.form-grouped.panel.details. That id is also what a watch's requirePermission references. Derivation rules, gating the backing routes, and registering standalone permissions are covered in Auth & permissions. A Form gates at the component level — it does not assign a permission per field.