The kit
Everything a form can use comes from one object, the kit. It holds the validator kinds, the effect kinds, the facts a rule's expression may ask, the app's own field and content types and the parts its extensions carry: how a custom field draws at fill time, and how the designer offers and edits it. The host builds it once, from an ordered list of extensions, and passes it everywhere: the designer, the forms, the JSON codec and validation on a server.
There is no second place to register anything, and nothing is "extra": the built-ins are an extension like any app's. FormKit is a Kit<FormKitExtension> of vyuh_kit_types, the base every Blueprint kit shares; a FormKitExtension is a KitExtension.
Extensions
A FormKitExtension (in vyuh_form_types, pure Dart) holds ordered lists:
| List | Holds |
|---|---|
validators | FormValidatorKinds, such as RequiredKind() or an app's CustomValidatorKind |
ruleKinds | EffectKinds, such as ShowKind() or an app's CustomEffectKind (what a rule tests is an expression, not a kind) |
facts | FormFacts, the questions only the app can answer, which a rule's expression asks (see Rules as expressions) |
fieldTypes | the app's own CustomFieldTypes (see Custom fields) |
contentTypes | the app's own CustomContentTypes |
It also carries parts, the typed pieces the layers above read. The pure package only carries them; a Flutter package defines them:
| Part | Defined in | Read by |
|---|---|---|
ViewPart and EditorPart aimed at a custom type by their target (its type id): a view that draws the type at fill time, an editor that offers and edits it | vyuh_kit (attach with CustomFieldType.withParts; read with FormKitViews) | FormView, the designer's preview, the palette and the inspector |
ViewPart<FormActionBarModel>: the form's action bar, drawn by the host | vyuh_kit | FormView |
PaletteCategory, PaletteEntry: a section of the palette and what is dragged into the form (build entries with FormEntries) | vyuh_kit | the designer's palette |
ViewPart<SettingEditorInput> with the editorId as its target: the editor of a CustomSetting(editorId) | vyuh_kit | the inspector |
FormOutputPart: a slot for writing a form out (a PDF); nothing implements it yet | vyuh_form_types | a server that renders documents |
FormKitExtension.standard holds every built-in kind. FormEditorStandard.extension (in vyuh_form_kit_editor) is the same extension carrying the standard palette's parts, and FormEditorStandard.kit is a kit of it alone.
A server builds the extension without parts and an app adds them under the same id, with extension.withParts([...]): both kits save and load the same document.
Building a kit
// Shared with the server: pure Dart.
final qualityKinds = FormKitExtension(
id: 'acme.quality',
validators: [batchReleased],
facts: [equipmentCalibrated],
ruleKinds: [flagForReview],
fieldTypes: [signatureType],
);
// The app's kit: the same kinds, with the app's parts (the palette's
// category and entry, and a view for the signature field).
final appKit = FormKit([
FormEditorStandard.extension,
qualityKinds.withParts([
approvals, // a PaletteCategory
signatureEntry, // a PaletteEntry from FormEntries.field
...signatureUi.parts, // the view part, aimed at the signature type
]),
]);
// A server's kit: the same kinds, no Flutter.
final serverKit = FormKit([FormKitExtension.standard, qualityKinds]);The app's kit is one FormKit, so it goes to the designer, the forms and the codec as it is.
Order
The kit's order is the extensions' order, then each list's order, and every menu shows it:
- the palette's sections follow the categories, and each section's entries follow the palette;
- Add validator lists validator kinds in order, under their categories in the order they first appear;
- the effect dropdown lists effect kinds in order, and the rule editor's fact picker lists the kit's facts in order.
So the host decides where everything goes:
- put an app's extension first to show its category first;
- leave
standardout, for a kit of the app's kinds only; - build an extension that offers a subset of the built-ins:
final few = FormKit([
FormKitExtension(
id: 'basics',
validators: [RequiredKind(), LengthKind()],
ruleKinds: [ShowKind(), HideKind(), RequireKind()],
parts: [
FormCategories.textAndNumbers,
for (final part in FormEditorStandard.parts)
if (part is PaletteEntry<FormPaletteContext, DropPayload> &&
part.category == FormCategories.textAndNumbers.id)
part,
],
),
]);The designer builds its rule UI from the kit too. Add rule starts from the kit's first effect (already aimed at the element it was started for, when it has one) with an empty test, an and of nothing that the design check asks to fill. The test is an expression, so every operator of a field's type is on offer and the kit adds only facts and effects. A kit with no effect kind has no rules: the designer shows "The kit has no effect to apply." disabled.
A definition that uses a validator kind its kit doesn't have is refused when it is read ("Unknown validation type: pattern."), except an app kind saved as custom, which is kept as it was saved and shown as a design warning. An effect whose kind the kit doesn't have is always kept, unresolved, and saves back exactly as it was read; a fact the kit doesn't have is kept in the expression, never known, and reported as a warning.
Duplicates
An id registered twice throws an ArgumentError when the kit is built, naming both extensions:
The validator kind "required" is registered by both "vyuh.form" and "acme.quality".
The field type "acme.signature" is registered twice by "acme.quality".
The extension id "acme.quality" is used by both the extension at position 1 (FormKitExtension) and the one at position 2 (FormKitExtension).Validator kinds, effect kinds, facts, field types and content types each have their own ids. An app's field type can't take a built-in one's id, such as text. Every kit knows the built-in field types (standardFieldTypes), with or without the standard extension.
The designer's parts are checked when the designer starts (kit.checkEditorParts(), which FormKitEditorController calls): a palette category, palette entry or setting editor registered twice, or an entry whose category no extension registers, throws the same way:
The palette entry "signature" is registered twice by "acme.quality".Where the kit goes
| Consumer | How |
|---|---|
| Saving and loading | The kit owns its codec, kit.codec (a DefinitionCodec<FormDefinition>): kit.encode(form), kit.decode(json), kit.encodeString(form), kit.decodeString(source). There is no codec to give the kit to, so a custom type is registered in one place and a kit that lacks it fails loudly with a KitCodecException at the type: Unknown form field type: acme.signature. The kit used has the extensions vyuh.form; register the type in one of them. Every fault in a document is a KitCodecException whose path says where, such as fields[3].validations[1].config. |
| The designer | FormKitEditorController(definition, kit: designerKit); FormKitEditor reads it from the controller, for the palette, the inspector, the preview and the Definition JSON. |
| Filling | FormKitForm(definition, kit: kit), shown with FormView(form: …); its submitted values are what a submit carries (see Filling forms) |
| The forms and the designer together | KitScope<FormKit>(kit: kit, services: …, child: …) gives the kit and the host's FormHostServices below it; see Host services. The views come from the kit's own parts. |
| A server | FormValidationEngine.validate(definition, values, kit: kit), FormRuleEngine.evaluate(definition, values, kit: kit), kit.designIssuesOf(definition) |
| Importing JSON | kit.decodeString(source), as the designer's Import JSON does through the shared dialog: a refusal is shown with the path of the fault (fields[3].label: Expected a string), and one a host's own codec extension throws is shown too, as "Couldn't read the definition". A host importing elsewhere catches the KitCodecException. |
The engines default to the standard kit (FormKit.standard, or FormEditorStandard.kit for the controller); the codec has no default, since it is the kit's own. Build the kit once and keep it: a rule plan is cached per definition and kit.
In debug builds a FormView asserts its form was built with the KitScope<FormKit>'s kit, and FormKitEditor asserts its controller was, so a form built with the standard kit under an app's scope is caught where it is made. FormView also asserts that every custom field and content type of the scope's kit has a view part (kit.missingViews names those that do not).
kit.validators and kit.rules are views of the kit, for reading kinds: kit.validators.kindOf(validation), kit.rules.effectKinds, kit.rules.facts. They can't be built on their own.
Actions
What the author can do in the designer is one ordered list of FormEditorActions, which the host composes:
FormKitEditor(
controller: controller,
actions: [...FormEditorActions.standard, startCollapsed],
toolbarActions: [publish],
)FormEditorActions.standard is a plain list: undo, redo and preview for the form (their buttons are on the toolbar; the list gives them their keys and menu entries), then duplicate, delete, wrapInRow, wrapInGroup, ungroup, makeRepeating, makePlainGroup, equalSpans, rowColumns, unwrapRow, tidyPlacements and unwrapToFlow. Each is also on its own, so a host can:
- omit one: leave out
FormEditorActions.previewand the toolbar has no Design / Preview toggle; - reorder them:
[FormEditorActions.preview, FormEditorActions.undo, FormEditorActions.redo, …]; - insert its own anywhere.
The list's order decides every surface. The selection bar shows the pinned actions in list order, then ⋯. The ⋯ and context menus list the others under their group, the groups in the order they first appear and each group in list order; the context menu then lists the pinned ones. A key runs the first action in the list whose key matches. An id listed twice throws an ArgumentError.
An action's availability is an ActionAvailability:
ActionAvailability.available: it runs;ActionAvailability.unavailable([reason]): it can't, and there's nothing to explain, such as Undo with nothing to undo. Its button is disabled (the tooltip gives the reason) and its key does nothing;ActionAvailability.refused(reason): a rule forbids it, such as deleting a field a rule uses. Its button is disabled with the reason, and its key shows the reason for a moment.
Undo and Redo apply to the form, so they sit on the editor's toolbar, and their keys work whatever is selected (keysAnywhere); such a key is checked and run with the form's selection. An action's perform gets a BuildContext under the designer, the controller and the selection, so an app action can open its own dialog.
toolbarActions is where a host's own document-level buttons go, after the code icon on the toolbar, such as a Publish: each is a DesignerAction (an icon, a tooltip and what it does), not a FormEditorAction.