Skip to content

Filling forms ​

A form being filled is a FormKitForm (in vyuh_form_kit). It holds the values, one per field and one per field of each repeating group entry, so every field has the state a host expects: dirty, touched, pending and valid (form.fieldAt(place) gives a field's FieldState; form.dirty, form.touched, form.pending and form.valid the form's). FormView shows it. The package that holds the values is an implementation detail: none of its types is part of the API.

dart
final form = FormKitForm(definition, values: saved, kit: kit);

KitScope<FormKit>(
  kit: kit,
  services: services,
  child: FormView(form: form, onSubmit: save),
);

// later
form.dispose();

The KitScope<FormKit> gives the form's kit and the host's services (see Host services); the views of the app's own types are parts of the kit. FormView itself takes only the form, its access and the action bar's callbacks. Build the form with the scope's kit: in debug builds FormView asserts that it is the same one.

What is checked ​

Validity comes from the definition, checked by the kit's validator kinds and rules — the same checks the designer and a server run (FormValidationEngine, FormRuleEngine). Each field's control carries its errors as a validator of its own, so a rule that makes a field required, or a cross-field check, re-validates exactly the fields it changes.

Kind of checkWhere it runs
The definition's validations, required and rulesevery edit, for the fields the edit can affect
Checks only the host can run (a key already taken, a reference not allowed)asyncHost, after the field is still for asyncDebounce (400 ms); the field is pending until it answers
Quick host answershost, a FormValidationHost, asked first
The host's own validatorsvalidators and asyncValidators, by field key, beside the definition's. Each is a function of the value that returns its message, or null when the value passes (FormValidator, AsyncFormValidator)
dart
final form = FormKitForm(
  definition,
  kit: kit,
  asyncHost: FormAsyncValidationHost((rule, value, context) async =>
      switch (rule) {
        UniqueValidation() => !await api.taken(context.field.key, value),
        _ => null, // not mine to answer
      }),
  validators: {
    'notes': [
      (value) => value is String && value.length > 500
          ? 'Keep it under 500 characters.'
          : null,
    ],
  },
);

The host's answers are kept per rule, field and value, so the same value isn't asked twice; form.recheck() forgets them, such as after the host's data changed.

Errors block valid; warnings show but never do.

What a person sees ​

A field's message shows once it is dirty or touched: "Checking…" while the host checks it, else its first error, else its first warning. submit() touches every field, so every message shows, waits for pending checks, and says whether the form can be submitted.

Values ​

ReadWhat it is
form.valuesthe values as entered, only the fields that have one (each repeating entry likewise)
form.submittedwhat a submit carries: the values with formula results and the values rules set, less every field a rule hides
form.outcomewhat the rules decided: what shows, what is required, the computed values
form.issuesevery issue now, errors and warnings, in layout order

form.write(place, value) writes a value as the person's own edit: key at form level, group[1]/key in the second entry of a repeating group, or a repeating group's id with its entries. form.reset([values]) starts again, nothing dirty or touched. form.touch(place) and form.touchAll() touch fields without an edit, so their messages show.

Access: editable, read-only, disabled ​

FormView(access:) says what the whole form allows. It is a CdxAccess (re-exported by vyuh_form_kit), and each state means something different to the person reading it:

accessWhat it isHow it shows
CdxAccess.editableThe normal state.Unchanged.
CdxAccess.readOnlyThe value is shown as it is and can't be changed here: a record in review, a computed field, one that was signed or archived.Fully legible, never dimmed. Text, numbers, money, quantities, durations and rich text stay selectable. Nothing else marks it.
CdxAccess.disabledNot available at the moment.Each control is muted as one unit (38% opacity). Never use it for a value that has to be read.
dart
FormView(form: form, access: CdxAccess.readOnly);

A sealed record is just read-only. If the person should know why ("Signed by A. Rao on 3 Oct"), the host says so itself, beside the form, in whatever way suits the page.

A control that has no read-only mode of its own (a switch, a dropdown, a reference, a rating, a date or range picker) stays enabled and legible, and the form takes it out of focus and pointer handling. A repeating group in a read-only form keeps its entries and drops Add and the remove buttons; in a disabled form they stay, muted and unavailable.

How a field's access is decided ​

