Quick Start
Build a short deviation report in three steps: define it, render it for users to fill in, then open it in the designer. The concept pages extend this same form into the full deviation intake example.
1. Define the form
A definition has a flat list of fields, a layout tree under layout that places each field once through a FieldSlot, and optional rules.
import 'package:vyuh_form_types/vyuh_form_types.dart';
final deviationReport = FormDefinition(
key: 'deviation_report',
title: 'Deviation report',
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: 'what_happened',
label: 'What happened',
inputKind: TextInputKind.multiline,
required: true,
),
],
layout: FormLayout(
id: 'layout',
children: [
FormRow(
id: 'summary_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_what', fieldKey: 'what_happened'),
],
),
);The constructor checks the whole definition. A duplicate key, a slot for an unknown field, or a row whose spans add up to more than 12 throws a FormatException that names the problem, so an invalid definition never exists.
2. Fill it with FormKitForm and FormView
A FormKitForm holds the form being filled: its values, and each field's dirty, touched, pending and valid state. FormView shows it, and a field's message appears once the person has changed or left it. The kit is the one registry of everything a form can use (see The kit); a KitScope<FormKit> gives it, with the services of the host, to every form below it. This form uses built-in types only, so the standard kit is all it needs.
import 'package:material_ui/material_ui.dart' hide TextFormField;
import 'package:vyuh_form_kit/vyuh_form_kit.dart';
final class DeviationReportPage extends StatefulWidget {
const DeviationReportPage({super.key});
@override
State<DeviationReportPage> createState() => _DeviationReportPageState();
}
final class _DeviationReportPageState extends State<DeviationReportPage> {
final _form = FormKitForm(deviationReport, kit: FormKit.standard);
Future<void> _submit(Map<String, Object?> submitted) async {
// Every field checked out. Save `submitted`.
}
@override
void dispose() {
_form.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => KitScope<FormKit>(
kit: FormKit.standard,
child: FormView(
form: _form,
onCancel: () => Navigator.of(context).maybePop(),
onSubmit: _submit,
),
);
}The action bar comes with the callbacks: because the page gives onCancel and onSubmit, the form ends with a Cancel button and a primary Submit. Submit shows every field's message, waits for the host's checks, and calls onSubmit only when none has an error. A form with no callbacks has no bar, and a host that draws its own carries a ViewPart<FormActionBarModel> in the kit. Which buttons show, and what they say, is the definition's actions (a Save draft button is off until the definition turns it on); see Filling forms.
On a wide screen the row lays out 6 + 3 + 3 columns. Below 600 px (the row's stackBelow), or when a cell would be narrower than its field's minimum width, the cells stack into one column.
3. Edit it in the designer
FormKitEditor edits a definition through a FormKitEditorController. The controller holds the current definition, the selection, the undo history and the kit. The same KitScope<FormKit> that serves the forms serves the designer.
import 'package:material_ui/material_ui.dart' hide TextFormField;
import 'package:vyuh_form_kit/vyuh_form_kit.dart';
import 'package:vyuh_form_kit_editor/vyuh_form_kit_editor.dart';
final class DeviationDesigner extends StatefulWidget {
const DeviationDesigner({super.key});
@override
State<DeviationDesigner> createState() => _DeviationDesignerState();
}
final class _DeviationDesignerState extends State<DeviationDesigner> {
late final _controller = FormKitEditorController(deviationReport);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _publish() {
final json = _controller.kit.encodeString(_controller.definition);
// Store json; _controller.kit.decodeString(json) restores the same
// definition. Then tell the controller it is saved.
_controller.markSaved();
}
@override
Widget build(BuildContext context) => KitScope<FormKit>(
kit: _controller.kit,
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) => FormKitEditor(
controller: _controller,
toolbarActions: [
DesignerAction(
id: 'publish',
icon: Icons.publish_outlined,
tooltip: 'Publish',
onPressed: _controller.isDirty ? () async => _publish() : null,
),
],
),
),
);
}isDirty is true while the draft differs from the last saved one, and undoing back to the saved point makes it false again. FormKitEditor(onChanged:) and controller.changes tell the host each time the definition changes, for a draft autosave. See The editor.
Authors drag fields and containers from the palette, resize row cells and grid items on the canvas, edit properties in the inspector, and open Preview to fill in the form as FormView does. The form's Cancel, Save draft and Submit buttons are edited on the form's General tab. Every change is one undo step.
Next steps
- The form definition: fields, layout tree, slots and JSON
- Layout: groups, repeating groups, rows and grids
- The kit: one registry for the codec, the forms and the designer
- Filling forms: the action bar, validators and access
- The editor: controller commands, saving, palette entries and app actions