Skip to content

The form definition ​

FormDefinition (in vyuh_form_types) is the whole form as immutable data. The renderer, the designer, the engines and the JSON codec all work from it.

dart
FormDefinition({
  required String key,
  required String title,
  required Iterable<FormFieldDefinition> fields,
  required FormRoot root,
  String? description,
  Iterable<FormRule> rules = const [],
  int schemaVersion = 1,
})
MemberTypeMeaning
keyStringStable id of the form.
titleStringShown at the top of the form.
descriptionString?Shown under the title.
fieldsList<FormFieldDefinition>What the form captures, keyed by each field's key. See Field types.
rootFormRootWhere things appear: the layout tree. See Layout.
rulesList<FormRule>Conditions and effects between fields. See Rules and formulas.
schemaVersionintVersion of the definition format. Currently 1.

All collections are unmodifiable. To change a definition, build a new one, or edit it in the designer through FormKitEditorController.

Fields and the layout tree ​

A definition keeps what is captured apart from where it appears:

  • fields is a flat list. Values, rules and formulas refer to fields by key.
  • root is a tree of containers (FormGroup, RepeatingGroup, FormRow, FreeGrid) and content (HeadingContent, CalloutContent, and so on). It refers to a field only through a FieldSlot.
dart
const FieldSlot(id: 'slot_title', fieldKey: 'title')

Every field needs exactly one slot. Node ids (FormNode.id) and field keys are separate namespaces: rules and values use field keys, while the editor selects and moves nodes by id.

Checked at construction ​

The constructor calls validate(), so an invalid definition cannot exist. Every design error is a FormatException with a message that names the problem. Among the checks:

  • missing key or title; an unsupported schemaVersion
  • duplicate or blank field keys or labels; empty or duplicate node ids
  • a slot for an unknown field, a field slotted twice, or a field with no slot
  • a nested FormRoot; a repeating group inside another repeating group
  • a gap outside gapScale; initiallyCollapsed on a group that is not collapsible; invalid minItems / maxItems
  • a row that is empty, has a column count other than 6 or 12, more than 6 cells, a span below the element's minimum, or spans that overflow
  • a free grid with overlapping, out-of-bounds or undersized placements
  • field-specific problems: a choice field with no choices or with blank or duplicate choice values, a blank reference target, maxStars out of range, a formula that does not compile, a formula dependency cycle
  • validation settings that contradict each other, or two required validations on one field
  • rules with an empty or duplicate id, an unknown target field, an invalid ActivateValidation index or an invalid scopeGroupId
dart
try {
  final form = FormCodec.decodeString(source);
} on FormatException catch (error) {
  print(error.message); // names the field, node or rule at fault
}

Worked example: deviation intake ​

The concept pages share one example. It has:

  • a Deviation group with a 12-column row (Title, Severity, Discovered) and a full-width Batch number;
  • a repeating group of Affected materials, 1 to 10 entries, each with a per-entry formula line_cost;
  • a free grid with a multiline description, a CAPA owner and a star rating;
  • two rules that show the CAPA owner and make it required only for major or critical deviations.
dart
import 'package:cdx_formula/cdx_formula.dart' show FormulaType;
import 'package:vyuh_form_types/vyuh_form_types.dart';

const _majorOrWorse = [
  FieldEquals('severity', 'major'),
  FieldEquals('severity', 'critical'),
];

