Validation and watches
How a form validates its fields on the client and the server, reacts when a field changes, and shows a field per form mode.
A Form validates every field on the client and again on the server, can react when a field changes, and shows each field according to the form's mode.
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. A required boolean shown as a checkbox (
FormComponents.InputCheckbox()) is an agreement: only ticking it fills it; a switch keepsfalseas a value. - Localized fields validate as a record of locale → value, so each provided translation is checked against the field's rule. A field with a
fallbackLocalecannot be saved with a value in some language and none in that one: the error goes on the fallback language, and a required field left empty is reported there too.
Once a submit is refused, by the client or by the server, an alert above the fields lists the invalid ones as links that focus each; it goes away once they are fixed. An error the server raises on a part of a field (["address", "postalCode"], ["title", "fr"]) lands on that part. See Form sections.
An input component of your own refuses what was typed into it the same way the built-in ones do, with useControlError (auto-imported from the UI layer, or imported from #dms-ui/app/composables/form/useControlError): report(message) makes the message the field's error, which blocks the submit and joins the summary; report(undefined) clears it, and so does unmounting the input. Outside a form, the returned message ref holds it for the component to show.
<script setup lang="ts">
import { useControlError } from "#dms-ui/app/composables/form/useControlError";
const model = defineModel<string>();
const { report } = useControlError();
watch(model, (value) => {
report(value && !isKnownLocale(value) ? t("locale.unknown") : undefined);
});
</script>
The schema derives purely from the fields and their data types. There is no separate validation option. To change a rule, configure the DataType or the field's required flag.
Watches and reactivity
Watch-actions let one component react to another's events (e.g. conditional fields). .watch() below is the form-flavored use of a generic builder API. Its full reference, with .watchOn(), watchFilter and the events every component emits, is Actions & reactivity, and 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/interface-dms/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 }),SET_FIELD_REQUIRED({ targetField, setRequired }), which toggles a field's required state in the client-side validation (a hidden or disabled field is never required), andSET_FIELD_VALUE({ targetField, value }or{ targetField, valueMap }), which writes a field's value (see Setting a value from another field). 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.FIELD_CHANGEalso fires once per field with the values the form starts from (its defaults, or the record it loaded), so a watch applies to the form as it opens: one keyed onfieldIdalone runs at load. Key it on avaluetoo when it should apply only while the field holds that value, whether the form opened with it or the user picked it.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.
Declaring watchActions inside the options object is silently stripped by the builder, so those watches never reach the frontend. Use .watch() on the builder.
Setting a value from another field
SET_FIELD_VALUE writes a field when another one changes: a fixed value, or with valueMap the entry named by the new value of the field that changed. A deal's probability follows its stage:
static form = Form({ /* stage: SelectType, probability: PercentageType, … */ })
.watch(FormEvents.FIELD_CHANGE, FormFunctions.SET_FIELD_VALUE, {
params: { targetField: "probability", valueMap: { won: 100, lost: 0 } },
onParam: { key: "fieldId", value: "stage" },
})
.watch(FormEvents.FIELD_CHANGE, FormFunctions.SET_FIELD_VALUE, {
params: { targetField: "lostReason", value: null },
onParam: [
{ key: "fieldId", value: "stage" },
{ key: "value", value: "won" },
],
});
- A stage the map does not name (
qualified) writes nothing: the probability keeps whatever it holds. - It runs on a change only, in a create form as in an edit form. The
FIELD_CHANGEevents the form sends for the values it starts from carryinitial: trueand write nothing, so an edit form opens with the stored probability, even one the map disagrees with, and saving another field never sends it. Declare adefaultValueon the target that matches the source's default when a new record should start consistent. - A value it writes on a change is a change of the record like one typed in: the field shows as changed and is submitted.
- The field it writes fires
FIELD_CHANGEin turn and may drive other watches; two watches writing each other loop. - It writes in the browser only: a rule that must always hold (a won deal stores 100 %) still belongs to the write route.
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).