Skip to content

Architecture

The Vyuh Form system follows a layered architecture with a clear split between runtime and editor concerns. Both packages share the same schema types but manage state independently.

Package Split

Layered Architecture

Descriptor System

Forms use the Vyuh content extension system for registration. Every component type has a TypeDescriptor that enables JSON deserialization and runtime discovery.

Feature Registration

The feature.dart file registers everything through a single FeatureDescriptor:

dart
final feature = FeatureDescriptor(
  name: 'forms',
  title: 'Forms',
  extensions: [
    ContentExtensionDescriptor(
      contents: [
        // Form descriptor with validations, conditions, actions
        FormDescriptor(
          validations: [
            RequiredValidation.typeDescriptor,
            EmailValidation.typeDescriptor,
            // ... 15 validation types
          ],
          conditions: [
            BooleanCondition.typeDescriptor,
            StringCondition.typeDescriptor,
            // ... Empty, Numeric, Date, and Compound conditions
          ],
          actions: [
            VisibilityAction.typeDescriptor,
            EnabledAction.typeDescriptor,
            // ... Focus, SetValue, Compound, and Validation actions
          ],
        ),

        // StepForm descriptor with layout variants
        StepFormDescriptor(
          layouts: [
            DefaultStepFormLayout.typeDescriptor,
            VerticalStepperStepFormLayout.typeDescriptor,
            SingleStepLayout.typeDescriptor,
          ],
        ),

        // Field descriptors (one per field type)
        TextFieldDescriptor(),
        SelectFieldDescriptor(layouts: [...]),
        BooleanFieldDescriptor(layouts: [...]),
        // ... all 12 field types
      ],
      contentBuilders: [
        Form.contentBuilder,
        FormRowBlock.contentBuilder,
        FormSection.contentBuilder,
        RepeatingSection.contentBuilder,
        StepForm.contentBuilder,
        TextField.contentBuilder,
        // ... all field content builders
      ],
    ),
  ],
);

FormDescriptor

FormDescriptor extends ContentDescriptor and carries the registration lists for cross-cutting concerns:

dart
class FormDescriptor extends ContentDescriptor {
  final List<TypeDescriptor<ValidationConfiguration>>? validations;
  final List<TypeDescriptor<AsyncValidationConfiguration>>? asyncValidations;
  final List<TypeDescriptor<FormAsyncValidationConfiguration>>? formAsyncValidations;
  final List<TypeDescriptor<RuleCondition>>? conditions;
  final List<TypeDescriptor<RuleAction>>? actions;
  final List<TypeDescriptor<FormFieldRule>>? rules;
}

The FormContentBuilder collects all descriptors during init() and registers them into the content type system, enabling JSON deserialization of any registered type.

FormGroup as Single Source of Truth

The Form class creates a single FormGroup that owns all field state:

Key design decisions:

  1. Flat structure -- Fields inside FormSection are registered at the top level of the FormGroup. Sections are visual only.
  2. Rows are structural -- FormRowBlock creates no control; it wraps fields in {field, span} row items so layout stays separate from field state.
  3. Repeating sections use arrays -- RepeatingSection registers a FormArray where each instance is a FormGroup built from the template.
  4. Bridged validators -- ValidationConfiguration instances are bridged to reactive_forms Validator instances at construction time.
  5. Dynamic validators -- Fields with ValidationAction rules get a DynamicValidator that can activate/deactivate validators at runtime.
  6. Context-aware validators -- Fields with DateDependencyValidation (or similar) get cross-field subscriptions that re-validate when dependent fields change.
  7. MobX bridge -- The form's valid observable is updated via formGroup.valueChanges, enabling MobX Observer widgets to react to validity changes.

Editor Architecture

The visual editor (vyuh_form_editor) uses a different state model:

dart
// Store manages editor state with MobX
final store = FormEditorStore.withDefaults();

// Registry holds all registered types
final registry = store.registry;
registry.fieldTypes;        // All available field types
registry.validationTypes;   // All validation types
registry.ruleConditionTypes; // All condition types
registry.ruleActionTypes;   // All action types

// The editor widget composes three panels
FormEditor(
  store: store,
  onFieldsChanged: (fields) { /* ... */ },
)

The FormEditorRegistry is the sole compositor -- types never resolve other types. All cross-type composition (adding rules to fields, resolving conditions/actions in rules, mapping validations) flows through the registry's composition methods.

The editor model is block-based. FormInstance, SectionInstance, RepeatingSectionInstance, and RowInstance all use BlockContainer to declare which blocks they accept:

ContainerAccepted blocks
FormInstancefields, rows, sections, repeating sections
SectionInstancefields and rows
RepeatingSectionInstancefields and rows
RowInstancefields only

This is why drag/drop, palette tap-to-add, import, and command-history operations can share the same addBlock, removeBlock, and move commands.

Editor Data Flow

The editor outputs JSON compatible with vyuh_feature_forms. The JSON can be stored in a CMS, file, or database, then deserialized at runtime into a live Form with full validation and rules.

Next Steps