Dart DSL
Write a form in Dart the way it reads on screen. One import gives you a small vocabulary — form, text, choice, section, row, rule — and every call builds the same checked FormDefinition the renderer, the editor and the codec already use. There is no DSL runtime, no code generation and no second schema.
import 'package:vyuh_form_types/form_dsl.dart';
final severity = choice('severity', 'Severity', required: true, options: {
'minor': 'Minor',
'major': 'Major',
'critical': 'Critical',
});
final owner = text('owner', 'CAPA owner');
final capa = section('capa', title: 'Corrective action', [
owner,
date('due', 'Due by'),
]);
final finding = form('finding', 'Audit finding', [
row('summary', [
text('title', 'Title', required: true).span(8),
severity,
]),
text('details', 'Details', inputKind: TextInputKind.multiline),
quantity('sample', 'Sample size', unitPresets: [UnitPreset.mass, UnitPreset.count]),
capa,
callout('escalate', 'Escalate **critical** findings within 24 hours.',
variant: CalloutVariant.warning),
], rules: [
rule('capa-needed', when: severity.isAnyOf(['major', 'critical']))
.show(capa)
.require(owner),
rule('escalation', when: severity.equals('critical')).show(escalate),
]);form_dsl.dart also exports vyuh_form_types, so validations, enums and content types come with it.
Fields
Every field takes its key and label first, then what sets it apart. All of them also take description, hint, helpText, required, readOnly, validations and an optional slotId (field:<key> by default). Formula fields are always read-only, so they don't take required or readOnly.
| Function | What sets it apart |
|---|---|
text | inputKind: single line, multiline, rich text or masked |
number | decimal |
money | defaultCurrency, the default until another is picked |
quantity | units of its own, then unitPresets that add up; dimension for a host lookup |
duration | — |
toggle | — |
choice / choices | options, a map of value to label, in order; choices allows several |
reference / references | the host's target; references allows several; scan adds a camera button beside the lookup |
rating | maxStars |
date, time, dateTime | showCurrentAction (Today or Now) |
dateRange, dateTimeRange | showCurrentAction |
attachment | imagesOnly, multiple |
signature | meaning, remarks, signLabel, statement |
formula | an expression, and what it yields: FormulaOutput.number, .text, .boolean, .date, .dateTime, .time |
custom<T> | the host's typeId and typed configuration |
field | places an existing FormFieldDefinition as it is |
Use typed validations for formats, lengths, numeric ranges, cross-field and host checks:
text('email', 'Your email', required: true,
validations: const [FormatValidation(TextFormat.email)]);Unit presets
A quantity's units are its own units first, then each of its unitPresets', each unit once: Mass, Length, Area, Volume, Temperature, Time, Speed, Pressure, Energy, Power, Force, Electrical, Frequency, Flow rate, Concentration, Angle, Data, Light & sound and Count. The picker shows each unit's name with its symbol — "Kilogram kg" — and the first unit is the default.
quantity('load', 'Load', units: ['crate'], unitPresets: [UnitPreset.mass, UnitPreset.volume]);Layout
form(children) stacks its children top to bottom. Containers take an id, so rules and the editor can find them:
section('capa', title: 'CAPA', collapsible: true, [owner, due]);
repeating('actions', title: 'Actions', minItems: 1, maxItems: 10, [
text('action', 'Action', required: true),
reference('assignee', 'Assignee', target: 'user'),
]);
row('names', [first, last]); // 6 and 6
row('summary', [title.span(8), severity]); // 8 and 4
grid('notes', columns: 12, rows: 3, [
text('note', 'Note').at(0, 0, width: 6, height: 3),
]);Give form steps instead and it fills as a wizard; stepper: takes the FormStepper settings, the defaults when left out. A form can't mix steps with other elements, and a rule can show or hide a step like any other element:
form('change', 'Change control', [
step('basics', [title, owner], title: 'Basics'),
step('details', [reason], optional: true),
], stepper: const FormStepper(review: true));actions: takes the form's FormActions, which of Cancel, Save draft and Submit show and what they say (see Actions); it works for a form of any number of steps:
form('change', 'Change control', [title, owner],
actions: const FormActions(draft: FormActionSetting()));In a row, .span(n) fixes a cell's columns and the other cells share what is left evenly. Rows stack below 600 px unless stackBelow says otherwise.
Content sits between fields: callout(id, body) takes inline rich text, plus image(id, asset), divider(id) and spacer(id). content(node) places any other content node, such as a host's custom content.
Rules
Keep a handle to a field and its conditions read off it. A handle offers only the conditions that suit its value, so a condition the engine would refuse doesn't compile:
| Field | Conditions |
|---|---|
| any | isEmpty, isNotEmpty |
| number, money, quantity, rating, number formula | <, <=, >, >=, equals, notEquals, between |
| duration | the same, with Durations |
| text | equals, notEquals, contains, startsWith, endsWith, matches |
| date, time, date-time | before, after, on, between |
| toggle | isOn, isOff |
| choice, reference | equals, notEquals, isAnyOf, isNoneOf |
| choices, references | includes, excludes, atLeast, atMost |
| attachment | atLeast, atMost |
A value can be another field's handle — due.after(started), qty <= limit. Join conditions with &, | and ~; chains flatten, so a & b & c is one "all of". all([...]), any([...]) and expression(r'$qty * $price > 1000') cover the rest.
Dart binds & and | tighter than < and >, so wrap comparisons when you join them — (qty > 5) & urgent.isOn — or list them: all([qty > 5, urgent.isOn]).
The vocabulary is top-level functions (text, date, row, any, …). Where they meet other names — a test's group, a mock library's any, a local called date — import the DSL with a prefix: import 'package:vyuh_form_types/form_dsl.dart' as f; and write f.form(...), f.text(...).
A rule names its id and condition, then what it does:
rule('large-order', when: (qty > 100) & ~approved.isOn, title: 'Large orders')
.show(approvalSection)
.require(approver)
.set(status, 'pending')
.compute(total, r'$qty * $price')
.disable(submitNote);show, hide, enable and disable take any element — a field, content or a container. require, set and compute take a field; validate takes a field and one of its own validations. then(effect) adds any other effect, such as a host's custom one, and RuleSpec.of(rule) brings in a FormRule built elsewhere.
Identity
A field's slot is field:<key> and the root layout layout:<form-key>; containers and content use the ids you give them. Pass slotId or layoutId to match an existing definition. These ids survive JSON and visual editing, so a form written in Dart can be opened in the editor and saved back.
form calls the ordinary FormDefinition constructor: duplicate keys, spans that don't fit, illegal nesting and grid overlaps fail with its FormatException. Values and submission go through the existing engines; validate on the server as well as in the UI.