Skip to content

proposal

Braulio Diez edited this page Aug 21, 2019 · 13 revisions

draft @lemoncode/form-validator proposal

Intro

Let's assume we want to validate a form called "MyForm"

Defining validation schema

Covering cases here:

-A field could include a validator that doesn't need params (just pass a function).

-If we need to do extra stuff we can use an object, use cases:

  • Inform here custom args (e.g. a maxLenght validator a custom args that indicates the max length).
  • Inform custom validation error for this args
  • We could combine both.
const myFormValidationSchema = {
  firstname: [
    Validators.required,
    { validator: Validators.minLenght, customArgs: { minLength: 5 } }
  ],
  email: [{ validator: Validators.regEX, errorMessage: "Not a valid mail" }],
  password: [
    {
      validator: Validators.passwordStrenght,
      customArgs: { strenght: strength.strong },
      errorMessage:
        "Your password is weak please ensure lenght is at least 8 characters and includes..."
    }
  ]
};

const myFormValidation = createFormValidation(myFormValidationSchema);

About validators you can combine both sync and async, the engine will be able to autodetect which approach are you using.

Definitions of a Validation Schema:

export class ValidationResult {
  key?: string;
  type: string;
  succeeded: boolean;
  errorMessage: string;
}

type ValidationResultSyncAsync = ValidationResult | Promise<ValidationResult>;

interface FieldValidationFunction {
  (value: any, vm: any, customParams: any): ValidationResultSyncAsync;
}

interface FieldValidationFull {
  validator: FieldValidationFunction;
  customArgs?: object;
  errorMessage : (string | string[]),
}

export type FieldValidation = (FieldValidationFunction | FielValidationFull);

export interface RecordValidationResult {
  succeeded: boolean;
  fieldErrors: { [key: string]: ValidationResult };
  formGlobalErrors: Array<ValidationResult>;
}

export interface ValidationSchema {
  global?: RecordValidationFunction[];
  fields?: { [key: string]: FieldValidation[] }
}

Validate single field

Proposal:

validateField(fieldId : string, value : any, viewModel? : any) : Promise<ValidationResult>

Samples:

Simple validation

myFormValidation.validateField("firstname", "John");

Passing whole record (viewmodel)

myFormValidation.validateField('creditCard', '888-333-222-111', vm);

Example where passing a record (viewmodel) is mandatory: when you a given field validation is dependant on another viewModel field.

Validate Record

api

validateRecord(record : any) : Promise<FormValidationResult>

Record === ViewModel

Example

const viewModel = { login: "[email protected]", password: "jdoe3981" };
myFormValidation
  .validateForm(viewModel)
  .then(validationResult => {
    console.log(validationResult.success); // true
    console.log(validationResult.formGlobalErrors); // []
    console.log(validationResult.fieldErrors);
  })
  .catch(error => {
    // handle unexpected errors
  });

Creating validators

A field validator:

Sync

interface ValidationResult {
  errorMessage : string;
  type : string;
  succeeded : boolean;
}

export let errorMessage = 'Please review field sintax'

export const validateRegEx(fieldName : string, value : string, customArgs? : object, record?: object, customErrorMessage? : (string | string[])) : FieldValidationResult;

A global validation:

interface ValidationResult {
  errorMessage : string;
  type : string;
  succeeded : boolean;
}

export const setErrorMessage = (customErroMessage : string) => 
  errorMessage = customErroMessage;

let errorMessage = 'Minimum order is 20 €';

export const validateTotalShoppingCart(viewModel : string, customErrorMessage?: (string | string[])) : FormValidationResult;

Async

How to create asynchrounous validators (based on promises beware for old browsers to have the corresponding promise polyfill):

sintax

export const myAsyncValidator = (fieldName : string, 
                                 value : string, 
                                 customArgs? : object, 
                                 record?: object, 
                                 customErrorMessage? : (string | string[])
                                 ) : Promise<FieldValidationResult>

Example

export let errorMessage = `The username already exists`

export const userExistsOnGitHubValidator = (value, vm, customParams) {  
  const validationResult = new FieldValidationResult();
  validationResult.type = 'GITHUB_USER_EXISTS';

  return new Promise(resolve => {
    fetch(`https://api.github.com/users/${value}`)
      .then(result => {
        // Status 200, meaning user exists, so the given user is not valid
        validationResult.isValid = false;
        validationResult.errorMessage = errorMessage;
        resolve(validationResult);
      })
      .catch(error => {
        if(error.status === 404) {
          // User does not exists, so the given user is valid
          validationResult.isValid = true;
          validationResult.errorMessage = '';
          resolve(validationResult);
        } else {
          // Unexpected error
          reject(error);
        }
      });
  });
}

Internationalization:

At i18n Level using type

On approach to handle internationalization is to move it away from the form validation library, each field validator will return the following structure:

interface ValidationResult {
  errorMessage : string;
  type : string;
  succeeded : boolean;
}

The TYPE field contains the name of validator that failed we can use it to map it to an error message.

Pros of this approach:

  • Single responsibility principle: the validation library just validates forms, an external helper takes care of mapping type to the proper string.

Limitations:

  • Cannot handle validator that generate more than on error message.
  • Need additional plumbing to be coded to handle the mapping between type and translation.

At validator level

Validators expose a setErrorMessage function that allow you to override default error message:

By doing this you setup a given errorMessage that will be consistent in any validation form using this validator.

You can setup this for instance at application startup.

Pros of this approach:

  • You can just setup all the validators message definition in a single entry point.
  • You can treat single string error messsage and multiple ones.

Limitations:

  • If you need to override this message for a given form read the section at validation schema level.

At validationSchema Level

Sometimes you need to customize the error message just for a single use case on a given form, e.g. a RegEx validator, you want to display for a given form field "Invalid credit card", and in another case "Not a valid DUNS number", how can you achieve this?

const myFormValidationSchema = {
  creditCard: [
    { validator: Validators.regEx, customArgs: { regEx: '\d{3}-\d{3}-\d{3}-d{3}', errorMessage='Not a valid credit card' } }
  ],
  duns: [{ validator: Validators.regEX, customArgs: { regEx: '\d{2}-\d{3}-\d{4}'},errorMessage: "Not a valid duns number" }],
};

const myFormValidation = createFormValidation(myFormValidationSchema);

Open points

This is a validation schema library, out of scope checking form state, validations triggers (onBlur, onFocus), but:

  • We have to check how to integate react final forms on scenarios like: i want to trigger this validation only on Blur.

How does React Final Form solves this?

  • You can setup this at field level, a field has a flag named validationOnBlur that triggers all that field validations on blur.

This solution can cobver 90% of the cases and is easy for us (in lc-form-validation we have a flag to control single validations at blur / onChange / or other custom events but probably we shouldn't include this on the base library).