Creating custom validators is very simple. To generate a validator named unique-username
in Ember CLI
ember generate validator unique-username
This will create the following files
app/validators/unique-username.js
tests/unit/validators/unique-username-test.js
// app/validators/unique-username.js
import BaseValidator from 'ember-cp-validations/validators/base';
const UniqueUsername = BaseValidator.extend({
validate(value, options, model, attribute) {
return true;
}
});
UniqueUsername.reopenClass({
getDependentsFor(attribute, options) {
return [];
}
});
export default UniqueUsername;
Side Note: Before we continue, I would suggest checking out the documentation for the Base Validator.
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 is unique.
// app/validators/unique-username.js
import Ember from 'ember';
import BaseValidator from 'ember-cp-validations/validators/base';
const UniqueUsername = BaseValidator.extend({
store: Ember.inject.service(),
validate(value, options, model, attribute) {
return this.get('store').findRecord('user', value).then((user) => {
if(user && user.id === value) {
let message = The username '${value}' already exists.
;
let meta = user.get('meta');
if(options.showSuggestions && meta && meta.suggestions) {
message += "What about one of the these: " + meta.suggestions.join(', ');
}
return message;
} else {
return true;
}
})
}
});
Dependent Keys
There will be times when your validator will be dependent on some other property or object. Instead of having to
include them in your option's dependentKeys
, you can declare them in the static getDependentsFor
hook. This hook
receives two parameters. The first is the attribute
that this validator is being added to, and the second are the options
there were passed to this validator.
From the above code sample:
// app/validators/unique-username.js
import BaseValidator from 'ember-cp-validations/validators/base';
const UniqueUsername = BaseValidator.extend({});
UniqueUsername.reopenClass({
getDependentsFor(attribute, options) {
return [];
}
});
export default UniqueUsername;
All dependent keys are in reference to the model's validations.attrs
object. So when you return ['username']
,
it will add a dependent to model.validations.attrs.username
. If you want to add a dependent on the model, your
key needs to be prefixed with model
. So when you return ['model.username']
, it will add a dependent to model.username
instead of model.validations.attrs.username
.
This means that if you have a dependent on a service, that service must be injected into the model since returning ['model.myService.someProperty']
will be interpreted as model.myService.someProperty
.
Usage
To use our unique-username validator we just have to add it to the model definition
const Validations = buildValidations({
username: validator('unique-username', {
showSuggestions: true
}),
});
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/unique-username-test.js
import Ember from 'ember';
import { moduleFor, test } from 'ember-qunit';
moduleFor('validator:unique-username', 'Unit | Validator | unique-username', {
needs: ['validator:messages']
});
test('it works', function(assert) {
const validator = this.subject();
assert.ok(validator);
});
A simple test for our validation method can be as such
test('username is unique', function(assert) {
assert.expect(1);
let validator = this.subject();
let done = assert.async();
validator.validate('johndoe42').then((message) => {
assert.equal(message, true);
done();
});
});
-
options
-
defaultOptions
-
globalOptions
Build options hook. Merges default options into options object. This method gets called on init and is the ideal place to normalize your options. The presence validator is a good example to checkout
Returns:
-
type
-
value
-
options
Used by all pre-defined validators to build an error message that is present
in validators/message
or declared in your i18n solution.
If we extended our default messages to include uniqueUsername: '{username} already exists'
,
we can use this method to generate our error message.
validate(value, options) {
const exists = false;
// check with server if username exists...
if(exists) {
// The username key on the options object will be used to create the error message
options.username = value;
return this.createErrorMessage('uniqueUsername', value, options);
}
return true;
}
If we input johndoe
and that username already exists, the returned message would be 'johndoe already exists'
.
Parameters:
Returns:
The generated message
Wrapper method to value
that passes the necessary parameters
Returns:
value
-
type
-
args
Easily compose complicated validations by using this method to validate against other validators.
validate(value, options, ...args) {
let result = this.test('presence', value, { presence: true }, ...args);
if (!result.isValid) {
return result.message;
}
// You can even test against your own custom validators
result = this.test('my-validator', value, { foo: 'bar' }, ...args);
if (!result.isValid) {
return result.message;
}
result = this.test('number', value, { integer: true }, ...args);
// You can easily override the error message by returning your own.
if (!result.isValid) {
return 'This value must be an integer!';
}
// Add custom logic...
return true;
}
Parameters:
-
type
StringThe validator type (e.x. 'presence', 'length', etc.) The following types are unsupported: 'alias', 'belongs-to', 'dependent', 'has-many'
-
args
...argsThe arguments to pass through to the validator
Returns:
The test result object which will contain isValid
and message
. If the validator is async, then the
return value will be a promise.
-
value
-
options
-
model
-
attribute
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:
Parameters:
Returns:
One of the following 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.