

ember-changeset-validations
is a companion validation library to ember-changeset
. It's really simple to use and understand, and there are no CPs or observers anywhere ā it's mostly just functions.
Since ember-changeset
is required to use this addon, please see documentation there on how to install and use changesets.
To install if your app is on ember-source >= 3.13:
ember install ember-changeset-validations
To install if your app is on ember-source < 3.13:
ember install ember-changeset-validations@v2.2.1
Starting with v4 this addon does not install ember-changeset
so make sure to list it in your devDependencies (for apps) or dependencies (for addons).
Watch a 6-part video series on ember-changeset and ember-changeset-validations presented by EmberScreencasts.
Usage
This addon updates the changeset
helper by taking in a validation map as a 2nd argument (instead of a validator function). This means that you can very easily compose validations and decouple the validation from the underlying model.
{{! application/template.hbs}}
<DummyForm
@changeset={{changeset user EmployeeValidations}}
@submit={{action "submit"}}
@rollback={{action "rollback"}} />
<DummyForm
@changeset={{changeset user AdminValidations}}
@submit={{action "submit"}}
@rollback={{action "rollback"}} />
A validation map is just a POJO (Plain Old JavaScript Object). Use the bundled validators from ember-changeset-validations
to compose validations or write your own. For example:
import {
validatePresence,
validateLength,
validateConfirmation,
validateFormat
} from 'ember-changeset-validations/validators';
import validateCustom from '../validators/custom';
import validatePasswordStrength from '../validators/password-strength';
export default {
firstName: [
validatePresence(true),
validateLength({ min: 4 })
],
lastName: validatePresence(true),
age: validateCustom({ foo: 'bar' }),
email: validateFormat({ type: 'email' }),
password: [
validateLength({ min: 8 }),
validatePasswordStrength({ minScore: 80 })
],
passwordConfirmation: validateConfirmation({ on: 'password' })
};
Then, you can use the POJO as a property on your Component or Controller and use it in the template:
import Component from '@glimmer/component';
import EmployeeValidations from '../validations/employee';
import AdminValidations from '../validations/admin';
export default class EmployeeComponent extends Component {
EmployeeValidations = EmployeeValidations;
AdminValidations = AdminValidations;
}
<DummyForm
@changeset={{changeset user this.EmployeeValidations}}
@submit={{action "submit"}}
@rollback={{action "rollback"}} />
Moreover, as of 3.8.0, a validator can be an Object or Class with a validate
function.
import fetch from 'fetch';
export default class PersonalNoValidator {
async validate(key, newValue, oldValue, changes, content) {
try {
await fetch(
'/api/personal-no/validation',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: newValue })
}
);
return true;
} catch (_) {
return 'Personal No is invalid';
}
}
}
When creating the Changeset
programmatically instead of using the changeset
helper, you will have to apply the lookupValidator
function to convert the POJO to a validator function as expected by Changeset
:
import Component from '@glimmer/component';
import EmployeeValidations from '../validations/employee';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class ChangesetComponent extends Component {
constructor() {
super(...arguments);
this.changeset = new Changeset(this.model, lookupValidator(EmployeeValidations), EmployeeValidations);
}
}
<DummyForm
@changeset={{this.changeset}}
@submit={{action "submit"}}
@rollback={{action "rollback"}} />
ember-changeset
and ember-changeset-validations
both also support creating changesets from promises. However, because that will also return a promise, to render in your template you will need to use a helper like await
from ember-promise-helpers
.
Validator API
ember-changeset-validations
utilizes ember-validators
as a core set of validators.
All validators take a custom message option.
presence
Validates presence/absence of a value.
š All Options
{
propertyName: validatePresence(true),
propertyName: validatePresence(false)
propertyName: validatePresence({ presence: true })
propertyName: validatePresence({ presence: true, ignoreBlank: true })
}
on
option for presence
Only validates for presence if any of the other values are present
{
password: validatePresence({ presence: true, on: 'ssn' })
password: validatePresence({ presence: true, on: [ 'ssn', 'email', 'address' ] })
password: validatePresence({ presence: false, on: 'alternative-login' })
}
ā¬ļø back to top
length
Validates the length of a String
or an Array
.
š All Options
{
propertyName: validateLength({ min: 1 }),
propertyName: validateLength({ max: 8 }),
propertyName: validateLength({ min: 1, max: 8 }),
propertyName: validateLength({ is: 16 }),
propertyName: validateLength({ allowBlank: true })
}
ā¬ļø back to top
date
This API accepts valid Date objects or a Date in milliseconds since Jan 1 1970, or a functiom that returns a Date. Strings are currently not supported. It is recommended you use use native JavaScript or you library of choice to generate a date from your data.
{
propertyName: validateDate({ before: new Date('3000-01-01') }),
propertyName: validateDate({ onOrBefore: Date.parse(new Date('3000-01-01')) }),
propertyName: validateDate({ after: new Date('3000-01-01') }),
propertyName: validateDate({ onOrAfter: new Date('3000-01-01') }),
propertyName: validateDate({ onOrAfter: () => new Date() }),
propertyName: validateDate({ onOrAfter: '3000-01-01' }),
}
ā¬ļø back to top
number
Validates various properties of a number.
š All Options
{
propertyName: validateNumber({ is: 16 }),
propertyName: validateNumber({ allowBlank: true }),
propertyName: validateNumber({ integer: true }),
propertyName: validateNumber({ lt: 10 }),
propertyName: validateNumber({ lte: 10 }),
propertyName: validateNumber({ gt: 5 }),
propertyName: validateNumber({ gte: 10 }),
propertyName: validateNumber({ positive: true }),
propertyName: validateNumber({ odd: true }),
propertyName: validateNumber({ even: true }),
propertyName: validateNumber({ multipleOf: 7 })
}
ā¬ļø back to top
inclusion
Validates that a value is a member of some list or range.
š All Options
{
propertyName: validateInclusion({ list: ['Foo', 'Bar'] }),
propertyName: validateInclusion({ range: [18, 60] }),
propertyName: validateInclusion({ allowBlank: true }),
}
ā¬ļø back to top
exclusion
Validates that a value is a not member of some list or range.
š All Options
{
propertyName: validateExclusion({ list: ['Foo', 'Bar'] }),
propertyName: validateExclusion({ range: [18, 60] }),
propertyName: validateExclusion({ allowBlank: true }),
}
ā¬ļø back to top
format
Validates a String
based on a regular expression.
š All Options
{
propertyName: validateFormat({ allowBlank: true }),
propertyName: validateFormat({ type: 'email' }),
propertyName: validateFormat({ type: 'phone' }),
propertyName: validateFormat({ type: 'url' }),
propertyName: validateFormat({ regex: /\w{6,30}/ })
propertyName: validateFormat({ type: 'email', inverse: true })
}
ā¬ļø back to top
confirmation
Validates that a field has the same value as another.
š All Options
{
propertyName: validateConfirmation({ on: 'password' }),
propertyName: validateConfirmation({ allowBlank: true }),
}
ā¬ļø back to top
Writing your own validators
Adding your own validator is super simple ā there are no Base classes to extend! Validators are just functions. All you need to do is to create a function with the correct signature.
Create a new validator using the blueprint:
ember generate validator <name>
ember-changeset-validations
expects a higher order function that returns the validator function. The validator (or inner function) accepts a key
, newValue
, oldValue
, changes
, and content
. The outer function accepts options for the validator.
Synchronous validators
For example:
export default function validateCustom({ min, max } = {}) {
return (key, newValue, oldValue, changes, content) => {
}
}
Asynchronous validators
In addition to conforming to the function signature above, your validator function should return a Promise that resolves with true
(if valid), or an error message string if invalid.
For example:
export default function validateUniqueness(opts) {
return (key, newValue, oldValue, changes, content) => {
return new Promise((resolve) => {
resolve(true);
});
};
}
Using custom validators
That's it! Then, you can use your custom validator like so:
import { validateLength } from 'ember-changeset-validations/validators';
import validateUniqueness from '../validators/unique';
import validateCustom from '../validators/custom';
export default {
firstName: validateCustom({ min: 4, max: 8 }),
lastName: validateCustom({ min: 1 }),
email: [
validateFormat({ type: 'email'}),
validateUniqueness()
]
};
Testing
Since validators are higher order functions that return functions, testing is straightforward and requires no additional setup:
import validateUniqueness from 'path/to/validators/uniqueness';
import { module, test } from 'qunit';
module('Unit | Validator | uniqueness');
test('it does something', function(assert) {
let key = 'email';
let options = { };
let validator = validateUniqueness(options);
assert.equal(validator(key, undefined), );
assert.equal(validator(key, null), );
assert.equal(validator(key, ''), );
assert.equal(validator(key, 'foo@bar.com'), );
});
Validation composition
Because validation maps are POJOs, composing them couldn't be simpler:
import {
validatePresence,
validateLength
} from 'ember-changeset-validations/validators';
export default {
firstName: validatePresence(true),
lastName: validatePresence(true)
};
You can easily import other validations and combine them using Object.assign
.
import UserValidations from './user';
import { validateNumber } from 'ember-changeset-validations/validators';
export const AdultValidations = {
age: validateNumber({ gt: 18 })
};
export default Object.assign({}, UserValidations, AdultValidations);
Custom validation messages
Each validator that is a part of this library can utilize a message
property on the options
object passed to the validator. That message
property can either be a string or a function.
If message
is a string, you can put particular placeholders into it that will be automatically replaced. For example:
{
propertyName: validatePresence({ presence: true, message: '{description} should be present' })
}
{description}
is a hardcoded placeholder that will be replaced with a normalized version of the property name being validated. Any other placeholder will map to properties of the options
object you pass to the validator.
Message can also accept a function with the signature (key, type, value, context)
. Key is the property name being validated. Type is the type of validation being performed (in the case of validators such as number
or length
, there can be a couple of different ones.) Value is the actual value being validated. Context maps to the options
object you passed to the validator.
If message
is a function, it must return the error message as a string.
Overriding validation messages
If you need to be able to override the entire validation message object, simply create a module at app/validations/messages.js
, exporting a POJO with the following keys:
export default {
inclusion:
exclusion:
invalid:
confirmation:
accepted:
empty:
blank:
present:
collection:
singular:
tooLong:
tooShort:
between:
before:
onOrBefore:
after:
onOrAfter:
wrongDateFormat:
wrongLength:
notANumber:
notAnInteger:
greaterThan:
greaterThanOrEqualTo:
equalTo:
lessThan:
lessThanOrEqualTo:
otherThan:
odd:
even:
positive:
multipleOf:
date:
email:
phone:
url:
}
In the message body, any text wrapped in single braces will be replaced with their appropriate values that were passed in as options to the validator. For example:
import buildMessage from 'ember-changeset-validations/utils/validation-errors';
export default function validateIsOne(options) {
return (key, newValue, oldValue, changes, content) => {
return newValue === 1 || buildMessage(key, { type: 'isOne', value: newValue, context: options });
}
}
export default {
mySpecialNumber: validateIsOne({ foo: 'foo' }})
};
The above will look for a key isOne
in your custom validation map, and use keys defined on the options object (in this case, foo
) to replace tokens. With the custom validator above, we can add:
export default {
isOne: '{description} must equal one, and also {foo}'
}
Will render: My special number must equal one, and also foo
.
Raw error output
By default, ember-changeset-validations
returns the errors as plain strings.
In some situations, it may be preferable for the developer that the library returns a description of the errors;
internationalisation (i18n) for example, or finer-grained error output.
To have ember-changeset-validations
return such data structure, add the following to you config/environment.js
let ENV = {
...
'changeset-validations': { rawOutput: true }
...
}
This will return an object with the following structure, that you can then pass to your applications's error processing:
{
value, // the value to validate
type, // the type of the error (`present`, `blank`...)
message, // the **unprocessed** error message
context: {
description // the description of the field
// ...and other options given to configure the validator
}
}
Contributors
We're grateful to these wonderful contributors who've contributed to ember-changeset-validations
:

Installation
git clone <repository-url>
this repositorycd ember-changeset-validations
npm install
Running
Running Tests
npm test
(Runs ember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
Building
For more information on using ember-cli, visit https://ember-cli.com/.