Skip to main content

Custom Validators

Mat Dynamic Form supports Angular’s full validation system, including built-in validators, custom synchronous validators, and asynchronous validators.

Angular Built-in Validators

You can use any validator from @angular/forms when creating nodes.
import { Validators } from '@angular/forms';
import { Input, TextArea } from 'mat-dynamic-form';

// Single validator
new Input('email', 'Email').apply({
  validator: Validators.email
})

// Multiple validators
new Input('username', 'Username').apply({
  validator: [Validators.required, Validators.minLength(3), Validators.maxLength(20)]
})

// Required with character limit
new TextArea('comments', 'Comments').apply({
  validator: [Validators.required, Validators.maxLength(100)],
  maxCharCount: 100
})
See app.component.ts:118-119 for examples.

Common Validators

ValidatorDescriptionExample
Validators.requiredField must have a valuevalidator: Validators.required
Validators.requiredTrueCheckbox must be checkedvalidator: Validators.requiredTrue
Validators.emailMust be valid email formatvalidator: Validators.email
Validators.min(n)Minimum numeric valuevalidator: Validators.min(0)
Validators.max(n)Maximum numeric valuevalidator: Validators.max(100)
Validators.minLength(n)Minimum string lengthvalidator: Validators.minLength(3)
Validators.maxLength(n)Maximum string lengthvalidator: Validators.maxLength(100)
Validators.pattern(regex)Must match regex patternvalidator: Validators.pattern(/^[0-9]+$/)

Global Validators

Apply validators to all form fields using globalValidators:
import { FormStructure } from 'mat-dynamic-form';
import { Validators } from '@angular/forms';

const formStructure = new FormStructure();
formStructure.globalValidators = Validators.required;

// All nodes will now be required by default
formStructure.nodes = [
  new Input('name', 'Name'),
  new Input('email', 'Email').apply({
    validator: Validators.email // Combined with global required
  })
];
See README.md:95 and FormStructure.ts:38 for implementation.
Global validators are combined with node-specific validators using Validators.compose(). Both sets of validation rules must pass for the field to be valid.

Dynamic Validation

Add or modify validators at runtime using FormStructure methods.

addValidators()

Adds validators to an existing control without overwriting existing validators.
addValidators(id: string, validators: ValidatorFn | ValidatorFn[]): void
Example:
// Add additional validation to existing field
formStructure.addValidators('email', Validators.email);

// Add multiple validators
formStructure.addValidators('password', [
  Validators.minLength(8),
  Validators.pattern(/^(?=.*[A-Z])(?=.*[0-9])/)
]);
See FormStructure.ts:332 for implementation.

setValidator()

Replaces all existing validators with new ones.
setValidator(id: string, validators: ValidatorFn | ValidatorFn[]): void
Example:
// Replace validation rules entirely
formStructure.setValidator('phone', [
  Validators.required,
  Validators.pattern(/^\+?[1-9]\d{1,14}$/)
]);

// Remove all validators by passing null
formStructure.setValidator('optional', null);
See FormStructure.ts:356 for implementation.
Use addValidators() when you want to add additional rules while preserving existing validation. Use setValidator() when you need to completely change the validation behavior.

Custom Validator Functions

Create your own validation logic using Angular’s validator pattern.
1

Create the validator function

A validator is a function that returns null for valid values or an error object for invalid values:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function phoneValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    
    if (!value) {
      return null; // Don't validate empty values (use Validators.required for that)
    }
    
    const phoneRegex = /^\+?[1-9]\d{1,14}$/;
    const valid = phoneRegex.test(value);
    
    return valid ? null : { invalidPhone: { value } };
  };
}
2

Apply to a node

Use your custom validator like any built-in validator:
new Input('phone', 'Phone Number').apply({
  validator: [Validators.required, phoneValidator()]
})
3

Display error messages

Access validation errors in your template or code:
const phoneControl = formStructure.getControlById('phone');

if (phoneControl.hasError('invalidPhone')) {
  console.log('Invalid phone number format');
}

Password Matching Validator

