Skip to content

zestia/ember-validation

Repository files navigation

@zestia/ember-validation

Ember Observer

This lightweight addon lets you validate an object, or an array of objects.

It works by running one or more functions against each property on the object (or array of objects), and returns a matching structure containing an array of messages that describe each property.

Installation

ember install @zestia/ember-validation

Add the following to ~/.npmrc to pull @zestia scoped packages from Github instead of NPM.

@zestia:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<YOUR_GH_TOKEN>

Demo

https://zestia.github.io/ember-validation

Features

Notes

  • Making a constraint is as simple as writing a function that returns nothing if it passes, or a string if it fails.

Example

import validate from '@zestia/ember-validation';
import {
  present,
  maxLength,
  truthy,
  email,
  date
} from '@zestia/ember-validation/constraints';

const person = {
  id: 1,
  name: '',
  emailAddress: 'joe@bloggs',
  dateOfBirth: null,
  terms: false
};

const constraints = {
  name() {
    return [
      present({ message: 'Please enter your name' }),
      maxLength({ max: 255 })
    ];
  },

  emailAddress() {
    return [present(), email()];
  },

  dateOfBirth() {
    return [present(), date({ format: 'dd/MM/yyyy' })];
  },

  terms() {
    return [truthy({ message: 'Please accept the terms' })];
  }
};

const errors = await validate(person, constraints);

/**
 *  {
 *    name: [
 *      'Please enter your name'
 *    ],
 *    terms: [
 *      'Please accept the terms'
 *    ],
 *    emailAddress: [
 *      'Invalid email'
 *    ],
 *    dateOfBirth: [
 *      'Required value',
 *      'Invalid date, expecting dd/MM/yyyy'
 *    ]
 *  }
 */

Adhoc Constraints

You can validate properties that aren't actually on the object being validated. Here is a contrived example...

const person = {
  firstName: 'Joe',
  lastName: 'Bloggs'
};

const constraints = {
  firstName() {
    return [present()];
  },

  lastName() {
    return [present()];
  },

  name() {
    return [nameIsUnique];
  }
};

const errors = await validate(person, constraints);

/**
 *  {
 *    firstName: null,
 *    lastName: null,
 *    name: ['Must be unique']
 *  }
 */

const names = ['Joe Bloggs'];

function nameIsUnique(value, object) {
  if (names.includes(`${object.firstName} ${object.lastName}`)) {
    return;
  }

  return 'Must be unique';
}

Dynamic Constraints

Because constraints are functions, this allows for a very powerful approach for validating arrays of objects. For example, imagine you have an array of items of a varying types.

const items = [
  { id: 1, value: '', type: 'text' },
  { id: 2, value: '', type: 'number' },
  { id: 3, value: '', type: 'email' },
  { id: 4, value: '', type: 'date' }
];

const constraints = (item) => {
  return {
    value() {
      switch (item.type) {
        case 'text':
          return [present()];
        case 'number':
          return [present(), number()];
        case: 'email':
          return [present(), email()];
        case: 'date':
          return [present(), date({ format: 'dd/MM/yyyy' })];
        default:
          return [];
      }
    }
  };
}

const errors = await validate(items, constraints);

/*
 *  [
 *    {
 *      value: ['Required value']
 *    },
 *    {
 *      value: ['Required value', 'Invalid number']
 *    },
 *    {
 *      value: ['Required value', 'Invalid email']
 *    },
 *    {
 *      value: ['Required value', 'Invalid date, expecting dd/MM/yyyy']
 *    }
 *  ]
 */

Constraints

The following constraints come with this addon. Creating a constraint is as simple as making a function that returns a string if the constraint has failed. Constraints can be asynchronous too.

  • bigDecimal
  • date
  • email
  • greaterThan
  • lessThan
  • maxLength
  • minLength
  • number
  • phoneNumber
  • present
  • truthy

Internationalisation

There are a few approaches you can take to internationalise the error messages. The most obvious one would be to set the message property as the translated string, e.g.

message: this.intl.t('too-large');

Alternatively, you could set the message just as the key, and internationalise it later in handlebars, e.g.

message: 'too-large';

Or, as of version 5, you can provide a key, e.g.

key: 'too-large';

...along with a function that will be called for each failed constraint, e.g.

import { setMessageFn } from '@zestia/ember-validation';

export function initialize(appInstance) {
  const intl = this.owner.lookup('service:intl');

  setMessageFn((key, tokens) => intl.t(`validation.${key}`, tokens));
}

Utils

  • setMessageFn
    Sets the function that will build a string for a given constraint

  • messageFor
    Should be used as the return value of constraint function

  • flattenErrors
    Flattens a validation result into a single array of all the messages

  • collateErrors
    Flattens a validation result into an array of the messages for each field