A field shows the most restrictive of three things, in the order editable < readOnly < disabled (CdxAccess.restrictedBy):

  1. the form's access;
  2. what its rules decided: a rule that disables it, or a container of it, makes it disabled; a value a rule sets or a formula computes makes it readOnly;
  3. its own readOnly: true, which makes it readOnly.

So a computed total is read-only even in an editable form, and a field a rule disables is muted even in a read-only form. FieldState.access gives the rules' part, and FormFieldControl.access takes the form's.

Locking a record after signing ​

The access is the host's to change; the form keeps its values, dirty and touched state when it does. Make the form read-only when the record is sealed, and editable again when it is reopened:

dart
enum Status { draft, inReview, signed, archived }

class FindingPage extends StatelessWidget {
  const FindingPage({super.key, required this.form, required this.status});

  final FormKitForm form;
  final Status status;

  @override
  Widget build(BuildContext context) => FormView(
    form: form,
    access: switch (status) {
      Status.draft => CdxAccess.editable,
      Status.inReview => CdxAccess.readOnly, // reviewers read, nobody edits
      Status.signed || Status.archived => CdxAccess.readOnly,
    },
  );
}

access only changes what the form shows. The host still refuses to save a sealed record on the server; a read-only FormView is not a security boundary.

The signature that seals a record is a SignatureFormField signed through the host's FormSignatureService (see Host services): once it returns, make the form read-only. A signed field is a legible receipt in every access, and a read-only one shows no Sign button.

The action bar ​

Every form has an action bar, whether it is one step or several. The definition's actions (see Actions) says which of Cancel, Save draft and Submit show and what they say; the host's callbacks say what they do:

dart
FormView(
  form: form,
  onCancel: close,
  onSaveDraft: (values) => saveDraft(values),
  onSubmit: (submitted) => save(submitted),
);
  • The bar is drawn whenever the host gives any of the three callbacks. A form with a single step and no callbacks has no bar, so a host that wrote its own buttons keeps them by leaving the callbacks out. A stepped form always has the bar.
  • Cancel and Save draft show only when the definition shows them and the host gave the callback; Save draft is off by default, and hands over the values as they are, unchecked.
  • Submit always shows, and is the only primary button. It checks every field, shows every message and waits for the host's checks, and calls onSubmit with form.submitted only when none has an error. With no onSubmit it checks and does nothing more.
  • A stepped form adds Back (hidden on the first step), Skip (on an optional step only) and Next, which Submit replaces on the last step; these three are on the FormStepper.
  • A host that wants its own bar carries a ViewPart<FormActionBarModel>((context, bar) => ...) in one of the kit's extensions; it is handed the buttons and what each does (FormActionBarModel).

The kit never stores or submits.

Forms with steps ​

A definition whose layout holds steps (see Steps) fills as a wizard: FormView draws the stepper, the current step and the action bar. The values live in the FormKitForm, never in the step shown, so going back or jumping loses nothing. form.steps (a FormKitSteps) says where the person is and moves them: current, items, statusOf(id) (notStarted, current, complete, error, skipped, locked), goTo, next, back, skip and canGoTo.

The action bar is the one above. - Next checks the fields of the current step only: it shows their messages, marks the step as an error and stays when one fails.

  • Submit (form.submit()) checks every step that shows and moves to the first that fails. A field a rule hides is never checked.
  • Skip leaves an optional step unchecked. Submit still checks every field that applies, so a required field in an optional step is asked for again there.
  • A step a rule hides, or whose fields are all hidden, drops out of the stepper.

Rebuilding only what changed ​

FormView rebuilds its tree only when the form's structure changes: a rule shows or hides something, or an entry is added or removed. Each field listens to its own form.fieldAt(place), a FieldState of its value, message, whether it is enabled, required and read only (its rules' access), and whether it is dirty, touched, pending or invalid, compared by value, so typing in one field rebuilds that field alone.

Each edit runs the rules again (their outcome is form-wide and cheap) but validates only the fields the edit can affect: the field itself, fields whose validators read it, fields a rule reading it writes or targets, formulas naming it, and fields whose rule-decided state changed. FormDependencies.of(definition, kit:) works this out once per definition. A custom validator that reads other fields says so with reads; see Validators.

FormKitForm is also a ChangeNotifier that notifies on every change, for a host that shows a status or enables a Save button.

App types ​

An app's own field and content types draw through the view parts their extension carries in the kit; see Custom fields.