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)));
FormFactory
From the 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();