Validation

The validation relies on the @banquette/validation package. If you are not familiar with it, consider reading this part of the documentation first.

If you're still here, we will consider you're familiar with the concept of validator in Banquette.

Setting a validator

Every type of component can have one (and only one) validator. There is no need for multiple validators because by nesting them you can create a validation tree as complex as you want.

Because there are many ways of creating a form, there are as many ways of setting a validator.

On manual creation

Every type of component can take a ValidatorInterface as second argument of its constructor:

import { NotEmpty, Min, And } from '@banquette/validation';

const control    = new FormControl('value', NotEmpty());
const formObject = new FormObject({/* children */}, NotEmpty());
const formArray  = new FormArray([/* children */], And(NotEmpty(), Min(5)));

From the FormFactory

Because of the way the FormFactory define the form, there is no easy way of setting a validator:

const form = FormFactory.Create({
    name: 'John',
    email: 'john@domain.tld'
});

You can do it by adding a prefix (and/or a suffix) to the name of the component. By default, only a suffix is expected, with the value $:

So to set a validator to the name control, we can do:

const form = FormFactory.Create({
    name$: ['John', NotEmpty()],
    email: 'john@domain.tld'
});

The suffix tells the factory that we will define the constructor arguments instead of just the value. So it expects an array with multiple information inside: the value and a validator.

NOTE

If our example, if you remove the suffix, the factory will create a FormArray with two FormControl inside... not what we want here.

Change the prefix

The prefix is configurable using the Configuration:

Injector.Get(ConfigurationService).modify(FormConfigurationSymbol, {
    factory: {
        extendedNamePrefix: null, // Any prefix you want or `null` for no prefix
        extendedNameSuffix: '$'   // Any suffix you want or `null` for no prefix
    }
});

TIP

If no prefix nor suffix is defined, the functionality is disabled.


From a model

If you use a model to create your form, you can use the @Assert() decorator to set a validator:

class User {
    @Assert(NotBlank())
    @Form()
    public name: string = 'John';
    
    @Form()
    public email: string = 'john@domain.tld';
}

The validator will be automatically applied when you transform the model into a form.


On an existing component

There is a setValidator method on every component:

control.setValidator(NotEmpty());

On a collection

Just like above for a single component, a collection have a setValidator method:

collection.setValidator(NotEmpty());

This will call setValidator on each component of the collection.

TIP

You could set a NotEmpty validator to all components of a form in a single line by doing:

form.getByPattern('**').setValidator(NotEmpty());

Validate a form

You can call the method validate() on any component of your form.
This will validate the component on which you called the method, as well as all its children if it's a group:

const form = FormFactory.Create({
    name$: ['', NotEmpty()],
    category: {
        name$: ['cat', And(NotEmpty(), Min(10))]
    }
});

form.validate(); // return false
//
// Will find two violations:
//  - /name (NotEmpty)
//  - /category/name (Min)
//

TIP

You may have noted that, unlike a normal validator, the validate() method returns a boolean.

This is by design. It would create confusion to expose the ValidationResult outside of the form for multiple reasons relative to the inner working of the form.

We'll talk more about error handling later in the doc.


Implicit validation

Calling validate() manually is an explicit validation. It will always run the moment you ask for it, no matter the state of the form.

But in most cases, you will want the form to "validate itself" on the fly, while it changes. This is called implicit validation.

It works by defining one or multiple triggers for each component of the form. When the form mutates it will check the corresponding trigger and run a validation if needed.

These "triggers" are called a validation strategy.

You set it like this:

control.setValidationStrategy(ValidationStrategy.OnChange);

Here is the list of available strategies:

Strategy Description
None The component will never trigger a validation by itself.
OnChange The component will trigger a validation each time the value changes.
OnFocus The component will trigger a validation when the focus is gained.
OnBlur The component will trigger a validation when the focus is lost.
Inherit The component will use the validation strategy of its parent.
If no parent, OnChange is used.

TIP

The default strategy is Inherit, so you can set the strategy of the whole form simply by calling setValidationStrategy() on the root component.


Using a collection

If you want to cherry-pick some components on which you want a different strategy, you can use getByPattern and call setValidationStrategy on the resulting collection:

import { ValidationStrategy } from '@banquette/form';

// Make the whole form trigger a validation on change
form.setValidationStrategy(ValidationStrategy.OnChange);

// But all controls named "email" or "username", at any depth in the form will only validate on blur.
form.getByPattern('**/[email,username]').setValidationStrategy(ValidationStrategy.OnBlur);

Multiple strategies

If you want a component to validate on change and on blur, you can combine strategies:

control.setValidationStrategy(ValidationStrategy.OnChange | ValidationStrategy.OnBlur);

Error management

If errors are found when validating a component, they will be stored in the component as FormError objects.

These objects are very similar to the Violation objects of the validation package with the exception that they each contain the path of the component from which the error originates.

Click here to see the detail of the `FormError` object.
class FormError {
    /**
     * The path of the component to which the error belongs.
     */
    public readonly path: string;

    /**
     * The type of error, can be any string.
     */
    public readonly type: string;

    /**
     * An optional error description.
     */
    public readonly message: string | null;
}

Get errors

You have then three ways of accessing the errors:

Errors of the component itself

The list of errors found on the component itself:

const errors: FormError[] = form.errors;

/**
 * Example:
 * 
 * [
 *      (FormError) {path: '/', type: 'not-empty', ...},
 *      (FormError) {path: '/', type: 'pattern', ...}
 * ]
 */

Errors of the component + its children as an array

Flattened array of errors found on the component itself and all its children (recursively):

const errors: FormError[] = form.errorsDeep;

/**
 * Example:
 *
 * [
 *      (FormError) {path: '/', type: 'not-empty', ...},
 *      (FormError) {path: '/name', type: 'not-empty', ...},
 *      (FormError) {path: '/category/name', type: 'not-empty', ...}
 * ]
 */

Errors of the component + its children as a map

A key/value pair object containing the path of each children component as index and the array of their errors as value:

const errors: Record<string, FormError[]> = form.errorsDeepMap;

/**
 * Example:
 *
 * {
 *      '/':              [(FormError) {path: '/', type: 'not-empty', ...}],
 *      '/name':          [(FormError) {path: '/name', type: 'not-empty', ...}],
 *      '/category/name': [(FormError) {path: '/category/name', type: 'not-empty', ...}]
 * }
 */

Add error

Every type of component have a addError method you can use to add an error manually:

control.addError('type', 'optional message');

Clear errors

Every type of component have a clearErrors method you can use to remove all errors.

component.clearErrors();

WARNING

This only remove the errors of the component itself, not its children.

If you want to clear the errors recursively, use the clearErrorsDeep method:

component.clearErrorsDeep();