TL;DR
While building the docs for@myndpm/dyn-formsat mynd.dev, we introduced support for several custom function types, including Validators, AsyncValidators, Matchers, Conditions, and others.
Validation sits at the heart of any form, and we spent time designing a clean approach for Validators and AsyncValidators. After exploring the options, we settled on the most declarative pattern we could find:
createMatConfig('INPUT', {
name: 'quantity',
validators: ['required', ['min', 1] ],
asyncValidators: ['myAsyncValidator'],
Angular Validators
Angular ships with a set of built-in Validators that we typically invoke directly within Reactive Forms. Some, such as Validators.required, are plain Validator Functions (ValidatorFn), while others are Validator Factories ((args) => ValidatorFn) that generate a validator based on a parameter, like Validators.minLength(4).
A Validator Function is defined as:
(control: AbstractControl) => ValidationErrors | null
It receives the control under validation, returning null for a valid value, or an error object shaped as { [error: string]: any }.
Validator Factories are higher-order functions that produce a Validator Function from given parameters:
function minLength(minLength: number): ValidatorFn {
return (control: AbstractControl) => {
return (control.value && control.value.length < minLength)
? { minLength: true } // invalid
: null; // valid
}
}
This pattern offers a clean way to parametrize functions, so we applied it to the provisioning of Validators and other handlers, using an id plus a factory fn:
export interface DynControlValidator {
id: string;
fn: (...args: any[]) => ValidatorFn;
}
The id serves as the string we reference in the Configuration Object. Out of the box, @myndpm/dyn-forms includes the standard Angular Validators under their familiar names: required, requiredTrue, email, pattern, minLength, maxLength, min, and max.
To use them in the Config Object, the notation looks like this:
// without parameters
validators: ['required'],
// with parameters as array
validators: ['required', ['min', 1] ],
// with parameters as object
validators: { required: null, minLength: 4 },
// with an inline ValidatorFn or ValidatorFn factory
validators: [myValidatorFn, myValidatorFactory(args)],
Supporting multiple notation styles comes at little cost, and it accommodates different systems or personal preferences.
Custom Validators
As noted, providing our ValidatorFn Factory involves just an id and a fn. In our module setup, we can register custom validators like this:
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { DynFormsModule } from '@myndpm/dyn-forms';
import { DynControlValidator } from '@myndpm/dyn-forms/core';
const validators: DynControlValidator[] = [
{
id: 'email',
fn: (): ValidatorFn => {
return (control: AbstractControl) => {
// implement my validator
// to return { email: true } | null;
}
}
}
];
@NgModule({
imports: [
DynFormsModule.forFeature({ validators, priority: 100 });
Notice the priority field, which lets us override the default validators (those carry a weight of 0); we'll explore priorities more deeply in a future post.
AsyncValidators
Async validators follow the same logic. Register your fn with an id, then reference it in the Config Object:
createMatConfig('INPUT', {
name: 'quantity',
validators: ['required'],
asyncValidators: ['myAsyncValidatorId'],
When your AsyncValidator factory needs parameters, use this form:
// single argument which can be an object
asyncValidators: [['myAsyncValidatorId', args]],
// your factory will receive fn(args)
// multiple arguments in array to be destructured
asyncValidators: [['myAsyncValidatorId', [arg1, arg2]]],
// your factory will receive fn(arg1, arg2)
Custom Handlers
This notation extends to other function types we need in Dynamic Forms. Alongside Validators and AsyncValidators, we support Matchers and Conditions for controlling form behavior under specific scenarios, plus ParamFns to inject functions into DynControls' parameters.
We'll dive into conditional logic in the next installment.
In the meantime, what are your thoughts on this notation?
// PS. We're hiring!
