[data-reveal]{opacity:1!important;transform:none!important}
AntelopeJS DMS
DocumentationValidation and watches

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 keeps false as 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 fallbackLocale cannot 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.

LocaleInput.vue vue
<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:

src/pages/demo.ts ts
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), and SET_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. For FIELD_CHANGE the payload carries fieldId and value, so the example above disables displayName only when isOwner changes to true. FIELD_CHANGE also 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 on fieldId alone runs at load. Key it on a value too 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:

src/pages/deal.ts ts
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_CHANGE events the form sends for the values it starts from carry initial: true and 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 a defaultValue on 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_CHANGE in 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).