Skip to content

Form Model

The form model consists of runtime content items that define form structure: Form, FormRowBlock, FormSection, RepeatingSection, and StepForm.

Form

Form is the top-level container and the single source of truth for all form state. It extends ContentItem and creates a FormGroup from its items.

dart
class Form extends ContentItem {
  static const schemaName = 'vyuh.form';

  final String formId;       // Unique identifier (auto-generated via nanoid)
  final String? title;
  final String? description;
  final double hSpacing;     // Horizontal grid spacing (default: 16.0)
  final double vSpacing;     // Vertical grid spacing (default: 16.0)
  final List<ContentItem> items;  // Fields, rows, sections, repeating sections
  final List<FormAsyncValidationConfiguration>? formAsyncValidations;
  final List<FormValidationConfiguration>? formValidations;

  late final FormGroup formGroup;  // reactive_forms FormGroup
  final Observable<bool> valid;    // MobX-reactive validity
}

Creating a Form

Via the DSL:

dart
final form = FormBuilder(title: 'Course Enrollment')
  .text('name', title: 'Student Name').required()
  .text('email', title: 'Email').email()
  .build();

Via constructor (typically from JSON):

dart
final form = Form(
  title: 'Course Enrollment',
  items: [
    TextField(name: 'name', title: 'Student Name', validations: [...]),
    TextField(name: 'email', title: 'Email', validations: [...]),
  ],
);

Form API

MethodDescription
currentValuesReturns Map<String, dynamic> of all current field values
getFieldValue(name)Returns the value of a specific field
setFieldValue(name, value)Sets a single field's value
patchValues(map)Patches multiple field values at once
validate()Marks all fields as touched, returns bool
saveAndValidate()Alias for validate() (values are always live)
isValidSynchronous validity check
resetForm()Resets to initial values, clears validation state
isDirtyWhether any field was modified
isTouchedWhether any field was interacted with
validateAllAsync()Runs all async validations, returns Future<bool>
getAllErrors()Returns Map<String, String> of all validation errors
collectSoftBreaches()Returns List<SoftBreachDetails> of soft validation warnings
addListener(callback)Fires when form values change
dispose()Cleans up all subscriptions

Initial Values

You can pre-populate a form at construction time or after:

dart
// At construction (via DSL)
final form = FormBuilder(title: 'Edit Student')
  .text('name', title: 'Name').required()
  .text('email', title: 'Email').email()
  .build(initialValues: {'name': 'Alice', 'email': 'alice@lms.edu'});

// After construction
form.initialValues = {'name': 'Bob', 'email': 'bob@lms.edu'};

Setting initialValues patches the FormGroup and resets touched/pristine state so initial values do not trigger error display.

FormSection

FormSection is a structural container that groups fields visually. It is not a FormField and creates no form control.

dart
class FormSection extends ContentItem {
  static const schemaName = 'vyuh.form.section';

  final String title;
  final String? description;
  final double? hSpacing;     // Null inherits from parent form
  final double? vSpacing;
  final bool collapsible;
  final bool initiallyCollapsed;
  final List<ContentItem> items;  // Child fields
  final List<FormFieldRule> rules; // Section-level visibility rules
}

Key characteristics:

  • Flat FormGroup -- Fields inside a section are registered in the parent Form's FormGroup, not in a nested group.
  • Visual only -- Sections affect rendering (title, description, collapse) but not data structure.
  • Section rules -- Sections can have visibility rules that show/hide all children together.
  • Spacing override -- Sections can override the parent form's spacing.
dart
// Via DSL
FormBuilder(title: 'Application')
  .section('Personal Info', (s) => s
    .text('first_name', title: 'First Name')
    .text('last_name', title: 'Last Name')
    .text('email', title: 'Email'),
    collapsible: true,
    description: 'Your contact information',
  )
  .section('Employment', (s) => s
    .text('company', title: 'Company')
    .text('position', title: 'Position'),
  )
  .build();

// FormGroup has: {first_name, last_name, email, company, position}

FormRowBlock

FormRowBlock is a structural row. It is a ContentItem, not a FormField, so it creates no control and holds no value. Each child is a FormRowItem wrapper containing a FormField plus a span.

dart
FormRowBlock(
  items: [
    FormRowItem(
      field: TextField(name: 'first_name', title: 'First Name'),
      span: 6,
    ),
    FormRowItem(
      field: TextField(name: 'last_name', title: 'Last Name'),
      span: 6,
    ),
  ],
)

Rows pack fields left-to-right in declaration order. There is no start column and no field-level width property; row width is represented only by the FormRowItem.span values. The visual editor mirrors this runtime model with RowInstance, where row children are FieldInstances and spans are stored by field id.

RepeatingSection

RepeatingSection holds a template of fields that can be repeated N times (dynamic add/remove). It uses a FormArray in the FormGroup.

dart
class RepeatingSection extends ContentItem {
  static const schemaName = 'vyuh.form.repeating_section';

  final String title;
  final String? description;
  final String name;             // Namespace for field names
  final List<ContentItem> template; // Template fields
  final int minInstances;        // Minimum copies (default: 1)
  final int? maxInstances;       // Maximum copies (null = unlimited)
  final RepeatDisplayMode displayMode; // accordion or expanded
  final bool collapsible;
}

The collected values output as an array:

json
{
  "modules": [
    { "module_title": "Introduction", "duration": 2 },
    { "module_title": "Advanced Topics", "duration": 4 }
  ]
}

Field names are namespaced: modules.0.module_title, modules.1.module_title, etc.

dart
// Repeating section for course modules
RepeatingSection(
  title: 'Course Modules',
  name: 'modules',
  minInstances: 1,
  maxInstances: 10,
  displayMode: RepeatDisplayMode.accordion,
  template: [
    TextField(name: 'module_title', title: 'Module Title', validations: [...]),
    NumberField(name: 'duration', title: 'Duration (hours)', validations: [...]),
  ],
)

StepForm

StepForm is a multi-step wizard where each step is a complete Form. It supports sequential navigation, progress tracking, and draft saving.

dart
final class StepForm extends ContentItem {
  static const schemaName = 'vyuh.stepForm';

  final String? title;
  final StepFormControllerConfiguration controller;
  final Action? submitAction;
  final List<Form> steps;
  final ProgressTrackingType trackProgress; // none, all, required
  final bool allowDrafts;
  final bool isSequential;
  final bool showStepStatus;
}

ProgressTrackingType

ValueBehavior
noneNo progress tracking
allTrack completion of all fields
requiredTrack only required field completion

StepForm Layouts

LayoutDescription
DefaultStepFormLayoutHorizontal step indicator with animated transitions
VerticalStepperStepFormLayoutMaterial vertical stepper
SingleStepLayoutShows one step at a time, no indicator

StepFormDescriptor

Register custom controllers via StepFormDescriptor:

dart
StepFormDescriptor(
  layouts: [
    DefaultStepFormLayout.typeDescriptor,
    VerticalStepperStepFormLayout.typeDescriptor,
    SingleStepLayout.typeDescriptor,
  ],
  controllers: [
    MyCustomController.typeDescriptor,
  ],
)

Container Hierarchy

Next Steps