UNPKG

ember-cp-validations

Version:
476 lines (443 loc) 14.4 kB
// False positive: the native class that exists in the document does not use CPs /* eslint-disable ember/no-computed-properties-in-native-classes */ import { bool } from '@ember/object/computed'; import EmberObject, { set, get, computed } from '@ember/object'; import { isNone } from '@ember/utils'; import { getOwner } from '@ember/application'; import Messages from 'ember-cp-validations/validators/messages'; import Options from 'ember-cp-validations/-private/options'; import lookupValidator from 'ember-cp-validations/utils/lookup-validator'; import { unwrapString, getValidatableValue, mergeOptions, isPromise, } from 'ember-cp-validations/utils/utils'; class TestResult { constructor(result) { this.isValid = result === true; this.message = typeof result === 'string' ? result : null; } } /** * @class Base * @module Validators */ const Base = EmberObject.extend({ /** * Options passed in to the validator when defined in the model * @property options * @type {Object} */ options: null, /** * Default validation options for this specific attribute * @property defaultOptions * @type {Object} */ defaultOptions: null, /** * Global validation options for this model * @property globalOptions * @type {Object} */ globalOptions: null, /** * Model instance * @property model * @type {Model} */ model: null, /** * Attributed name of the model this validator is attached to * @property attribute * @type {String} */ attribute: null, /** * Error message object. Populated by validators/messages * @property errorMessages * @type {Object} */ errorMessages: null, /** * @property isWarning * @type {Boolean} */ isWarning: bool('options.isWarning').readOnly(), /** * Validator type * @property _type * @private * @type {String} */ _type: null, /** * Validators cache used by `test` api * @property _testValidatorCache * @private * @type {Object} */ _testValidatorCache: computed(function () { return {}; }).readOnly(), init() { this._super(...arguments); let globalOptions = this.globalOptions; let defaultOptions = this.defaultOptions; let options = this.options; let owner = getOwner(this); let errorMessages; if (!isNone(owner)) { // Since default error messages are stored in app/validators/messages, we have to look it up via the owner errorMessages = owner.factoryFor('validator:messages'); } // If for some reason, we can't find the messages object (i.e. unit tests), use default errorMessages = errorMessages || Messages; set( this, 'options', this.buildOptions(options, defaultOptions, globalOptions), ); set(this, 'errorMessages', errorMessages.create()); }, /** * 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](https://github.com/adopted-ember-addons/ember-cp-validations/blob/master/addon/validators/presence.js) is a good example to checkout * @method buildOptions * @param {Object} options * @param {Object} defaultOptions * @param {Object} globalOptions * @return {Object} */ buildOptions(options = {}, defaultOptions = {}, globalOptions = {}) { let builtOptions = mergeOptions(options, defaultOptions, globalOptions); // Overwrite the validator's value method if it exists in the options and remove it since // there is no need for it to be passed around this.value = builtOptions.value || this.value; delete builtOptions.value; return new Options({ model: this.model, attribute: this.attribute, options: builtOptions, }); }, /** * Used to retrieve the value to validate. * This method gets called right before `validate` and the returned value * gets passed into the validate method. * * @method value * @param {Object} model * @param {String} attribute * @return The current value of `model[attribute]` */ value(model, attribute) { return get(model, attribute); }, /** * Wrapper method to `value` that passes the necessary parameters * * @method getValue * @private * @return {Mixed} value */ getValue() { let value = this.value(this.model, this.attribute); return getValidatableValue(value); }, /** * 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: * @method validate * @param {Mixed} value The current value of the attribute * @param {Object} options The built and processed options * @param {Object} model The current model being evaluated * @param {String} attribute The current attribute being evaluated * @return * One of the following types: * - `Boolean`: `true` if the current value passed the validation * - `String`: The error message * - `Promise`: A promise that will either resolve or reject, and will finally return either `true` or the final error message string. */ validate() { return true; }, /** * 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. * * ```javascript * 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'`. * * @method createErrorMessage * @param {String} type The type of message template to use * @param {Mixed} value Current value being evaluated * @param {Object} options Validator built and processed options (used as the message string context) * @return {String} The generated message */ createErrorMessage(type, value, options = {}) { let messages = this.errorMessages; let message = unwrapString(options.message); set( options, 'description', messages.getDescriptionFor(this.attribute, options), ); if (message) { if (typeof message === 'string') { message = messages.formatMessage(message, options); } else if (typeof message === 'function') { message = message.apply(this, arguments); message = isNone(message) ? messages.getMessageFor(type, options) : messages.formatMessage(message, options); } } else { message = messages.getMessageFor(type, options); } return message.trim(); }, /** * Easily compose complicated validations by using this method to validate * against other validators. * * ```javascript * 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; * } * ``` * @method test * @param {String} type The validator type (e.x. 'presence', 'length', etc.) * The following types are unsupported: * 'alias', 'belongs-to', 'dependent', 'has-many' * @param {...args} args The arguments to pass through to the validator * @return {Object} The test result object which will contain `isValid` * and `message`. If the validator is async, then the * return value will be a promise. */ test(type, ...args) { const cache = this._testValidatorCache; const unsupportedTypes = ['alias', 'belongs-to', 'dependent', 'has-many']; if (unsupportedTypes.includes(type)) { throw new Error( `[ember-cp-validations] The \`test\` API does not support validators of type: ${type}.`, ); } cache[type] = cache[type] || lookupValidator(getOwner(this), type).create(); const result = cache[type].validate(...args); if (isPromise(result)) { return result.then( (r) => new TestResult(r), (r) => new TestResult(r), ); } return new TestResult(result); }, }); Base.reopenClass({ /** * Generate the needed dependent keys for this validator * * @method getDependentsFor * @static * @param {String} attribute * @param {Object} options * @return {Array} dependent keys */ getDependentsFor() { return []; }, }); export default Base; /** * Creating custom validators is very simple. To generate a validator named `unique-username` in Ember CLI * * ```bash * ember generate validator unique-username * ``` * * This will create the following files * * * `app/validators/unique-username.js` * * `tests/unit/validators/unique-username-test.js` * * ```javascript * // 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 {{#crossLink 'Base'}}Base Validator{{/crossLink}}. * * 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. * * ```javascript * // app/validators/unique-username.js * * import { service } from '@ember/service'; * import BaseValidator from 'ember-cp-validations/validators/base'; * * const UniqueUsername = BaseValidator.extend({ * store: 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: * * ```javascript * // 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 * * ```javascript * const Validations = buildValidations({ * username: validator('unique-username', { * showSuggestions: true * }), * }); * * export default Model.extend(Validations, { * 'username': attr('string'), * }); * ``` * * ## Testing * As mentioned before, the generator created a unit test for your new custom validator. * * ```javascript * // 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 * * ```javascript * test('username is unique', function(assert) { * assert.expect(1); * * let validator = this.subject(); * let done = assert.async(); * * validator.validate('johndoe42').then((message) => { * assert.strictEqual(message, true); * done(); * }); * }); * ``` * @class Custom * @module Validators * @extends Base */