Example of a validator that compares two fields:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function passwordMatchValidator(passwordField: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const password = control.parent?.get(passwordField)?.value;
    const confirmPassword = control.value;
    
    if (!password || !confirmPassword) {
      return null;
    }
    
    return password === confirmPassword ? null : { passwordMismatch: true };
  };
}

// Usage
new InputPassword('password', 'Password'),
new InputPassword('confirmPassword', 'Confirm Password').apply({
  validator: passwordMatchValidator('password')
})

Async Validators

Validate against remote APIs or perform time-consuming validation asynchronously.

addAsyncValidators()

addAsyncValidators(id: string, validators: AsyncValidatorFn | AsyncValidatorFn[]): void

setAsyncValidator()

setAsyncValidator(id: string, validators: AsyncValidatorFn | AsyncValidatorFn[]): void
See FormStructure.ts:344 and FormStructure.ts:370 for implementations.
1

Create async validator

Return an Observable or Promise that resolves to validation errors or null:
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, debounceTime, switchMap, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

export function usernameAvailableValidator(http: HttpClient): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    
    return control.valueChanges.pipe(
      debounceTime(500),
      switchMap(username => 
        http.get(`/api/check-username?username=${username}`)
      ),
      map((response: any) => 
        response.available ? null : { usernameTaken: true }
      ),
      catchError(() => of(null))
    );
  };
}
2

Apply to node

Set the async validator when creating the node:
new Input('username', 'Username').apply({
  validator: Validators.required,
  asyncValidator: usernameAvailableValidator(this.http)
})
3

Handle pending state

Async validators set the control status to PENDING while validating:
const usernameControl = formStructure.getControlById('username');

if (usernameControl.status === 'PENDING') {
  console.log('Checking availability...');
}
Async validators only run after all synchronous validators pass. Make sure to include basic synchronous validation (like Validators.required) before expensive async checks.

Validation on Specific Nodes

Different node types support validation in different ways:
// Checkbox - require it to be checked
new Checkbox('agreement', 'I agree to terms').apply({
  validator: Validators.requiredTrue
})

// Number input - restrict range
new InputNumber('age', 'Age').apply({
  validator: [Validators.required, Validators.min(18), Validators.max(100)]
})

// File input - custom file type/size validation
new InputFile('document', 'Upload Document').apply({
  accept: ['pdf', 'docx'],
  maxSize: 5000, // KB
  validator: Validators.required
})

// Dropdown - ensure selection is made
new Dropdown('country', 'Country', countryOptions).apply({
  validator: Validators.required
})

Error Messages

Set custom error messages for validation failures:
new Input('email', 'Email Address').apply({
  validator: [Validators.required, Validators.email],
  errorMessage: 'Please enter a valid email address'
})
See app.component.ts:32 for example.
The errorMessage property displays when any validator fails. For more granular control, you may need to implement custom error handling in your component.

Validation State Methods

Check form validation status:
// Check if entire form is valid
if (formStructure.isValid()) {
  const values = formStructure.getValue();
  // Submit form
}

// Check if form is invalid
if (formStructure.isInvalid()) {
  console.log('Please fix validation errors');
}

// Get control to check specific field
const emailControl = formStructure.getControlById('email');
if (emailControl.invalid && emailControl.touched) {
  console.log('Email is invalid');
}
See FormStructure.ts:197-224 for validation methods.

Validate Form on Button Click

Validate the entire form when a button is clicked:
new Button('save', 'Save', {
  onEvent: (param) => {
    if (param.structure.isValid()) {
      const values = param.structure.getValue();
      console.log('Form is valid:', values);
    } else {
      console.log('Form has errors');
    }
  },
  style: 'primary'
}).apply({
  validateForm: true, // Form will be validated before button click
  icon: 'save'
})
See app.component.ts:158-161 for example.
When validateForm: true is set on a Button, the form must be valid for the button’s onEvent to execute. This is useful for submit buttons.

Best Practices

  • Use built-in validators when possible - they’re well-tested and performant
  • Combine global and local validators for DRY code
  • Debounce async validators to avoid excessive API calls
  • Provide clear error messages to help users fix issues
  • Validate on blur for better UX than validating on every keystroke
  • Test edge cases like empty strings, null values, and whitespace

Build docs developers (and LLMs) love