Cross-Field Validation
Cross-field validation enforces rules that involve two or more fields — such as password confirmation, date range checks (start before end), or ensuring at least one contact method is provided. It uses the allValues argument available to all custom validators, or the matchesField built-in shorthand.
Built-In: matchesField
The matchesField rule is the simplest cross-field check — it verifies that two fields have identical values. It is the standard approach for confirm-password and confirm-email patterns:
{
"id": "password",
"type": "password",
"label": "Password",
"validation": {
"required": true,
"minLength": 12
}
},
{
"id": "confirm-password",
"type": "password",
"label": "Confirm Password",
"validation": {
"required": true,
"matchesField": "password",
"matchesFieldMessage": "Passwords do not match"
}
}
Cross-Field via Inline Expression
For date range validation, use an inline expression in customRule. The expression has access to value (current field) and allValues (all form values):
{
"id": "start-date",
"type": "date",
"label": "Start Date",
"validation": { "required": true }
},
{
"id": "end-date",
"type": "date",
"label": "End Date",
"validation": {
"required": true,
"customRule": {
"expression": "!allValues['start-date'] || value > allValues['start-date']",
"message": "End date must be after start date"
}
}
}
Cross-Field via Custom Validator
For more complex cross-field logic, write a named custom validator that reads allValues:
// Register a cross-field validator
import { registerValidator } from '@atlas-forms/validation-js';
registerValidator('contractValueWithinBudget', (value, fieldId, allValues) => {
const contractValue = parseFloat(value) || 0;
const approvedBudget = parseFloat(allValues['approved-budget']) || 0;
return {
valid: contractValue <= approvedBudget,
message: `Contract value ($${contractValue.toLocaleString()}) exceeds approved budget ($${approvedBudget.toLocaleString()})`
};
});
// Use in schema
{
"id": "contract-value",
"type": "number",
"label": "Contract Value",
"validation": {
"required": true,
"min": 0,
"customRule": {
"name": "contractValueWithinBudget",
"message": "Contract value exceeds approved budget"
}
}
}
Form-Level Validators
Form-level validators run after all field-level validation and can produce errors that are not tied to a specific field. Define them in the schema root's formValidators array:
// Schema root-level formValidators
{
"formValidators": [
{
"name": "atLeastOneContactMethod",
"message": "At least one contact method (email, phone, or post) must be provided"
}
]
}
// Register the validator
registerValidator('atLeastOneContactMethod', (value, fieldId, allValues) => {
const hasEmail = !!allValues['contact-email'];
const hasPhone = !!allValues['contact-phone'];
const hasPost = allValues['contact-method'] === 'post';
return {
valid: hasEmail || hasPhone || hasPost,
message: 'Provide at least one contact method: email, phone, or postal address'
};
});
Re-triggering Cross-Field Validation
When the user changes field A that a cross-field rule on field B depends on, field B's validation must re-run. Configure this with revalidateOnChange:
// The confirm-password field re-validates whenever "password" changes
{
"id": "confirm-password",
"type": "password",
"label": "Confirm Password",
"validation": {
"matchesField": "password",
"revalidateOnChange": ["password"]
}
}