Skip to content

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:

ListHolds
validatorsFormValidatorKinds, such as RequiredKind() or an app's CustomValidatorKind
ruleKindsEffectKinds, such as ShowKind() or an app's CustomEffectKind (what a rule tests is an expression, not a kind)
factsFormFacts, the questions only the app can answer, which a rule's expression asks (see Rules as expressions)
fieldTypesthe app's own CustomFieldTypes (see Custom fields)
contentTypesthe 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:

PartDefined inRead 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 itvyuh_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 hostvyuh_kitFormView
PaletteCategory, PaletteEntry: a section of the palette and what is dragged into the form (build entries with FormEntries)vyuh_kitthe designer's palette
ViewPart<SettingEditorInput> with the editorId as its target: the editor of a CustomSetting(editorId)vyuh_kitthe inspector
FormOutputPart: a slot for writing a form out (a PDF); nothing implements it yetvyuh_form_typesa 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 ​

dart
// 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 standard out, for a kit of the app's kinds only;
  • build an extension that offers a subset of the built-ins:
dart
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:

text
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:

text
The palette entry "signature" is registered twice by "acme.quality".

Where the kit goes ​

ConsumerHow
Saving and loadingThe 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 designerFormKitEditorController(definition, kit: designerKit); FormKitEditor reads it from the controller, for the palette, the inspector, the preview and the Definition JSON.
FillingFormKitForm(definition, kit: kit), shown with FormView(form: …); its submitted values are what a submit carries (see Filling forms)
The forms and the designer togetherKitScope<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 serverFormValidationEngine.validate(definition, values, kit: kit), FormRuleEngine.evaluate(definition, values, kit: kit), kit.designIssuesOf(definition)
Importing JSONkit.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:

dart
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.preview and 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.