Package Exports
- ember-cp-validations
- ember-cp-validations/htmlbars-plugins/v-get
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ember-cp-validations) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Ember CP Validations
A Ruby on Rails inspired model validation framework that is completely and utterly computed property based.
Features
No observers were used nor harmed while developing and testing this addon.
- Lazily computed validations
- Ruby on rails inspired validators
- Support for both Ember Data Models and Objects
- Synchronous and asynchronous support for both validators and validations
- Dirty tracking
- Support for nested models via
belongs-to
andhasMany
relationships - No observers. Seriously... there are none. Like absolutely zero....
Integrated with Ember Data's DS.Errors APIwaiting on #3707 to be resolved- Meta data based cycle tracking to detect cycles within your model relationships which could break the CP chain
- Custom validators
- Ember CLI generator to create custom validators with a unit test
Installation
ember install ember-cp-validations
Changelog
Changelog can be found here
Live Demo
A live demo can be found here
Looking for help?
If it is a bug please open an issue on GitHub.
Basic Usage
The first thing we need to do it build our validation rules. This will then generate a Mixin that you will be able to incorporate into your model or object.
// models/user.js
import Ember from 'ember';
import DS from 'ember-data';
import {
validator, buildValidations
}
from 'ember-cp-validations';
var Validations = buildValidations({
username: validator('presence', true),
password: [
validator('presence', true),
validator('length', {
min: 4,
max: 8
})
],
email: [
validator('presence', true),
validator('format', { type: 'email' }),
validator('confirmation', {
message: 'do not match',
attributeDescription: 'Email addresses'
})
],
emailConfirmation: [
validator('presence', true),
validator('confirmation', {
on: 'email',
message: 'do not match',
attributeDescription: 'Email addresses'
})
]
});
Once our rules are created and our Mixin is generated, all we have to do is add it to our model.
// models/user.js
export default DS.Model.extend(Validations, {
'username': attr('string'),
'password': attr('string'),
'email': attr('string')
});
You can also use the generated Validations
mixin on any Ember.Object
or child
of Ember.Object
, like Ember.Component
. For example:
// components/x-foo.js
import Ember from 'ember';
import {
validator, buildValidations
}
from 'ember-cp-validations';
var Validations = buildValidations({
bar: validator('presence', true)
});
export default Ember.Component.extend(Validations, {
bar: null
});
Validators
Common Options
attributeDescription
A descriptor for your attribute used in generating the error messages. Defaults to This field'
// Examples
validator('date', {
attributeDescription: 'Date of birth'
})
// If validation is run and the attribute is empty, the error returned will be:
// 'Date of birth can't be blank'
message
This option can take two forms. It can either be a string
or a function
. If a string is used, then it will overwrite all error message types for the specified validator. Some messages are passed values such as the confirmation
validator and can be accessed via %@
. To overwrite this, we can simply do
// Example: String
validator('confirmation', {
message: 'does not match %@. What are you even thinking?!'
})
We can pass a function
into our message option for even more customization capabilities.
// Example: Function
validator('date', {
message: function(type, options, value) {
if (type === 'before') {
return 'should really be before %@';
}
if (type === 'after') {
return 'should really be after %@';
}
}
})
The message function is given the following arguments:
type
(String): The error message typeoptions
(Object): The validator options that were defined in the modelvalue
: The current value being evaluated
The return value must be a string
. If nothing is returned (undefined
), defaults to the default error message of the specified type.
Within this function, the context is set to that of the current validator. This gives you access to the model, defaultMessages, options and more.
Presence
If true
validates that the given value is not empty, if false
, validates that the given value is empty.
// Examples
validator('presence', true)
validator('presence', false)
validator('presence', {
presence: true,
message: 'should not be empty'
})
Inclusion
Validates that the attributes’ values are included in a given list. All comparisons are done using strict equality so type matters! For range, the value type is checked against both lower and upper bounds for type equality.
Options
allowBlank
(Boolean): If true, skips validation if the value is emptyin
(Array): The list of values this attribute could berange
(Array): The range in which the attribute's value should reside in
// Examples
validator('inclusion', {
in: ['User', 'Admin']
})
validator('inclusion', {
range: [0, 5] // Must be between 0 (inclusive) to 5 (inclusive)
})
Because of the strict equality comparisons, you can use this validator in many different ways.
// Examples
validator('inclusion', {
in: ['Admin'] // Input must be equal to 'Admin'
})
validator('inclusion', {
range: [0, Infinity] // Input must be positive number
})
validator('inclusion', {
range: [-Infinity, Infinity] // Input must be a number
})
Exclusion
Validates that the attributes’ values are not included in a given list. All comparisons are done using strict equality so type matters! For range, the value type is checked against both lower and upper bounds for type equality.
Options
allowBlank
(Boolean): If true, skips validation if the value is emptyin
(Array): The list of values this attribute should not berange
(Array): The range in which the attribute's value should not reside in
// Examples
validator('exclusion', {
in: ['Admin', 'Super Admin']
})
validator('exclusion', {
range: [0, 5] // Cannot be between 0 (inclusive) to 5 (inclusive)
})
Length
Validates the length of the attributes’ values.
Options
allowBlank
(Boolean): If true, skips validation if the value is emptyis
(Number): The exact length the value can bemin
(Number): The minimum length the value can bemax
(Number): The maximum length the value can be
// Examples
validator('length', {
is: 15
})
validator('length', {
min: 5,
max: 10
})
Date
Validate over a date range. Uses MomentJS for date mathematics and calculations.
Options
allowBlank
(Boolean): If true, skips validation if the value is emptybefore
(String): The specified date must be before this dateafter
(String): The specified date must be after this dateformat
(String): Input value date formaterrorFormat
(String): Error output date format. Defaults toMMM Do, YYYY
// Example
validator('date', {
after: 'now',
before: '1/1/2020',
format: 'M/D/YYY',
errorFormat: 'M/D/YYY'
})
// If before or after is set to 'now', the value given to the validator will be tested against the current date and time.
Format
Validate over a predefined or custom regular expression.
Options
allowBlank
(Boolean): If true, skips validation if the value is emptytype
(String): Can be the one of the following options [email
,phone
,url
]regex
(RegExp): The regular expression to test against
// Examples
validator('format', {
type: 'email'
})
validator('format', {
allowBlank: true
type: 'phone'
})
validator('format', {
type: 'url'
})
validator('format', {
regex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$/,
message: 'Password must include at least one upper case letter, one lower case letter, and a number'
})
If you do not want to use the predefined regex for a specific type, you can do something like this
// Example
validator('format', {
type: 'email',
regex: /My Better Email Regexp/
})
This allows you to still keep the email error message but with your own custom regex.
Dependent
Defines an attribute as valid only if its dependents are valid.
Options
on
(Array): Attributes this field is dependent on
// Example
// Full name will only be valid if firstName and lastName are filled in
validator('dependent', {
on: ['firstName', 'lastName'],
})
Confirmation
You should use this validator when you have two text fields that should receive exactly the same content. For example, you may want to confirm an email address or a password. This validator doesnt have to be created on an attribute defined in your model. This means that when you save your model, in this case, verfiedEmail
will not be part of the payload.
// Example
email: validator('format', {
type: 'email'
})
verifiedEmail: validator('confirmation', {
on: 'email'
message: 'do not match'
attributeDescription: 'Email addresses'
})
Collection
If true
validates that the given value is a valid collection and will add <ATTRIUTE>.[]
as a dependent key to the CP. If false
, validates that the given value is singular. Use this validator if you want validation to occur when the content of your collection changes.
// Examples
validator('collection', true)
validator('collection', false)
validator('collection', {
collection: true,
message: 'must be a collection'
})
Belongs To
Identifies a belongs-to
relationship in an Ember Data Model. This is used to create a deeper validation across all of your nested models.
// Example
validator('belongs-to')
Has Many
Identifies a has-many
relationship in an Ember Data Model. This is used to create a deeper validation across all of your nested models.
// Example
validator('has-many')
Function
A validator can also be declared with a function. The function will be then wrapped in the Base Validator
class and used just like any other pre-defined validator.
// Example
validator(function(value, options /*, model, attribute*/) {
return value === options.username ? true : `must be ${options.username}`;
} , {
username: 'John' // Any options can be passed here
})
Custom Validators
Creating custom validators is very simple. To generate a validator named username-exists
in Ember CLI
ember generate validator username-exists
This will create the following files
app/validators/username-exists,js
tests/unit/validators/username-exists-test.js
// app/validators/username-exists,js
import Ember from 'ember';
import BaseValidator from 'ember-cp-validations/validators/base';
export default BaseValidator.extend({
validate(value, options /*, model, attribute*/) {
return true;
}
});
If you want to interact with the store
within your validator, you can simply inject the service like you would a component. Since you have access to your model and the current value, you should be able to send the server the right information to determine if this username exists or not.
The validate method is where all of your logic should go. It will get passed in the current value of the attribute this validator is attached to. Within the validator object, you will have access to the following properties:
model
(Model): The current model being validatedoptions
(Object): The options that were passed in to the validator definition in the modelattribute
(String): The current attribute being validateddefaultMessages
(Object): The default error messages
The validate
method has the following signature:
function validate(value, options, model, attribute) { }
The validate
method should return one of three types
Boolean
:true
if the current value passed the validationString
: The error messagePromise
: A promise that will either resolve or reject, and will finally return eithertrue
or the final error message string.
To use our username-exists validator we just have to add it to the model definition
var Validations = buildValidations({
username: validator('username-exists'),
});
export default DS.Model.extend(Validations, {
'username': DS.attr('string'),
});
Testing
As mentioned before, the generator created a unit test for your new custom validator.
// tests/unit/validators/username-exists-test.js
import Ember from 'ember';
import { moduleFor, test } from 'ember-qunit';
moduleFor('validator:username-exists', 'Unit | Validator | username-exists', {
needs: ['validator:messages']
});
test('it works', function(assert) {
var validator = this.subject();
assert.ok(validator);
});
A simple test for our validation method can be as such
test('is required', function(assert) {
var validator = this.subject();
message = validator.validate('johndoe42');
assert.equal(message, 'Username already exists');
});
Overwriting and Extending
All predefined validators are imported into your application under app/validators
. This means that if you want to overwrite the predefined length validator, all you have to do is create validator app/validators/length.js
and put in your own logic. On the other hand, if you just want to extend a predefined validator, you can do something like this
// app/validators/better-format,js
import Ember from 'ember';
import Format from './format';
export default Format.extend({
validate(value, options, model /*, attribute*/) {
// Do some custom logic here
return this._super(...arguments);
}
});
Custom Error Messages
The default validation error messages are imported in your app's validators
folder. If you want to overwrite them, all you need to do is create a messages.js
file under app/validators
. All default error messages can be found here.
Running Manual Validations
Although validations are lazily computed, there are times where we might want to force all or specific validations to happen. For this reason we have exposed two methods:
validateSync
: Should only be used if all validations are synchronous. It will throw an error if any of the validations are asynchronousvalidate
: Will always return a promise and should be used if asynchronous validations are present
Both methods have the same signature:
function validateSync(options) {}
function validate(options) {}
Options
on
(Array): Only validate the given attributes. If empty, will validate over all validatable attributeexcludes
(Array): Exclude validation on the given attributes
// Examples
const {
m,
validations
} = model.validateSync();
validations.get('isValid') // true or false
model.validate({
on: ['username', 'email']
}).then(({m, validations}) => {
// all validations pass
validations.get('isValid'); // true or false
validations.get('isValidating'); // false
var usernameValidations = validations.get('content').findBy('attribute', 'username');
// can also use m.get('validations.attrs.username');
usernameValidations.get('isValid') // true or false
});
Inspecting Validations
All validations can be accessed via the validations
object created on your model/object. Each attribute also has its own validation which has the same properties. An attribute validation can be accessed via validations.attrs.<ATTRIBUTE>
. If you want to use Ember Data's Errors API, check out their docs on how to access everything you might need.
isValid
// Examples
get(user, 'validations.isValid')
get(user, 'validations.attrs.username.isValid')
isInvalid
// Examples
get(user, 'validations.isInvalid')
get(user, 'validations.attrs.username.isInvalid')
isValidating
This property is toggled only if there is an async validation
// Examples
get(user, 'validations.isValidating')
get(user, 'validations.attrs.username.isValidating')
isTruelyValid
Will be true only if isValid is true
and isValidating is false
// Examples
get(user, 'validations.isTruelyValid')
get(user, 'validations.attrs.username.isTruelyValid')
isDirty
Will be true is the attribute in question is not null
or undefined
. If the object being validated is an Ember Data Model and you have a defaultValue
specified, then it will use that for comparison.
// Examples
// 'username' : DS.attr('string', { defaultValue: 'johndoe' })
get(user, 'validations.isDirty')
get(user, 'validations.attrs.username.isDirty')
isAsync
Will be true
only if a validation returns a promise
// Examples
get(user, 'validations.isAsync')
get(user, 'validations.attrs.username.isAsync')
messages
A collection of all error messages on the object in question
// Examples
get(user, 'validations.messages')
get(user, 'validations.attrs.username.messages')
message
An alias to the first message in the messages collection.
// Example
get(user, 'validations.message')
get(user, 'validations.attrs.username.message')
Templating Example
Accessing validation information in your templates is really simple. This addon provides a v-get
helper to bypass the long validation pathing.
Instead of doing this:
{{model.validations.attrs.username.message}}
You can do this:
{{v-get model 'username' 'message'}}
What's awesome about this is that you can pass in bound properties!
{{v-get model attr prop}}
{{v-get model prop}}
Here is a more extensive example:
<form>
{{input value=model.username placeholder="Username"}}
{{#if (v-get model 'username' 'isInvalid')}}
<div class="error">
{{v-get model 'username' 'message'}}
</div>
{{/if}}
<button type="submit" disabled={{v-get model 'isInvalid'}}>Submit</button>
</form>