Pattern 14: Cross-Field Validation
Part II: Interaction Patterns - Relationship Patterns
Opening Scenario: The Date That Came Before It Started
Michael was booking a conference room. He filled out the reservation form:
Start Date: 2024-12-15
End Date: 2024-12-10
He clicked Submit. The system accepted it.
The facilities manager, Sarah, looked at the reservation in disbelief. "The event ends five days before it starts? That's impossible!"
She looked at the validation code:
// Bad: Only validates individual fields
function validateStartDate(date) {
return date instanceof Date && !isNaN(date);
}
function validateEndDate(date) {
return date instanceof Date && !isNaN(date);
}
// Both dates are valid dates!
// But the relationship between them is invalid
Sarah added cross-field validation:
// Better: Validate the relationship
function validateDateRange(startDate, endDate) {
if (endDate <= startDate) {
return {
valid: false,
message: "End date must be after start date",
fields: ['startDate', 'endDate']
};
}
return { valid: true };
}
But Sarah discovered many more cross-field validation issues:
Budget allocation:
Category A: $5,000
Category B: $3,000
Category C: $4,000
Total: $12,000
Budget Limit: $10,000 ← Problem!
Each category was valid individually, but together they exceeded the limit.
Contact information:
Primary Contact: John Smith
Email: john@example.com
Phone: (555) 123-4567
Emergency Contact: John Smith ← Same person!
Email: john@example.com
Phone: (555) 123-4567
All fields individually valid, but logically wrong - emergency contact can't be the same person.
Shipping calculations:
Item 1: $50.00
Item 2: $30.00
Item 3: $20.00
Subtotal: $95.00 ← Should be $100.00
Line items were valid, but subtotal didn't match their sum.
Password confirmation:
Password: MySecurePass123!
Confirm Password: MySecurePass124! ← Doesn't match
Both passwords met complexity requirements individually, but they didn't match each other.
Sarah built a comprehensive cross-field validation system:
class CrossFieldValidator {
constructor() {
this.rules = [];
}
addRule(rule) {
this.rules.push({
name: rule.name,
fields: rule.fields,
validate: rule.validate,
message: rule.message,
severity: rule.severity || 'error'
});
}
validateAll(formData) {
const violations = [];
this.rules.forEach(rule => {
const result = rule.validate(formData);
if (!result.valid) {
violations.push({
rule: rule.name,
fields: rule.fields,
message: result.message || rule.message,
severity: rule.severity
});
}
});
return violations;
}
}
// Define validation rules
const validator = new CrossFieldValidator();
// Date range validation
validator.addRule({
name: 'date-range',
fields: ['startDate', 'endDate'],
validate: (form) => {
if (form.endDate <= form.startDate) {
return {
valid: false,
message: 'End date must be after start date'
};
}
return { valid: true };
}
});
// Budget total validation
validator.addRule({
name: 'budget-total',
fields: ['categories', 'budgetLimit'],
validate: (form) => {
const total = form.categories.reduce((sum, cat) => sum + cat.amount, 0);
if (total > form.budgetLimit) {
return {
valid: false,
message: `Total allocation ($${total}) exceeds budget limit ($${form.budgetLimit})`
};
}
return { valid: true };
}
});
// Password match validation
validator.addRule({
name: 'password-match',
fields: ['password', 'confirmPassword'],
validate: (form) => {
if (form.password !== form.confirmPassword) {
return {
valid: false,
message: 'Passwords do not match'
};
}
return { valid: true };
}
});
The new system caught relationship errors immediately, with clear visual feedback showing which fields were in conflict. Booking errors dropped to zero.
Context
Cross-Field Validation applies when:
Field relationships exist: One field's validity depends on another
Totals must match: Sum of parts must equal whole
Ranges have boundaries: Start < End, Min < Max
Uniqueness across fields: Same value can't appear in multiple fields
Conditional logic: If Field A, then Field B must be...
Consistency required: Related fields must stay synchronized
Business rules span fields: Policies involve multiple values
Problem Statement
Most validation checks fields in isolation, missing relationship errors:
Independent validation only:
// Bad: Each field validated separately
function validateAge(age) {
return age >= 0 && age <= 120;
}
function validateRetirementAge(retAge) {
return retAge >= 55 && retAge <= 75;
}
// But what if retirement age < current age?
// What if someone is 30 and retiring at 65? Valid!
// What if someone is 70 and retiring at 60? Invalid!
No relationship checking:
// Bad: Doesn't check if total matches sum
const lineItems = [
{ price: 10.00, quantity: 2 }, // $20
{ price: 15.00, quantity: 3 } // $45
];
const total = 70.00; // Should be $65!
// No validation catches this
Late discovery:
// Bad: Server-side only validation
// User fills entire form
// Submits
// Server says: "End date before start date"
// User has to go back and fix
Poor error messages:
// Bad: Doesn't indicate which fields are involved
"Invalid data" // What's invalid?
"Date error" // Which date? Why?
No visual connection:
// Bad: Highlights one field but not the relationship
startDate.classList.add('error');
// But endDate is also part of the problem
// User doesn't see the relationship
We need validation that checks field relationships, catches conflicts early, and clearly shows which fields are involved.
Forces
Timing vs Performance
- Real-time validation provides immediate feedback
- But checking relationships on every keystroke is expensive
- Balance responsiveness with performance
Completeness vs Clarity
- Check all possible relationships thoroughly
- But too many errors overwhelm users
- Balance comprehensive validation with usability
Proactive vs Reactive
- Prevent invalid combinations as user types
- Or catch them at submission
- Balance prevention with flexibility
Local vs Global
- Check each relationship independently
- Or consider all relationships together
- Balance modularity with holistic view
Strict vs Forgiving
- Enforce all rules rigidly
- Or allow exceptions in edge cases
- Balance data quality with user needs
Solution
Implement multi-field validation rules that check relationships between fields, provide clear feedback about which fields conflict and why, and integrate with form state to prevent invalid combinations proactively.
The pattern has five key strategies:
1. Declarative Relationship Rules
Define cross-field rules explicitly:
class CrossFieldRuleDefinition {
// Date range relationships
static dateRange(startField, endField, config = {}) {
return {
type: 'date-range',
fields: [startField, endField],
validate: (form) => {
const start = new Date(form[startField]);
const end = new Date(form[endField]);
if (end <= start) {
return {
valid: false,
message: config.message || `${endField} must be after ${startField}`,
suggestion: `Set ${endField} to at least one day after ${startField}`
};
}
// Optional: Check maximum duration
if (config.maxDuration) {
const daysDiff = (end - start) / (1000 * 60 * 60 * 24);
if (daysDiff > config.maxDuration) {
return {
valid: false,
message: `Duration cannot exceed ${config.maxDuration} days`,
suggestion: `Reduce the date range`
};
}
}
return { valid: true };
}
};
}
// Numeric range relationships
static numericRange(minField, maxField, config = {}) {
return {
type: 'numeric-range',
fields: [minField, maxField],
validate: (form) => {
const min = parseFloat(form[minField]);
const max = parseFloat(form[maxField]);
if (max <= min) {
return {
valid: false,
message: `${maxField} must be greater than ${minField}`
};
}
return { valid: true };
}
};
}
// Sum equals total
static sumEquals(itemsField, totalField, config = {}) {
return {
type: 'sum-equals',
fields: [itemsField, totalField],
validate: (form) => {
const items = form[itemsField] || [];
const expectedTotal = items.reduce((sum, item) => {
return sum + (item.amount || 0);
}, 0);
const actualTotal = parseFloat(form[totalField]);
const tolerance = config.tolerance || 0.01;
if (Math.abs(actualTotal - expectedTotal) > tolerance) {
return {
valid: false,
message: `${totalField} ($${actualTotal.toFixed(2)}) doesn't match sum of items ($${expectedTotal.toFixed(2)})`,
expectedValue: expectedTotal,
actualValue: actualTotal,
suggestion: `Update ${totalField} to $${expectedTotal.toFixed(2)}`
};
}
return { valid: true };
}
};
}
// Fields must match
static mustMatch(field1, field2, config = {}) {
return {
type: 'must-match',
fields: [field1, field2],
validate: (form) => {
if (form[field1] !== form[field2]) {
return {
valid: false,
message: config.message || `${field1} and ${field2} must match`
};
}
return { valid: true };
}
};
}
// Fields must be different
static mustDiffer(field1, field2, config = {}) {
return {
type: 'must-differ',
fields: [field1, field2],
validate: (form) => {
if (form[field1] === form[field2]) {
return {
valid: false,
message: config.message || `${field1} and ${field2} must be different`
};
}
return { valid: true };
}
};
}
// Conditional requirement
static conditionalRequired(triggerField, requiredField, condition) {
return {
type: 'conditional-required',
fields: [triggerField, requiredField],
validate: (form) => {
const shouldBeRequired = condition(form[triggerField]);
if (shouldBeRequired && !form[requiredField]) {
return {
valid: false,
message: `${requiredField} is required when ${triggerField} is ${form[triggerField]}`
};
}
return { valid: true };
}
};
}
// Custom relationship
static custom(fields, validateFn, message) {
return {
type: 'custom',
fields: fields,
validate: (form) => {
const valid = validateFn(form);
if (!valid) {
return {
valid: false,
message: message
};
}
return { valid: true };
}
};
}
}
2. Relationship Validator
Execute validation rules:
class RelationshipValidator {
constructor() {
this.rules = [];
this.violations = new Map();
}
addRule(rule) {
this.rules.push(rule);
}
validate(formData, options = {}) {
const violations = [];
this.rules.forEach(rule => {
// Skip if not all required fields present
if (!this.hasRequiredFields(rule.fields, formData)) {
return;
}
const result = rule.validate(formData);
if (!result.valid) {
violations.push({
type: rule.type,
fields: rule.fields,
message: result.message,
suggestion: result.suggestion,
expectedValue: result.expectedValue,
actualValue: result.actualValue,
severity: result.severity || 'error'
});
}
});
// Store violations for later reference
this.violations.clear();
violations.forEach(v => {
v.fields.forEach(field => {
if (!this.violations.has(field)) {
this.violations.set(field, []);
}
this.violations.get(field).push(v);
});
});
return violations;
}
hasRequiredFields(fields, formData) {
return fields.every(field => {
// Handle nested fields (e.g., 'items[0].amount')
return this.getNestedValue(formData, field) !== undefined;
});
}
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current?.[key];
}, obj);
}
getViolationsForField(fieldName) {
return this.violations.get(fieldName) || [];
}
clearViolations() {
this.violations.clear();
}
}
3. Visual Relationship Feedback
Show which fields are related in the error:
class RelationshipFeedback {
showViolation(violation) {
// Highlight all involved fields
violation.fields.forEach(field => {
this.highlightField(field, 'error');
});
// Draw visual connection between fields
if (violation.fields.length === 2) {
this.connectFields(violation.fields[0], violation.fields[1]);
}
// Show error message near the fields
this.showErrorMessage(violation);
}
highlightField(fieldName, type = 'error') {
const field = document.getElementById(fieldName);
const container = field?.closest('.field-container');
if (container) {
container.classList.add(`validation-${type}`);
field.setAttribute('aria-invalid', 'true');
}
}
connectFields(field1Name, field2Name) {
const field1 = document.getElementById(field1Name);
const field2 = document.getElementById(field2Name);
if (!field1 || !field2) return;
// Create SVG connector
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('field-connector');
svg.style.position = 'absolute';
svg.style.pointerEvents = 'none';
svg.style.zIndex = '1000';
// Calculate positions
const rect1 = field1.getBoundingClientRect();
const rect2 = field2.getBoundingClientRect();
const x1 = rect1.right;
const y1 = rect1.top + rect1.height / 2;
const x2 = rect2.left;
const y2 = rect2.top + rect2.height / 2;
// Draw curved line
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const midX = (x1 + x2) / 2;
path.setAttribute('d', `M ${x1},${y1} Q ${midX},${y1} ${midX},${(y1+y2)/2} T ${x2},${y2}`);
path.setAttribute('stroke', '#e74c3c');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
svg.appendChild(path);
document.body.appendChild(svg);
// Remove after delay
setTimeout(() => svg.remove(), 5000);
}
showErrorMessage(violation) {
// Find best position for error message
const primaryField = document.getElementById(violation.fields[0]);
const container = primaryField?.closest('.field-container');
if (!container) return;
// Create error element
const error = document.createElement('div');
error.className = 'cross-field-error';
error.innerHTML = `
<div class="error-icon">⚠️</div>
<div class="error-content">
<div class="error-message">${violation.message}</div>
${violation.suggestion ? `
<div class="error-suggestion">
💡 ${violation.suggestion}
</div>
` : ''}
<div class="error-fields">
Affects: ${violation.fields.join(', ')}
</div>
</div>
`;
container.appendChild(error);
}
clearViolations() {
document.querySelectorAll('.validation-error').forEach(el => {
el.classList.remove('validation-error');
});
document.querySelectorAll('.cross-field-error').forEach(el => {
el.remove();
});
document.querySelectorAll('.field-connector').forEach(el => {
el.remove();
});
}
}
4. Real-Time Relationship Checking
Validate relationships as user types:
class RealTimeRelationshipChecker {
constructor(validator, feedback) {
this.validator = validator;
this.feedback = feedback;
this.debounceTimers = new Map();
}
setupListeners() {
// Listen to all form inputs
document.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('input', (e) => {
this.handleInput(e.target.name);
});
input.addEventListener('change', (e) => {
this.handleChange(e.target.name);
});
});
}
handleInput(fieldName) {
// Debounce input validation
if (this.debounceTimers.has(fieldName)) {
clearTimeout(this.debounceTimers.get(fieldName));
}
const timer = setTimeout(() => {
this.validateField(fieldName);
}, 500); // Wait 500ms after typing stops
this.debounceTimers.set(fieldName, timer);
}
handleChange(fieldName) {
// Immediate validation on change (blur, selection)
this.validateField(fieldName);
}
validateField(fieldName) {
// Clear previous violations for this field
this.feedback.clearViolations();
// Get form data
const formData = this.getFormData();
// Validate all rules
const violations = this.validator.validate(formData);
// Show violations
violations.forEach(violation => {
this.feedback.showViolation(violation);
});
}
getFormData() {
const form = document.querySelector('form');
const data = new FormData(form);
return Object.fromEntries(data.entries());
}
}
5. Auto-Fix Suggestions
Offer to fix relationship violations:
class AutoFixEngine {
constructor(validator) {
this.validator = validator;
}
suggestFixes(violations, formData) {
const fixes = [];
violations.forEach(violation => {
const fix = this.generateFix(violation, formData);
if (fix) {
fixes.push(fix);
}
});
return fixes;
}
generateFix(violation, formData) {
switch(violation.type) {
case 'sum-equals':
return this.fixSumMismatch(violation, formData);
case 'date-range':
return this.fixDateRange(violation, formData);
case 'must-match':
return this.fixMismatch(violation, formData);
default:
return null;
}
}
fixSumMismatch(violation, formData) {
if (violation.expectedValue !== undefined) {
return {
description: `Update total to ${violation.expectedValue}`,
apply: () => {
const totalField = violation.fields.find(f => f.includes('total'));
if (totalField) {
document.getElementById(totalField).value = violation.expectedValue;
}
}
};
}
}
fixDateRange(violation, formData) {
const [startField, endField] = violation.fields;
const start = new Date(formData[startField]);
return {
description: `Set end date to one day after start date`,
apply: () => {
const newEnd = new Date(start);
newEnd.setDate(newEnd.getDate() + 1);
const endInput = document.getElementById(endField);
endInput.value = newEnd.toISOString().split('T')[0];
}
};
}
fixMismatch(violation, formData) {
const [field1, field2] = violation.fields;
return {
description: `Copy ${field1} to ${field2}`,
apply: () => {
const value = document.getElementById(field1).value;
document.getElementById(field2).value = value;
}
};
}
presentFixOptions(fixes) {
if (fixes.length === 0) return;
const modal = document.createElement('div');
modal.className = 'fix-suggestions-modal';
modal.innerHTML = `
<div class="modal-header">
<h3>Auto-Fix Suggestions</h3>
</div>
<div class="modal-body">
<p>We found some issues that can be fixed automatically:</p>
<ul class="fix-list">
${fixes.map((fix, index) => `
<li>
<button onclick="applyFix(${index})">
${fix.description}
</button>
</li>
`).join('')}
</ul>
</div>
<div class="modal-footer">
<button onclick="closeModal()">Cancel</button>
<button onclick="applyAllFixes()">Apply All</button>
</div>
`;
document.body.appendChild(modal);
}
}
Implementation Details
Complete Cross-Field Validation System
class CrossFieldValidationSystem {
constructor() {
this.validator = new RelationshipValidator();
this.feedback = new RelationshipFeedback();
this.checker = new RealTimeRelationshipChecker(this.validator, this.feedback);
this.autoFix = new AutoFixEngine(this.validator);
}
initialize(formDefinition) {
// Define all relationship rules
formDefinition.relationships?.forEach(rel => {
const rule = this.createRule(rel);
this.validator.addRule(rule);
});
// Setup real-time checking
this.checker.setupListeners();
}
createRule(relationship) {
const RuleDef = CrossFieldRuleDefinition;
switch(relationship.type) {
case 'date-range':
return RuleDef.dateRange(
relationship.startField,
relationship.endField,
relationship.config
);
case 'sum-equals':
return RuleDef.sumEquals(
relationship.itemsField,
relationship.totalField,
relationship.config
);
case 'must-match':
return RuleDef.mustMatch(
relationship.field1,
relationship.field2,
relationship.config
);
case 'must-differ':
return RuleDef.mustDiffer(
relationship.field1,
relationship.field2,
relationship.config
);
case 'custom':
return RuleDef.custom(
relationship.fields,
relationship.validate,
relationship.message
);
default:
throw new Error(`Unknown relationship type: ${relationship.type}`);
}
}
validateForm() {
const formData = this.getFormData();
const violations = this.validator.validate(formData);
if (violations.length > 0) {
// Show violations
violations.forEach(v => {
this.feedback.showViolation(v);
});
// Offer auto-fixes
const fixes = this.autoFix.suggestFixes(violations, formData);
if (fixes.length > 0) {
this.autoFix.presentFixOptions(fixes);
}
return false;
}
return true;
}
getFormData() {
const form = document.querySelector('form');
const data = new FormData(form);
return Object.fromEntries(data.entries());
}
}
// Example usage
const system = new CrossFieldValidationSystem();
system.initialize({
relationships: [
{
type: 'date-range',
startField: 'startDate',
endField: 'endDate',
config: { maxDuration: 365 }
},
{
type: 'must-match',
field1: 'password',
field2: 'confirmPassword',
config: { message: 'Passwords must match' }
},
{
type: 'must-differ',
field1: 'primaryContact',
field2: 'emergencyContact',
config: { message: 'Emergency contact must be different from primary' }
},
{
type: 'sum-equals',
itemsField: 'lineItems',
totalField: 'subtotal'
},
{
type: 'custom',
fields: ['age', 'retirementAge'],
validate: (form) => {
return parseInt(form.retirementAge) > parseInt(form.age);
},
message: 'Retirement age must be greater than current age'
}
]
});
Consequences
Benefits
Catch Relationship Errors: - Impossible combinations prevented - Business rules enforced - Data integrity maintained
Better User Feedback: - Clear which fields are involved - Visual connection between related fields - Helpful suggestions for fixes
Improved Data Quality: - Consistent relationships - No orphaned data - Validated totals
Reduced Support Burden: - Fewer "why was my submission rejected" questions - Self-explanatory error messages - Auto-fix reduces frustration
Real-Time Prevention: - Catch errors as they're made - Don't wait for submission - Faster error resolution
Liabilities
Performance Cost: - Checking relationships continuously is expensive - Complex forms can get slow - Need optimization and debouncing
Complex Error States: - Multiple violations can overwhelm - Prioritization needed - Clear presentation challenging
False Positives: - Rules may be too strict - Edge cases not handled - Need flexibility for exceptions
Implementation Complexity: - Many relationship types - Testing all combinations hard - Maintenance burden grows
User Confusion: - "Why are these fields connected?" - Need clear explanations - Visual feedback must be obvious
Domain Examples
Financial: Budget Management
// Budget allocation cross-field validation
defineBudgetRules() {
// Sum of categories must equal total budget
this.validator.addRule(
CrossFieldRuleDefinition.sumEquals('categories', 'totalBudget')
);
// No category can exceed 50% of total
this.validator.addRule(
CrossFieldRuleDefinition.custom(
['categories', 'totalBudget'],
(form) => {
const max = form.totalBudget * 0.5;
return form.categories.every(cat => cat.amount <= max);
},
'No single category can exceed 50% of total budget'
)
);
}
Healthcare: Prescription Validation
// Medication dosage cross-field validation
definePrescriptionRules() {
// Daily dose cannot exceed maximum
this.validator.addRule({
type: 'custom',
fields: ['dosageAmount', 'frequency', 'maxDailyDose'],
validate: (form) => {
const daily = form.dosageAmount * form.frequency;
return daily <= form.maxDailyDose;
},
message: 'Daily dose exceeds maximum safe dosage'
});
// Refill date must be after prescription date
this.validator.addRule(
CrossFieldRuleDefinition.dateRange('prescriptionDate', 'refillDate')
);
}
E-commerce: Order Validation
// Order total cross-field validation
defineOrderRules() {
// Subtotal = sum of line items
this.validator.addRule(
CrossFieldRuleDefinition.sumEquals('lineItems', 'subtotal')
);
// Discount cannot exceed subtotal
this.validator.addRule({
type: 'custom',
fields: ['subtotal', 'discount'],
validate: (form) => form.discount <= form.subtotal,
message: 'Discount cannot exceed subtotal'
});
// Total = subtotal - discount + tax + shipping
this.validator.addRule({
type: 'custom',
fields: ['subtotal', 'discount', 'tax', 'shipping', 'total'],
validate: (form) => {
const expected = form.subtotal - form.discount + form.tax + form.shipping;
return Math.abs(form.total - expected) < 0.01;
},
message: 'Total does not match calculation'
});
}
Related Patterns
Prerequisites: - Volume 3, Pattern 6: Domain-Aware Validation (individual field validation)
Synergies: - Volume 3, Pattern 7: Calculated Dependencies (relationships drive calculations) - Volume 3, Pattern 9: Contextual Constraints (context affects valid relationships) - Volume 3, Pattern 11: Cascading Updates (changing one field affects others) - Volume 3, Pattern 13: Conditional Requirements (requirements affect validation)
Conflicts: - Extreme performance requirements (continuous checking too expensive) - Offline forms (can't do real-time validation)
Alternatives: - Server-side only (validate on submit) - Prevent invalid entry (disable conflicting options) - Guided workflows (step-by-step prevents conflicts)
Known Uses
Tax Software: Deductions can't exceed income, dependent ages, marriage date validation
Booking Systems: Check-out after check-in, room availability vs capacity
Financial Applications: Transaction amounts, account balances, payment totals
Healthcare Forms: Medication interactions, dosage calculations, appointment scheduling
E-commerce: Cart totals, shipping calculations, inventory availability
Project Management: Task dependencies, resource allocation, timeline validation
HR Systems: Salary ranges, tenure calculations, benefit eligibility
Further Reading
Academic Foundations
- Integrity Constraints: Date, C.J. (2004). An Introduction to Database Systems (8th ed.). Addison-Wesley. ISBN: 978-0321197849 - Chapter on constraints
- Form Validation: Wroblewski, L. (2008). Web Form Design: Filling in the Blanks. Rosenfeld Media. ISBN: 978-1933820241
- Error Prevention: Norman, D.A. (2013). The Design of Everyday Things (Revised ed.). Basic Books. ISBN: 978-0465050659
Practical Implementation
- Yup Test Method: https://github.com/jquense/yup#mixedtestname-string-message-string--function-test-function-schema - Custom cross-field tests
- React Hook Form Custom Validation: https://react-hook-form.com/advanced-usage#CustomValidation
- Formik Validate Function: https://formik.org/docs/guides/validation#validate - Form-level validation
- Zod Refine: https://zod.dev/?id=refine - Cross-field refinement
- Vest Suite Tests: https://vestjs.dev/docs/writing_tests/test - Multi-field validation
Standards & Best Practices
- HTML5 Constraint Validation: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Constraint_validation - Built-in validation
- WCAG Error Identification: https://www.w3.org/WAI/WCAG21/Understanding/error-identification.html - Accessible errors
- ISO 9241-110: Dialogue principles (error management)
Related Trilogy Patterns
- Pattern 11: Validation Rules - Cross-field as advanced validation
- Pattern 12: Calculated Dependencies - Calculations inform validation
- Pattern 16: Cascading Updates - Updates trigger re-validation
- Pattern 18: Conditional Requirements - Validate conditional fields together
- Pattern 20: Real-Time Feedback - When to show cross-field errors
- Volume 2, Chapter 4: Interaction Outcome Classification - Validation quality metrics
Tools & Libraries
- validator.js: https://github.com/validatorjs/validator.js - String validation utilities
- joi: https://joi.dev/ - Schema validation with cross-field tests
- class-validator: https://github.com/typestack/class-validator - Decorator-based validation
- ajv: https://ajv.js.org/ - JSON Schema validator with custom keywords
Implementation Examples
- Date Range Validation: https://css-tricks.com/form-validation-part-3-a-calendar-picker/ - Start/end date patterns
- Password Confirmation Pattern: https://www.smashingmagazine.com/2022/03/design-better-password-confirmation/
- Cross-Field Error Display: https://www.nngroup.com/articles/error-message-guidelines/ - Where to show errors