Skip to content

Validation

The form system supports four categories of validation: synchronous, asynchronous, soft (warnings), and context-aware (cross-field). All validations extend ValidationConfiguration and are bridged to reactive_forms validators at form construction time.

Validation Pipeline

Synchronous Validation

Sync validators run immediately when a field value changes. They return an error message string or null.

ValidationConfiguration

dart
abstract class ValidationConfiguration implements SchemaItem {
  final String schemaType;
  final String title;
  final String? errorMessage;

  bool get isSoft => false;  // Override for soft validations

  String? validate<T>(T? value);
}

Built-in Sync Validators

ValidatorSchema NameProperties
RequiredValidationformfield.validation.requirederrorMessage
EmailValidationformfield.validation.text.emailerrorMessage
UrlValidationformfield.validation.text.urlprotocols, hostBlacklist, hostWhitelist
PatternValidationformfield.validation.patternpattern, allowEmpty
TextLengthValidationformfield.validation.text.lengthoperator (min/max/equal), length
MinLengthValidationformfield.validation.minLengthminLength
MaxLengthValidationformfield.validation.maxLengthmaxLength
NumberRangeValidationformfield.validation.numberRangemin, max
DateRangeValidationformfield.validation.dateRangestartDate, endDate
CreditCardValidationformfield.validation.text.creditCardLuhn algorithm
IpAddressValidationformfield.validation.text.ipAddressIPv4 and IPv6
FutureDateValidationformfield.validation.futureDatemaxFutureDays, maxFutureHours, includeTime
PastDateValidationformfield.validation.pastDatemaxPastDays, maxPastHours, includeTime
BusinessHoursValidationformfield.validation.businessHoursallowedWeekdays, timeRanges, excludedDates, dateOnly

Example: Enrollment Validation

dart
TextField(
  name: 'email',
  title: 'Email',
  validations: [
    RequiredValidation(errorMessage: 'Email is required'),
    EmailValidation(errorMessage: 'Enter a valid email'),
  ],
)

DateTimeField(
  name: 'start_date',
  title: 'Start Date',
  selectionType: SelectionType.date,
  validations: [
    RequiredValidation(errorMessage: 'Start date is required'),
    FutureDateValidation(
      maxFutureDays: 90,
      errorMessage: 'Cannot schedule more than 90 days ahead',
    ),
    BusinessHoursValidation(
      allowedWeekdays: [1, 2, 3, 4, 5], // Mon-Fri
      timeRanges: [TimeRange(startTime: '09:00', endTime: '17:00')],
      dateOnly: true,
      errorMessage: 'Must be a business day',
    ),
  ],
)

Asynchronous Validation

Async validators run after sync validators pass, with configurable debounce. They are bridged to reactive_forms AsyncValidator instances.

dart
abstract class AsyncValidationConfiguration implements SchemaItem {
  final Duration timeout;
  final Duration debounceDelay;
  final String? errorMessage;

  Future<String?> validateAsync(dynamic value);
}

Async validations are useful for server-side checks like duplicate email detection or username availability.

dart
TextField(
  name: 'email',
  title: 'Email',
  validations: [RequiredValidation(errorMessage: 'Required')],
  asyncValidations: [
    CustomAsyncValidation(
      // Server-side uniqueness check
      errorMessage: 'This email is already registered',
    ),
  ],
)

Form-Level Async Validation

For cross-field async checks (e.g., "this combination of course + date is already booked"), use formAsyncValidations on the Form:

dart
Form(
  title: 'Enrollment',
  items: [...],
  formAsyncValidations: [
    BookingConflictValidation(),
  ],
)

Form-level async validations run on submit via form.validateAllAsync() and can set errors on specific fields.

Soft Validation (Warnings)

Soft validations warn the user but do not block form submission. They show amber/yellow indicators instead of red errors.

dart
abstract class SoftValidation extends ValidationConfiguration {
  @override
  bool get isSoft => true;

  SoftBreachDetails? getBreachDetails<T>(T? value, String fieldName);
}

SoftRangeValidation

dart
NumberField(
  name: 'temperature',
  title: 'Temperature',
  validations: [
    // Hard validation: block submission
    NumberRangeValidation(min: -50, max: 200, errorMessage: 'Out of range'),
    // Soft validation: warn but allow
    SoftRangeValidation(
      softMin: 15.0,
      softMax: 35.0,
      warningMessage: 'Temperature outside normal range',
      triggersException: true,
      exceptionSeverity: ExceptionSeverity.medium,
    ),
  ],
)

Collecting Soft Breaches

dart
final breaches = form.collectSoftBreaches();
for (final breach in breaches) {
  print('${breach.fieldName}: ${breach.warningMessage}');
  print('  Value: ${breach.value}');
  print('  Expected: ${breach.softMin} - ${breach.softMax}');
  print('  Severity: ${breach.exceptionSeverity}');
}

Context-Aware Validation (Cross-Field)

Context-aware validations depend on other fields' values. They implement the ContextAwareValidation mixin:

dart
mixin ContextAwareValidation {
  String? validateWithContext<T>(T? value, Map<String, dynamic> formValues);
  Set<String> getDependentFields();
}

The form automatically subscribes to value changes on dependent fields and re-validates the target field when they change.

DateDependencyValidation

The built-in cross-field validator for date comparisons:

dart
DateTimeField(
  name: 'end_date',
  title: 'End Date',
  selectionType: SelectionType.date,
  validations: [
    DateDependencyValidation(
      dependentFieldName: 'start_date',
      operator: DateComparisonOperator.after,
      offsetDays: 1,
      errorMessage: 'End date must be at least 1 day after start date',
    ),
  ],
)

DateComparisonOperator values:

OperatorMeaning
beforeMust be before the dependent field
afterMust be after the dependent field
beforeOrEqualMust be before or same day
afterOrEqualMust be after or same day
sameDayMust be the same day
differentDayMust be a different day

How Cross-Field Validation Works

  1. During Form._buildFormGroup(), fields with ContextAwareValidation are detected
  2. A _ContextAwareBridgedValidator is added to the FormControl
  3. Subscriptions are created: when any dependent field changes, the target field re-validates
  4. At validation time, the bridged validator reads the full formGroup.rawValue and delegates to validateWithContext()

DynamicValidator

DynamicValidator is used by the rules system to add or remove validations at runtime. It is attached to fields that have ValidationAction rules.

dart
class DynamicValidator extends Validator<dynamic> {
  void activate(String key, ValidationConfiguration config);
  void deactivate(String key);
}

When a ValidationAction rule fires, it calls activate() or deactivate() on the field's DynamicValidator, which adds or removes the validation from the active set.

DSL Validation Methods

The FieldBuilder base class provides shared validation methods:

dart
.required([String? message])
.email([String? message])
.pattern(String regex, {String? message})
.minLength(int min, {String? message})
.maxLength(int max, {String? message})
.identifier([String? message])
.asyncValidation(AsyncValidationConfiguration validation)

Field-specific builders add type-specific methods:

dart
// NumberFieldBuilder
.range(min: 1, max: 100)
.min(1)
.max(100)

Next Steps