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.
FormDefinition({
required String key,
required String title,
required Iterable<FormFieldDefinition> fields,
required FormRoot root,
String? description,
Iterable<FormRule> rules = const [],
int schemaVersion = 1,
})| Member | Type | Meaning |
|---|---|---|
key | String | Stable id of the form. |
title | String | Shown at the top of the form. |
description | String? | Shown under the title. |
fields | List<FormFieldDefinition> | What the form captures, keyed by each field's key. See Field types. |
root | FormRoot | Where things appear: the layout tree. See Layout. |
rules | List<FormRule> | Conditions and effects between fields. See Rules and formulas. |
schemaVersion | int | Version 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:
fieldsis a flat list. Values, rules and formulas refer to fields bykey.rootis a tree of containers (FormGroup,RepeatingGroup,FormRow,FreeGrid) and content (HeadingContent,CalloutContent, and so on). It refers to a field only through aFieldSlot.
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;initiallyCollapsedon a group that is not collapsible; invalidminItems/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,maxStarsout 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
ActivateValidationindex or an invalidscopeGroupId
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.
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:
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.
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);| Method | Returns |
|---|---|
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:
{
"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:
| Kind | Values |
|---|---|
| Fields | text, number, money, quantity, duration, boolean, choice, reference, rating, dateTime, dateRange, dateTimeRange, attachment, formula, or a registered custom typeId |
| Nodes | field, heading, text, image, callout, divider, spacer, dataDisplay, custom, root, group, repeatingGroup, row, freeGrid |