final deviationIntake = FormDefinition(
  key: 'deviation_intake',
  title: 'Deviation intake',
  description: 'Log a deviation within 24 hours of discovery.',
  fields: [
    TextFormField(
      key: 'title',
      label: 'Title',
      hint: 'e.g. Temperature excursion in cold room 2',
      required: true,
      validations: const [LengthValidation(max: 120)],
    ),
    ChoiceFormField(
      key: 'severity',
      label: 'Severity',
      required: true,
      choices: const [
        FormChoice(value: 'minor', label: 'Minor'),
        FormChoice(value: 'major', label: 'Major'),
        FormChoice(value: 'critical', label: 'Critical'),
      ],
    ),
    DateTimeFormField(key: 'discovered_at', label: 'Discovered', required: true),
    TextFormField(
      key: 'batch',
      label: 'Batch number',
      hint: 'e.g. B-2026-0412',
      validations: const [
        PatternValidation(r'^B-\d{4}-\d{4}$', message: 'Use the B-YYYY-NNNN format.'),
      ],
    ),

    // Inside the repeating group: one value scope per entry.
    TextFormField(key: 'material', label: 'Material', required: true),
    NumberFormField(
      key: 'qty',
      label: 'Quantity',
      decimal: true,
      validations: const [NumericRangeValidation(min: 0, includeMin: false)],
    ),
    NumberFormField(key: 'unit_cost', label: 'Unit cost', decimal: true),
    FormulaFormField(
      key: 'line_cost',
      label: 'Line cost',
      expression: r'$qty * $unit_cost',
      outputType: FormulaType.number,
    ),

    // In the free grid.
    TextFormField(key: 'what_happened', label: 'What happened', multiline: true, required: true),
    TextFormField(
      key: 'capa_owner',
      label: 'CAPA owner',
      // Index 0. It starts inactive because a rule below activates it.
      validations: const [RequiredValidation(message: 'Name a CAPA owner for major deviations.')],
    ),
    RatingFormField(key: 'confidence', label: 'Confidence in root cause'),
  ],
  root: FormRoot(
    id: 'root',
    rowGap: 24,
    children: [
      FormGroup(
        id: 'deviation',
        title: 'Deviation',
        children: [
          FormRow(
            id: 'deviation_row',
            columns: 12,
            cells: const [
              FormCell(child: FieldSlot(id: 'slot_title', fieldKey: 'title'), span: 6),
              FormCell(child: FieldSlot(id: 'slot_severity', fieldKey: 'severity'), span: 3),
              FormCell(child: FieldSlot(id: 'slot_discovered', fieldKey: 'discovered_at'), span: 3),
            ],
          ),
          const FieldSlot(id: 'slot_batch', fieldKey: 'batch'),
        ],
      ),
      RepeatingGroup(
        id: 'materials',
        title: 'Affected materials',
        minItems: 1,
        maxItems: 10,
        children: [
          FormRow(
            id: 'material_row',
            columns: 12,
            columnGap: 12,
            cells: const [
              FormCell(child: FieldSlot(id: 'slot_material', fieldKey: 'material'), span: 4),
              FormCell(child: FieldSlot(id: 'slot_qty', fieldKey: 'qty'), span: 2),
              FormCell(child: FieldSlot(id: 'slot_unit_cost', fieldKey: 'unit_cost'), span: 3),
              FormCell(child: FieldSlot(id: 'slot_line_cost', fieldKey: 'line_cost'), span: 3),
            ],
          ),
        ],
      ),
      FreeGrid(
        id: 'assessment',
        columns: 12,
        rows: 2,
        placements: const [
          GridPlacement(
            child: FieldSlot(id: 'slot_what', fieldKey: 'what_happened'),
            x: 0, y: 0, width: 8, height: 2,
          ),
          GridPlacement(
            child: FieldSlot(id: 'slot_capa', fieldKey: 'capa_owner'),
            x: 8, y: 0, width: 4, height: 1,
          ),
          GridPlacement(
            child: FieldSlot(id: 'slot_confidence', fieldKey: 'confidence'),
            x: 8, y: 1, width: 4, height: 1,
          ),
        ],
      ),
    ],
  ),
  rules: [
    FormRule(
      id: 'capa_hidden_for_minor',
      when: NotCondition(AnyCondition(_majorOrWorse)),
      effects: const [SetVisibility('capa_owner', false)],
    ),
    FormRule(
      id: 'capa_required_for_major',
      when: AnyCondition(_majorOrWorse),
      effects: const [ActivateValidation('capa_owner', 0)],
    ),
  ],
);

Values ​

FormView reads and writes a Map<String, Object?> keyed by field key. A repeating group stores its entries under the group's id as a list of entry maps:

dart
final values = <String, Object?>{
  'title': 'Temperature excursion in cold room 2',
  'severity': 'major',
  'discovered_at': DateTime(2026, 9, 27, 14, 30),
  'materials': [
    {
      'material': 'Insulin vials',
      // A decimal NumberFormField holds a FormulaNumber; a whole one an int.
      'qty': FormulaNumber.parse('40'),
      'unit_cost': FormulaNumber.parse('12.50'),
    },
  ],
};

Formula results are computed for display. FormView never writes them back through onChanged.

JSON with FormCodec ​

FormCodec is a static namespace. Built-in types need no setup. Pass extensions: (a FormCodecRegistry) when the form uses custom fields or content.

dart
final json = FormCodec.encodeString(deviationIntake);
final restored = FormCodec.decodeString(json);

// Map form, for storage adapters that take structured JSON.
final Map<String, Object?> map = FormCodec.encode(deviationIntake);
final again = FormCodec.decode(map, extensions: appCodecs);
MethodReturns
encodeString(form, {extensions})String
decodeString(source, {extensions})FormDefinition
encode(form, {extensions})Map<String, Object?>
decode(json, {extensions})FormDefinition

Decoding runs the same constructor, so a decoded definition is valid or the call throws FormatException.

An excerpt of the encoded form:

json
{
  "schemaVersion": 1,
  "key": "deviation_intake",
  "title": "Deviation intake",
  "description": "Log a deviation within 24 hours of discovery.",
  "fields": [
    {
      "type": "text", "key": "title", "label": "Title",
      "hint": "e.g. Temperature excursion in cold room 2",
      "helpText": null, "readOnly": false, "inputKind": "singleLine",
      "validations": [
        { "message": null, "warning": false, "type": "required" },
        { "message": null, "warning": false, "type": "length", "min": null, "max": 120 }
      ]
    }
  ],
  "root": {
    "id": "root", "type": "root", "rowGap": 24,
    "children": [
      { "id": "deviation", "type": "group", "title": "Deviation", "collapsible": false,
        "initiallyCollapsed": false, "rowGap": 16, "children": [
          { "id": "deviation_row", "type": "row", "columns": 12, "stackBelow": 600.0, "columnGap": 16,
            "cells": [ { "span": 6, "child": { "id": "slot_title", "type": "field", "fieldKey": "title" } } ] }
      ] }
    ]
  },
  "rules": []
}

The type discriminators:

KindValues
Fieldstext, number, money, quantity, duration, boolean, choice, reference, rating, dateTime, dateRange, dateTimeRange, attachment, formula, or a registered custom typeId
Nodesfield, heading, text, image, callout, divider, spacer, dataDisplay, custom, root, group, repeatingGroup, row, freeGrid