Cross-Field Validation
Cross-field validation checks values across multiple fields. The form system supports this through the ContextAwareValidation mixin, which provides reactive cross-field subscriptions.
How It Works
- A validation implements the
ContextAwareValidationmixin - It declares which fields it depends on via
getDependentFields() - The form wires subscriptions: when a dependent field changes, the target field re-validates
- At validation time,
validateWithContext()receives the full form values map
DateDependencyValidation
The built-in cross-field date validator:
dart
// End date must be after start date
DateTimeField(
name: 'end_date',
title: 'Course End Date',
selectionType: SelectionType.date,
validations: [
RequiredValidation(errorMessage: 'End date is required'),
DateDependencyValidation(
dependentFieldName: 'start_date',
operator: DateComparisonOperator.after,
errorMessage: 'End date must be after start date',
),
],
)With Offset
dart
// End date must be at least 7 days after start date
DateDependencyValidation(
dependentFieldName: 'start_date',
operator: DateComparisonOperator.afterOrEqual,
offsetDays: 7,
errorMessage: 'Course must run at least 7 days',
)Comparison Operators
| Operator | Description |
|---|---|
before | Must be before the dependent field |
after | Must be after the dependent field |
beforeOrEqual | Must be before or same day |
afterOrEqual | Must be after or same day |
sameDay | Must be the same day |
differentDay | Must be a different day |
LMS Example: Course Schedule
dart
final form = FormBuilder(title: 'Course Schedule')
.dateTime('registration_open', title: 'Registration Opens')
.dateOnly()
.required()
.dateTime('registration_close', title: 'Registration Closes')
.dateOnly()
.required()
.dateTime('course_start', title: 'Course Start')
.dateOnly()
.required()
.dateTime('course_end', title: 'Course End')
.dateOnly()
.required()
.build();With cross-field validations added via constructors:
dart
DateTimeField(
name: 'registration_close',
title: 'Registration Closes',
selectionType: SelectionType.date,
validations: [
RequiredValidation(errorMessage: 'Required'),
DateDependencyValidation(
dependentFieldName: 'registration_open',
operator: DateComparisonOperator.after,
errorMessage: 'Must be after registration opens',
),
],
),
DateTimeField(
name: 'course_start',
title: 'Course Start',
selectionType: SelectionType.date,
validations: [
RequiredValidation(errorMessage: 'Required'),
DateDependencyValidation(
dependentFieldName: 'registration_close',
operator: DateComparisonOperator.afterOrEqual,
offsetDays: 1,
errorMessage: 'Must be at least 1 day after registration closes',
),
],
),
DateTimeField(
name: 'course_end',
title: 'Course End',
selectionType: SelectionType.date,
validations: [
RequiredValidation(errorMessage: 'Required'),
DateDependencyValidation(
dependentFieldName: 'course_start',
operator: DateComparisonOperator.afterOrEqual,
offsetDays: 7,
errorMessage: 'Course must run at least 7 days',
),
],
),When the user changes "Registration Opens", the "Registration Closes" field re-validates automatically. The chain continues through "Course Start" and "Course End".
Custom Context-Aware Validation
To create your own cross-field validator:
dart
@JsonSerializable()
class PasswordMatchValidation extends ValidationConfiguration
with ContextAwareValidation {
static const schemaName = 'formfield.validation.passwordMatch';
static final typeDescriptor = TypeDescriptor(
schemaType: schemaName,
title: 'Password Match',
fromJson: PasswordMatchValidation.fromJson,
);
final String passwordFieldName;
PasswordMatchValidation({
required this.passwordFieldName,
required super.errorMessage,
}) : super(schemaType: schemaName, title: 'Password Match');
factory PasswordMatchValidation.fromJson(Map<String, dynamic> json) =>
_$PasswordMatchValidationFromJson(json);
@override
String? validate<T>(T? value) => null; // Handled by validateWithContext
@override
Set<String> getDependentFields() => {passwordFieldName};
@override
String? validateWithContext<T>(T? value, Map<String, dynamic> formValues) {
if (value == null) return null;
final password = formValues[passwordFieldName]?.toString();
if (password == null) return null;
return value.toString() == password ? null : errorMessage;
}
}Next Steps
- Validation -- Complete validation pipeline
- Validation Patterns -- More validation examples
- Repeating Sections -- Dynamic field arrays