1.1.3 • Published 5 years ago

validator-creator v1.1.3

Weekly downloads
12
License
ISC
Repository
github
Last release
5 years ago

Validator creator

Library to create client-side validation functions.

Features:

  • multiple rules per field
  • asynchronous rules with built-in concurrency prevention
  • BYO rules and display logic

Install

npm install validator-creator

Example usage

import { createRule, createValidator } from "validator-creator";
const filled = createRule("filled", value => value.length > 0, "Required");
const rules = {
  field1: [filled],
  field2: [filled]
};
const validate = createValidator(rules);
const change = {
  field1: ""
};
validate(change).then(([results]) => {
  // --> results: [{ type: 'filled', prop: 'field1', payload: 'Required' }]
});

Rules

A "rule" is a function that validates a field in a "change object" and returns a "result object".

/**
 * Example rule - say the magic word!
 * @param  {object} change - The change being validated
 * @param  {string} prop - The field to apply the rule to
 * @return {object} - A result object with shape { type, prop } that identifies
 *     the rule and the field to associate it with
 */
function exampleRule(change, prop) {
  const type = "example";
  if (change[prop] === "please") {
    // pass: the magic word was provided
    return null;
  }
  // fail
  return { type, prop };
}

The createRule function can be used to help writing simple rules more simply.

// Example rule - say the magic word!
const exampleRule = createRule("example", value => value === "please");

A rule can provide a payload containing additional data related to the rule.

// the payload can be any value
createRule("example", value => false, { text: "Example payload text" });

The payload can be a callback function that returns a payload. The callback will be passed the { type, prop } object and the data passed to validate().

const example = createRule(
  "example",
  value => false,
  ({ type, prop }, data) => `${type} payload for ${prop} with value '${data[prop]}'`
);
const validate = createValidator({ field1: [example] });
validate({ field1: "test" }).then(([result]) => {
  // --> result: { type: "example", prop: "field1", payload: "example payload for field1 with value 'test'" }
});

Rule collections

A rule collection assigns a list of rules to fields. These are passed to the createValidator function.

const rules = {
  field1: [filled, exampleRule],
  field2: [filled, email],
  field3: [number]
};

The validator will apply the rules for each field in the order specified, stopping at the first rule that returns a non-null response.

Asynchronous rules

An asynchronous rule is called once by the validator even if multiple fields need to apply the rule.

The rule should resolve to a list of result objects with shape { type, prop }.

/**
 * Checks whether the field has value 'async'
 */
async function asyncRule(change) {
  const type = "async";
  return Object.keys(change)
    .filter(prop => change[prop] !== "async")
    .map(prop => ({ type, prop }));
}
const rules = {
  field1: [asyncRule],
  field2: [asyncRule]
};
const validate = createValidator(rules);
const change = { field1: "abc", field2: "xyz" };
validate(change).then(([result]) => {
  // --> result: [ {type: "async", prop: "field1" }, {type: "async", prop: "field2" }]
});

Asynchronous rules can be mixed with synchronous rules. In the next example, the 'email' field needs to pass three rules:

  • filled -- it cannot be a blank value
  • email -- it must conform to an email address format
  • network -- the change is passed to a server-side validator (async)
const rules = {
  email: [filled, email, network]
};

Interrupting validation

A common scenario is to validate a change from within an event handler.

async function handleChange(event) {
  const {
    target: { name, value }
  } = event;
  const change = { [name]: value };
  // ... update state with the change ...
  const [results] = await validate(change);
  // ... translate results into validation messages ...
}

It's desirable to not have concurrent network requests build up if lots of change events occur in rapid succession. The validator will avoid this by blocking and discarding stale results.

The application should catch ValidatorError exceptions generated by the interrupted or discarded calls.

try {
  const [results, mergedChange] = await validate(change);
  // ... translate results into validation messages ...
  // mergedChange will have the changes of interrupted calls to validate()
} catch (error) {
  if (error instanceof ValidatorError) {
    const { validatorErrorPayload: results } = error;
    // ... handle stale results ...
  }
}

The following timeline shows how this behaves.

   1  2  3  4  5
A: *==------!     interrupted
B:    ***!        discarded
C:       ****==>  completed
  • Time 1 -- Event A occurs. validate(A) starts
  • Time 2 -- Event B occurs. validate(B) is blocked; A is marked as interrupted
  • Time 3 -- Event C occurs. validate(C) is blocked. B is rejected with a discarded error
  • Time 4 -- validate(A) is rejected with an interrupted error. validate(C) starts
  • Time 5 -- validate(C) resolves

At time 4, the validate(C) call is actually validate({...A, ...B, ...C}) so no changes are ignored.

Example: client-side generated validation text

Our "server" in this example is simulated by a module that exposes an async function, validate(change), that returns a list of validation results containing { type, prop } objects. In real life, this module would be sending the change to an API endpoint for server-side validation.

// server.js
import { createRule, createValidator } from "validator-creator";
const filled = createRule("filled", value => value.length > 0);
const rules = {
  field1: [filled]
};
const serverValidate = createValidator(rules);
export const validate = change =>
  serverValidate(change).then(([result]) => result);

On the "client" an async rule name server is created which will send the change to the server module's validate() function. Results from the server are augmented with a payload.

In this case the payload is a string containing the type and prop values of the result. In real life you would generate an appropriate message to display to the user based on the rule type.

Finally we transform the list of results into a "messages" object with the field name as key and the payload as value.

import {
  createAsyncRule,
  createValidator,
  getPayload
} from "validator-creator";
import * as Server from "./server";
const server = createAsyncRule(
  change => Server.validate(change),
  ({ type, prop }) => `${type}, ${prop}`
);
const rules = {
  field1: [server]
};
const validate = createValidator(rules);
const change = {
  field1: ""
};
validate(change)
  .then(getPayload)
  .then(messages => {
    // --> messages: { field1: "filled, field1" }
  });

Example: "max length" rule

This example demonstrates a "rule creator" pattern. This allows having rules that take arguments.

import { createRule, createValidator, getPayload } from "validator-creator";
const maxLength = length =>
  createRule(
    "maxLength",
    value => value.length <= length,
    `Maximum length is ${length} characters`
  );
const rules = {
  field1: [maxLength(5)],
  field2: [maxLength(10)]
};
const validate = createValidator(rules);
const change = {
  field1: "123456",
  field2: "123456"
};
validate(change)
  .then(getPayload)
  .then(messages => {
    // --> messages: { field1: "Maximum length is 5 characters"}
  });
1.1.3

5 years ago

1.1.2

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago