Skip to content

Custom fields ​

An app field type is a CustomFormField<T> whose configuration is a typed class you own. Today a custom type takes four registrations in three packages, tied together by its typeId:

#WhatPackageWhere it goes
1A codec for the configurationvyuh_form_typesFormCodecRegistry(fields: [...]), passed to FormCodec and FormKitEditor(codecs:)
2A fill-time controlvyuh_form_kitFormView(customBuilder:)
3A palette entryvyuh_form_kit_editorFormKitEditor(extraPaletteEntries:)
4A palette category (optional)vyuh_form_kit_editorFormKitEditor(extraPaletteCategories:)

The example adds a QA signature field.

1. Configuration and codec ​

dart
import 'package:vyuh_form_types/vyuh_form_types.dart';

const signatureTypeId = 'acme.signature';

final class SignatureConfig implements CustomFieldConfiguration {
  const SignatureConfig({this.stampTime = true});
  final bool stampTime;
}

final signatureCodec = CustomFieldCodec(
  typeId: signatureTypeId,
  encode: (config) => switch (config) {
    SignatureConfig(:final stampTime) => {'stampTime': stampTime},
    _ => throw ArgumentError.value(config, 'config', 'Not a SignatureConfig'),
  },
  decode: (json, version) =>
      SignatureConfig(stampTime: json['stampTime'] as bool? ?? true),
);

/// One registry for every custom type the app stores.
final appCodecs = FormCodecRegistry(fields: [signatureCodec]);

CustomFormField<SignatureConfig> signatureField(String key) =>
    CustomFormField<SignatureConfig>(
      key: key,
      label: 'QA signature',
      typeId: signatureTypeId,
      configuration: const SignatureConfig(),
      required: true,
    );

decode receives the configurationVersion the field was saved with, so you can migrate old settings.

FormCodecRegistry throws a FormatException if a typeId is registered twice or collides with a built-in type. Encoding a form with a custom field and no matching codec throws "No field codec registered for acme.signature."

dart
final json = FormCodec.encodeString(form, extensions: appCodecs);
final restored = FormCodec.decodeString(json, extensions: appCodecs);

Custom content works the same way with CustomContent<T>, CustomContentConfiguration and CustomContentCodec, registered through FormCodecRegistry(content: [...]).

2. Fill-time control ​

FormView calls one customBuilder for every custom field. Switch on the type id:

dart
import 'package:flutter/material.dart' hide TextFormField;
import 'package:vyuh_form_kit/vyuh_form_kit.dart';

Widget buildCustomField(
  BuildContext context,
  FormFieldDefinition field,
  Object? value,
  ValueChanged<Object?> onChanged,
) => switch (field) {
  CustomFormField(typeId: signatureTypeId, configuration: final SignatureConfig config) =>
    SignaturePad( // the app's own widget
      stampTime: config.stampTime,
      value: value,
      onChanged: onChanged,
    ),
  _ => Text('No renderer for ${field.type}'),
};

// FormView(definition: form, values: values, onChanged: onChanged,
//          customBuilder: buildCustomField)

Without a customBuilder, FormView shows "Missing control for acme.signature." in the field's place.

3. Palette entry and editor wiring ​

dart
import 'package:flutter/material.dart' hide TextFormField;
import 'package:vyuh_form_kit_editor/vyuh_form_kit_editor.dart';

const approvals = FormPaletteCategory(id: 'approvals', title: 'Approvals', order: 10);

final signatureEntry = FormPaletteEntry(
  id: 'signature',
  title: 'Signature',
  icon: const ElementIcon.glyph('✎'),
  category: approvals,
  keywords: const ['sign', 'approve', 'e-signature'],
  create: (context) => NewField(signatureField(context.nextKey('signature'))),
);

Widget acmeEditor(FormKitEditorController controller) => FormKitEditor(
  controller: controller,
  extraPaletteEntries: [signatureEntry],
  extraPaletteCategories: const [approvals],
  codecs: appCodecs, // for Definition JSON export and import
);

create receives a FormPaletteContext; its nextKey and nextId return names not yet used in the definition. See The editor for categories and ordering.

Gaps today ​

  • No inspector properties. The field inspector is a closed switch over the sealed field types, so a custom field gets no properties of its own. SignatureConfig.stampTime cannot be edited in the designer. The same applies to CustomContent.
  • No preview control. The editor's Preview does not take a customBuilder, so a custom field shows the "Missing control" placeholder there.
  • No custom content renderer. FormView shows "Unsupported content" for CustomContent.
  • Fixed traits. Every custom field has the 3 × 1, 180 px minimum size and cannot be read by formulas.
  • Several registrations. The codec, control and palette entry are registered separately and must agree on the typeId by convention.

Planned: one descriptor per type ​

A single field-type descriptor will carry the codec, a default factory, the minimum size, the formula type, the validators that apply, the fill-time renderer, the inspector sections and the palette entry. Custom content gets the same shape. An editor contribution will bundle field types, palette categories, validators, rule kinds, presentations and actions, so an app hands the editor one object. Names are not final. See the Roadmap.