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 ​