Portal Community

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"]
  }
}
Best Practice Place the cross-field error message on the dependent field (the one doing the comparison), not on the source field. Users expect the "Confirm Password" field to show an error, not the original "Password" field. This also means clearing the error naturally when the dependent field is corrected.