Skip to content

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 carriesUsed by
typeId, titlethe JSON, the palette, the inspector header
defaultConfigurationa new field from the palette or the DSL
encodeConfiguration / decodeConfiguration, configurationVersionthe kit's codec (kit.encode, kit.decode)
configurationSettingsthe inspector: a section of the type's own settings
minimumSizeOf(configuration)rows and grids: the least room a field needs
rulesRead, formulasReadthe operators a condition offers, and whether formulas can read it
validatorsthe 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 ​

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

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

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

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

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

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

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

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

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