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 root 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',
multiline: true,
required: true,
),
],
root: FormRoot(
id: 'root',
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. Render it with FormView
FormView is stateless. You own the values and decide which fields show their messages.
import 'package:flutter/material.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> {
var _values = <String, Object?>{};
var _touched = <String>{};
void _submit() {
final errors = [
for (final issue in FormValidationEngine.validate(deviationReport, _values))
if (!issue.warning) issue,
];
if (errors.isNotEmpty) {
// Show every field's message, not only the ones the user touched.
setState(() => _touched = {for (final f in deviationReport.fields) f.key});
return;
}
// Save _values.
}
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 16,
children: [
FormView(
definition: deviationReport,
values: _values,
validationKeys: _touched,
onChanged: (key, value) => setState(() {
_values = {..._values, key: value};
_touched = {..._touched, key};
}),
),
FilledButton(onPressed: _submit, child: const Text('Submit')),
],
);
}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 and the undo history.
import 'package:flutter/material.dart' hide TextFormField;
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 = FormCodec.encodeString(_controller.definition);
// Store json; FormCodec.decodeString(json) restores the same definition.
}
@override
Widget build(BuildContext context) => FormKitEditor(
controller: _controller,
subtitle: 'Quality · Forms',
actions: [FilledButton(onPressed: _publish, child: const Text('Publish'))],
);
}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 with FormView. 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 editor: controller commands, palette entries and app actions