lion/packages/validate/docs/tutorials/CustomValidatorsTutorial.md
2019-07-19 14:58:19 +02:00

5.4 KiB

Custom Validator Tutorial

Validators consist of an array in the format of [<function>, <?params>, <?opts>]. They are implemented via pure functions and can be coupled to localized messages. They should be used in conjunction with a Form Control that implements the ValidateMixin.

This tutorial will show you how to build your own custom IBAN validator, including a validator factory function that makes it easy to apply and share it.

Implement the validation logic

It's a good habit to make your validators compatible with as many locales and languages as possible. For simplicity, this tutorial will only support Dutch and US zip codes.

As a best practice, create your validator in a new file from which it can be exported for eventual reuse.

We will start with this simple function body:

export function validateIban() {
  return true;
}

It return a boolean, which is true if the value is valid.

Which value? The modelValue like this:

export function validateIban(modelValue) {
  return /\d{5}([ \-]\d{4})?$/.test(modelValue.trim());
}

The above function validates IBANs for the United States ('us').

Suppose we want to support 'nl' IBANs as well. Since our validator is a pure function, we need a parameter for it:

export function validateIban(modelValue, { country } = {}) {
  const regexpes = {
    us: /\d{5}([ \-]\d{4})?$/,
    nl: /^[1-9]{1}[0-9]{3} ?[a-zA-Z]{2}$/,
  };

  return regexpes[country || 'us'].test(modelValue.trim());
}

Creating a validate function

As can be read in validate, a validator applied to .errorValidators expects an array with a validate function, a parameters object and an additional configuration object.

It expects a function with such an interface:

export function isIban(...args) {
  return {
    'my-app-isIban': validateIban(...args),
  };
}

my-app-isIban is the unique id of the validator. The naming convention for this unique id will be explained later in this document.

Creating a reusable factory function

But this is way easier to create a factory function which will automatically create an array for .errorValidators. We recommend using this approach and from now on when saying a validator function we mean this one:

export const isIbanValidator = (...factoryParams) => [
  (...params) => ({ 'my-app-isIban': isIban(...params) }),
  ...factoryParams,
];

Here the naming convention is validator name + Validator. Thus, isIbanValidator is your validator factory function.

Adding an error message

Now, we want to add an error message. For this, we need to have a bit more knowledge about how the ValidateMixin handles translation resources.

As can be read in validate, the ValidateMixin considers all namespaces configured via get loadNamespaces. By default, this contains at least the lion-validate namespace which is added by the ValidateMixin. On top of this, for every namespace found, it adds an extra {namespace}-{validatorUniqueId} namespace. Let's assume we apply our validator on a regular <lion-input>. If our validatorUniqueId was isIban, that would mean on validation these two namespaces are considered, and in this order:

  • lion-validate+isIban
  • lion-validate

One should be aware that localize namespaces are defined in a global scope. Therefore the approach above would only work fine when the IBAN validator would be part of the core code base (validate).

As long as validators are part of an application, we need to avoid global namespace clashes. Therefore, we recommend to prefix the application name, like this: my-app-isIban.

The resulting lion-validate+my-app-isIban namespace is now guaranteed to be unique.

In order for the localization data to be found, the translation files need to be added to the manager of localize. The recommended way to do this (inside your validators.js file):

localize.loadNamespace({
  'lion-validate+my-app-isIban': locale => {
    return import(`./translations/${locale}.js`);
  },
});

In (for instance) ./translations/en.js, we will see:

export default {
  error: {
    'my-app-isIban':
      'Please enter a(n) valid {validatorParams.country} IBAN number for {fieldName}.',
  },
  warning: {
    'my-app-isIban':
      'Please enter a(n) valid {validatorParams.country} IBAN number for {fieldName}.',
  },
};

validatorParams is the second argument passed to the validator. In this case this is the object { country: '%value%' } where %value% is the one passed by an app developer.

Conclusion

We are now good to go to reuse our validator in external contexts. After importing it, using the validator would be as easy as this:

<validatable-el .errorValidators="${[isIbanValidator({ country: 'nl' })]}"></validatable-el>