Custom fields
An app's own field type is registered once, as a CustomFieldType<C>, where C is a configuration class you own. The type says everything the form kit needs to know about it:
| The type carries | Used by |
|---|---|
typeId, title | the JSON, the palette, the inspector header |
defaultConfiguration | a new field from the palette or the DSL |
encodeConfiguration / decodeConfiguration, configurationVersion | the kit's codec (kit.encode, kit.decode) |
configurationSettings | the inspector: a section of the type's own settings |
minimumSizeOf(configuration) | rows and grids: the least room a field needs |
rulesRead, formulasRead | the operators a condition offers, and whether formulas can read it |
validators | the validator kinds its fields accept, beside Required |
Every field knows its type (field.fieldType), so sizing, operators, formula inputs and validation ask it directly, with or without a kit. The built-in types are described the same way: standardFieldTypes holds TextFieldType, NumberFieldType and the rest.
Only two things live outside the type, because they are Flutter: how its input looks at fill time (a ViewPart) and how the designer offers and edits it (an EditorPart). Both are parts of the same declaration: signatureType.withParts(view:, editor:) returns the extension that carries the type and its parts under one id, and a server builds the same extension without them.
The example adds a QA signature field.
1. The type
import 'package:vyuh_form_types/vyuh_form_types.dart';
final class SignatureConfig implements CustomFieldConfiguration {
const SignatureConfig({this.stampTime = true, this.wide = false});
final bool stampTime;
final bool wide;
}
final signatureType = CustomFieldType<SignatureConfig>(
typeId: 'acme.signature',
title: 'Signature',
defaultConfiguration: const SignatureConfig(),
configurationVersion: 2,
encodeConfiguration: (c) => {'stampTime': c.stampTime, 'wide': c.wide},
decodeConfiguration: (json, version) => SignatureConfig(
stampTime: json['stampTime'] as bool? ?? true,
// Version 1 had no wide pad.
wide: version >= 2 && json['wide'] == true,
),
configurationSettings: [
CustomKindSetting<SignatureConfig, bool>(
id: 'stampTime',
label: 'Stamp the time',
editor: const ToggleSetting(),
read: (c) => c.stampTime,
write: (c, on) => SignatureConfig(stampTime: on, wide: c.wide),
),
CustomKindSetting<SignatureConfig, bool>(
id: 'wide',
label: 'Wide pad',
editor: const ToggleSetting(),
read: (c) => c.wide,
write: (c, on) => SignatureConfig(stampTime: c.stampTime, wide: on),
),
],
minimumSizeOf: (c) => c.wide
? const FormElementSize.wide(minHeight: 160)
: const FormElementSize.regular(minHeight: 120),
validators: {'acme.signedBy'},
);
/// The app's kinds and types, pure Dart, shared with its server.
final acmeKinds = FormKitExtension(id: 'acme', fieldTypes: [signatureType]);
final serverKit = FormKit([FormKitExtension.standard, acmeKinds]);decodeConfiguration receives the configurationVersion the field was saved with, so an older configuration can be read into the current shape. encodeConfiguration must return JSON values; the codec checks.
A settings editor is one of the kit's SettingEditors, the same vocabulary validators and rules use: TextSetting, RichTextSetting, ToggleSetting, ChoiceSetting (as a menu or a segmented strip), OptionSetSetting, StringListSetting, WholeNumberSetting, BoundedWholeSetting, DecimalSetting, DateSetting, DurationSetting, ChoiceListSetting, FieldSetting and FormulaSetting. The designer has one editor for each.
A field of the type, in code or with the DSL:
final field = signatureType.create('approval'); // its defaults
final dsl = custom('approval', 'Approval', signatureType,
configuration: const SignatureConfig(wide: true));A kit throws an ArgumentError if a typeId is registered twice, naming both extensions, or is a built-in type's. Decoding a form whose kit lacks a type throws a KitCodecException at the type (fields[2].type) naming the type and the extensions of the kit that read it: "Unknown form field type: acme.signature. The kit used has the extensions vyuh.form; register the type in one of them." A type whose settings can't be read throws a FormatException from decodeConfiguration; the codec reports it as a KitCodecException at the field's configuration, so a malformed document is refused with the place at fault.
The kit that knows the type saves and loads it, so there is no codec to hand the type to:
final json = serverKit.encodeString(form);
final restored = serverKit.decodeString(json);2. How it looks at fill time: the view part
The form draws the label, description, help and messages; a view part draws the input. Attach it to the type with withParts, and put the result in the kit: FormView draws its custom fields with it, and so does the designer's preview.
import 'package:vyuh_form_kit/vyuh_form_kit.dart';
final acme = signatureType.withParts(
view: ViewPart<CustomFieldInput<SignatureConfig>>(
(context, input) => SignaturePad( // the app's own widget
stampTime: input.configuration.stampTime,
value: input.value,
enabled: input.enabled,
onChanged: input.onChanged,
),
),
);
final appKit = FormKit([FormKitExtension.standard, acme]);
final filling = FormKitForm(definition, kit: appKit);
FormView(form: filling);The extension is named by the type's id (acme.signature). The parts it carries are the shared ViewPart and EditorPart of vyuh_kit, aimed at the type by their target (its type id), so one extension can carry the parts of several types. To keep the app's other kinds in the same extension, copy the parts into it: FormKitExtension(id: 'acme', fieldTypes: [signatureType, stampType], parts: [...acme.parts, ...stamp.parts]). A kit reads them with kit.fieldViewOf(typeId) and kit.fieldEditorOf(typeId).
In debug builds a FormView under a KitScope<FormKit> checks that every custom field and content type of the scope's kit has a view part, and names the type that has none: The KitScope<FormKit> has no view part for field type "acme.signature".
input.enabled is false while the form is disabled or a rule disables the field. A view part has no read-only mode of its own: when the form is read-only, or the field is read only, input.enabled stays true, so the value stays fully legible, and the form takes the input out of focus and pointer handling. See Access. A kit built without the view (a server's) draws "No renderer for Signature." in its place.
3. The designer: the editor part
An EditorPart<CustomFormField<C>> gives the type a palette entry (its label and icon, under the "Custom" category) and an inspector shown under the type's settings when a field of the type is selected. Every change the inspector reports is one undoable step.
import 'package:vyuh_form_kit/vyuh_form_kit.dart';
final acme = signatureType.withParts(
view: ViewPart<CustomFieldInput<SignatureConfig>>(signaturePad),
editor: EditorPart<CustomFormField<SignatureConfig>>(
label: 'Signature',
icon: Icons.draw,
inspector: (context, field, onChanged) => SignatureNotes(
field: field,
onChanged: onChanged,
),
),
);
final designerKit = FormKit([FormEditorStandard.extension, acme]);
final controller = FormKitEditorController(form, kit: designerKit);
FormKitEditor(controller: controller);To put the entry in a category of your own, or to name keywords, list a palette entry beside it:
import 'package:vyuh_form_kit_editor/vyuh_form_kit_editor.dart';
const approvals = PaletteCategory(id: 'approvals', label: 'Approvals');
final designerKit = FormKit([
FormEditorStandard.extension,
acmeKinds.withParts([
approvals,
FormEntries.field(
signatureType,
glyph: '✎',
category: approvals,
keywords: const ['sign', 'approve', 'e-signature'],
),
// The view part only: an editor part would add an entry of its own.
...signatureType.withParts(view: ViewPart(signaturePad)).parts,
]),
]);FormEntries.field builds a PaletteEntry of vyuh_kit that makes the field at the type's defaults, named by the entry's label (the type's title by default); give it a glyph or an icon; configure adjusts it, as the built-in "Long text" entry makes a text field multi-line. Selected, the field shows its settings in the inspector under the type's title, and each change is one undoable step. It drops at the size its configuration needs, and that size follows its settings.
A host that gives the forms and the designer their services puts the kit in a scope once:
KitScope<FormKit>(
kit: designerKit,
services: FormHostServices(assets: assets),
child: FormKitEditor(controller: controller),
);Custom content
Content an app shows, holding no value, works the same way with CustomContentType<C> (in the extension's contentTypes), CustomContentType.withParts(view:, editor:) (a ViewPart<CustomContent<C>>) and FormEntries.content.
final bannerType = CustomContentType<BannerConfig>(
typeId: 'acme.banner',
title: 'Banner',
defaultConfiguration: const BannerConfig(''),
encodeConfiguration: (c) => {'text': c.text},
decodeConfiguration: (json, _) => BannerConfig(json['text']! as String),
configurationSettings: [/* … */],
);
final banner = bannerType.withParts(
view: ViewPart<CustomContent<BannerConfig>>(
(context, content) => Text(content.configuration.text),
),
);Your own setting editor
When none of the kit's setting editors suits a setting, such as a colour or a stamp, name an editor of your own with CustomSetting<T>(editorId) and register it with the designer:
CustomKindSetting<SignatureConfig, Color>(
id: 'ink',
label: 'Ink',
editor: const CustomSetting<Color>('acme.colour'),
read: (c) => c.ink,
write: (c, ink) => c.copyWith(ink: ink),
);
acmeKinds.withParts([
ViewPart<SettingEditorInput>(
target: 'acme.colour',
(context, input) => ColourSwatches( // the app's own widget
value: input.value as Color,
onChanged: input.write,
),
),
]);The inspector draws the label, help and any refusal around it; input.write saves a value as one undoable step and says whether it was taken. A setting whose editor isn't registered says so in its place. Editor ids are unique across the kit.