UNPKG

@darkobits/formation

Version:
619 lines (528 loc) 20.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.FormationControl = exports.CUSTOM_ERROR_MESSAGE_KEY = exports.NG_MESSAGES = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // ----------------------------------------------------------------------------- // ----- Control Base Class ---------------------------------------------------- // ----------------------------------------------------------------------------- var _ramda = require('ramda'); var _constants = require('../../etc/constants'); var _utils = require('../../etc/utils'); var _interfaces = require('../../etc/interfaces'); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Key at which ngMessage tuples for a control will be stored. * * Shared between: form, control base class. * * @memberOf FormationControl * @alias NG_MESSAGES * * @type {string} */ var NG_MESSAGES = exports.NG_MESSAGES = '$ngMessages'; /** * Key at which the control will store custom error messags. * * @private * * @type {string} */ var CUSTOM_ERROR_MESSAGE_KEY = exports.CUSTOM_ERROR_MESSAGE_KEY = '$customError'; /** * Curried assertType. * * Remaining parameters: * * @param {string} label * @param {any} value * * @return {boolean} */ var assertIsFunction = (0, _utils.assertType)('FormationControl', Function); /** * This class provides the functionality necessary for a component to interact * with a Formation form controller. It can be extended by component controllers * to create custom controls. All built-in Formation controls extend this class. * * ## Ways to use Formation * * 1. Use the built-in Formation components. * * 2. Create your own meta components that are composed of the built-in * Formation components. * * 3. Create your own custom components that extend `FormationControl` and * register them with `registerControl`. * * 4. Use `ngModel` on any element inside a Formation form to register the * ngModel controller with the form. * * ### Extending FormationControl * * Components that extend `FormationControl` should use the the * `registerControl` method of the Formation service. This ensures the necessary * bindings are defined so that the control will work with Formation forms. * Additionally, custom components should adhere to the following guidelines: * * 1. Define a binding for either `name` or `for`, which should refer to the * control name that the component will represent or interact with. Note that * multiple control instances can exist using the same name; ex: radio * buttons. * * 2. (Optional) If the component uses `ngModel`, ensure that the `ng-model` * expression in the component's template references the controller's * `$ngModelGetterSetter` property (provided by `FormationControl`). * * For a reference implementation, see `Input.js`. * * ### Control Configuration * * Control instances can be passed configuration at the control level and at the * form level, with the latter taking precedence over the former. This allows * generic components to be created that can be customized to suit a particular * form later. In both cases the following options are supported: * * - `parsers`: Array of parser functions. * - `formatters`: Array of formatter functions. * - `validators`: Object containing validator functions. * - `asyncValidators`: Object containing async validatior functions. * - `ngModelOptions`: Object with ngModelOptions configuration. * - `errors`: Array containing tuples of validation keys and error messages. */ var FormationControl = exports.FormationControl = function () { function FormationControl() { _classCallCheck(this, FormationControl); this[NG_MESSAGES] = []; // Expose interfaces as public methods. this.getModelValue = this[_interfaces.GetModelValue]; this.setModelValue = this[_interfaces.SetModelValue]; } // ----- Semi-Public Methods ------------------------------------------------- /** * Returns the name of the control, or the name of the control that this * component is for. * * @private * * @return {string} */ _createClass(FormationControl, [{ key: '$getName', value: function $getName() { return this.name || this.for; } /** * Returns the name of the form that this control belongs to. * * @private * * @return {string} */ }, { key: '$getFormName', value: function $getFormName() { return this[_constants.FORM_CONTROLLER] && this[_constants.FORM_CONTROLLER].name; } /** * If the component has an ngModel controller, unregister it when the scope is * destroyed. * * @private */ }, { key: '$onDestroy', value: function $onDestroy() { if (this[_constants.NG_MODEL_CTRL]) { this[_constants.FORM_CONTROLLER].$unregisterControl(this); } } /** * Returns a reference to the "canonical" control that this component instance * represents or interacts with. Ex: An error or other tertiary component that * doesn't use ngModel may use this to access the primary control of the same * name. This works by virtue of the fact that components that do not use * ngModel do not register as controls with the form. * * @private * * @return {object} */ }, { key: '$getControl', value: function $getControl() { return this[_constants.FORM_CONTROLLER].getControl(this.$getName()); } /** * Returns the ID used by the canonical control instance that this component * instance represents or interacts with. * * Example: The label used by the errors component will need the ID of the * control that it is "for", not the ID of its local element. * * @private * * @return {string} */ }, { key: '$getCanonicalControlId', value: function $getCanonicalControlId() { return this.$getControl().getControlId(); } /** * Used by ngModel (via ngModelOptions) to set and retreive model values using * the GetModelValue and SetModelValue interfaces. * * See: https://docs.angularjs.org/api/ng/directive/ngModelOptions * * @private * * @param {arglist} [args] - Arguments passed to the function. * @return {*} - Model value, if invoked without arguments. */ }, { key: '$ngModelGetterSetter', value: function $ngModelGetterSetter() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } if (args.length > 0) { var newValue = args[0]; // This does not defer to the SetModelValue implementation because // validators will need to be able to clear a model value when validation // fails by setting it to undefined. this[_constants.FORM_CONTROLLER].$setModelValue(this.$getName(), (0, _ramda.clone)(newValue)); } else { return this[_interfaces.GetModelValue](); } } // ----- Public Methods ------------------------------------------------------ /** * Returns the ID used by this control instance. * * @example * * vm.myForm.getControl('name').getControlId() => 'vm.myForm-name-2' * * @return {string} */ }, { key: 'getControlId', value: function getControlId() { return this[_constants.FORM_CONTROLLER].name + '-' + this.$uid; } /** * If the canonical control should be displaying errors (based on configured * error behavior) returns the controls' `$error` object. Otherwise, returns * `false`. * * @private * * @param {string} controlName * @return {object} */ }, { key: 'getErrors', value: function getErrors() { var _this = this; var control = this.$getControl(); if (!control) { // The form has no control matching our name/for attribute. return false; } var ngModelCtrl = this.$getControl()[_constants.NG_MODEL_CTRL]; var errorBehavior = this[_constants.FORM_CONTROLLER].$getErrorBehavior(); // If the control is valid, return. if (ngModelCtrl.$valid) { return false; } // If the user did not configure error behavior, return the control's errors // if it is invalid. if ((0, _ramda.isNil)(errorBehavior) || errorBehavior === '') { return !ngModelCtrl.$valid && ngModelCtrl.$error; } // Otherwise, determine if the control should show errors. var errorState = errorBehavior.reduce(function (accumulator, state) { var controlHasState = ngModelCtrl[state]; var formHasState = _this[_constants.FORM_CONTROLLER][state]; return accumulator || controlHasState || formHasState; }, false); return errorState ? ngModelCtrl.$error : false; } /** * Returns the configured error messages for the control. * * @example * * vm.myForm.getControl('name').getErrorMessages() => [ * ['required', 'This field is required.'], * ['minLength', 'Please enter at least 4 characters.'] * ] * * @return {array} */ }, { key: 'getErrorMessages', value: function getErrorMessages() { return this.$getControl(this.$getName())[NG_MESSAGES] || []; } /** * Returns the current custom error message for the control, if set. * * @example * * vm.myForm.getControl('email').getCustomErrorMessage() * // => 'This e-mail address is in use. Please try again.' * * @return {string} */ }, { key: 'getCustomErrorMessage', value: function getCustomErrorMessage() { return this[_constants.FORM_CONTROLLER].getControl(this.$getName())[CUSTOM_ERROR_MESSAGE_KEY]; } /** * Returns true if the control is disabled. * * @return {boolean} */ }, { key: 'isDisabled', value: function isDisabled() { return this.$ngDisabled || this.$disabled || this[_constants.FORM_CONTROLLER].isDisabled(); } /** * Sets the local disabled flag on the control to false. Note that the control * will still be disabled if the form's disabled flag or an ngDisabled * expression on the control is truthy. */ }, { key: 'enable', value: function enable() { this.$disabled = false; } /** * Sets the local disabled flag on the control to true. */ }, { key: 'disable', value: function disable() { this.$disabled = true; } }]); return FormationControl; }(); // ----- Interfaces ------------------------------------------------------------ /** * Configures the control by merging the provided configuration object with * the control's local configuration object. * * @param {object} configuration - Configuration to apply. */ _interfaces.Configure.implementedBy(FormationControl).as(function (configuration) { var _this2 = this; if (!this[_constants.NG_MODEL_CTRL]) { // If this control doesn't use ngModel (ex: Errors) bail. return; } // Merge provided configuration with local configuration. var mergedConfig = (0, _utils.mergeDeep)((0, _ramda.pathOr)({}, [_constants.COMPONENT_CONFIGURATION], this), configuration); var errors = mergedConfig.errors, parsers = mergedConfig.parsers, formatters = mergedConfig.formatters, validators = mergedConfig.validators, asyncValidators = mergedConfig.asyncValidators, ngModelOptions = mergedConfig.ngModelOptions; this[_constants.FORM_CONTROLLER].$debug('Applying configuration to "' + this.$getName() + '":', mergedConfig); // Set up error messages. if (Array.isArray(errors)) { errors.forEach(function (error) { (0, _utils.assertIsEntry)(error, 'error message'); if (!(0, _ramda.contains)(error, _this2[NG_MESSAGES])) { _this2[NG_MESSAGES].push(error); } }); } // Set up parsers. if (Array.isArray(parsers)) { parsers.forEach(function (parser) { if ((0, _ramda.has)(parser, _this2[_constants.NG_MODEL_CTRL].$parsers)) { // Parser already exists on this control, bail. _this2[_constants.FORM_CONTROLLER].$debug('Control "' + _this2.$getName() + '" already has parser:', parser); return; } assertIsFunction('parser', parser); _this2[_constants.NG_MODEL_CTRL].$parsers.push(parser.bind(_this2[_constants.NG_MODEL_CTRL])); }); } // Set up formatters. if (Array.isArray(formatters)) { formatters.forEach(function (formatter) { if ((0, _ramda.has)(formatter, _this2[_constants.NG_MODEL_CTRL].$formatters)) { // Formatter already exists on this control, bail. _this2[_constants.FORM_CONTROLLER].$debug('Control "' + _this2.$getName() + '" already has formatter:', formatter); return; } assertIsFunction('formatter', formatter); _this2[_constants.NG_MODEL_CTRL].$formatters.push(formatter.bind(_this2[_constants.NG_MODEL_CTRL])); }); } // Set up validators. if ((0, _ramda.is)(Object, validators)) { (0, _ramda.mapObjIndexed)(function (validator, name) { if (validator === false) { // Remove the named validator. if ((0, _ramda.has)(name, _this2[_constants.NG_MODEL_CTRL].$validators)) { _this2[_constants.FORM_CONTROLLER].$debug('Removing validator "' + name + '" from control "' + _this2.$getName() + '".'); } Reflect.deleteProperty(_this2[_constants.NG_MODEL_CTRL].$validators, name); _this2[_constants.NG_MODEL_CTRL].$setValidity(name, true); return; } if (Object.values(_this2[_constants.NG_MODEL_CTRL].$validators).includes(validator)) { // Validator already exists on this control, bail. _this2[_constants.FORM_CONTROLLER].$debug('Control "' + _this2.$getName() + '" already has validator:', validator); return; } if (validator && validator[_constants.CONFIGURABLE_VALIDATOR]) { // Check against the CONFIGURABLE_VALIDATOR constant here rather than // using is() because instanceof does not work across execution // contexts. _this2[_constants.NG_MODEL_CTRL].$validators[name] = validator.configure(_this2); } else { assertIsFunction('validator', validator); _this2[_constants.NG_MODEL_CTRL].$validators[name] = validator.bind(_this2[_constants.NG_MODEL_CTRL]); } }, validators); } // Set up asyncronous validators. if ((0, _ramda.is)(Object, asyncValidators)) { (0, _ramda.mapObjIndexed)(function (asyncValidator, name) { if (asyncValidator === false) { // Remove the named async validator. if ((0, _ramda.has)(name, _this2[_constants.NG_MODEL_CTRL].$asyncValidators)) { _this2[_constants.FORM_CONTROLLER].$debug('Removing async validator "' + name + '" from control "' + _this2.$getName() + '".'); } Reflect.deleteProperty(_this2[_constants.NG_MODEL_CTRL].$asyncValidators, name); _this2[_constants.NG_MODEL_CTRL].$setValidity(name, true); return; } if (Object.values(_this2[_constants.NG_MODEL_CTRL].$asyncValidators).includes(asyncValidator)) { // Async validator already exists on this control, bail. _this2[_constants.FORM_CONTROLLER].$debug('Control "' + _this2.$getName() + '" already has async validator:', asyncValidator); return; } if (asyncValidator && asyncValidator[_constants.CONFIGURABLE_VALIDATOR]) { // Check against the CONFIGURABLE_VALIDATOR constant here rather than // using is() because instanceof does not work across execution // contexts. _this2[_constants.NG_MODEL_CTRL].$asyncValidators[name] = asyncValidator.configure(_this2); } else { assertIsFunction('async validator', asyncValidator); _this2[_constants.NG_MODEL_CTRL].$asyncValidators[name] = asyncValidator.bind(_this2[_constants.NG_MODEL_CTRL]); } }, asyncValidators); } // Configure ngModelOptions. if ((0, _ramda.is)(Object, ngModelOptions)) { this[_constants.NG_MODEL_CTRL].$options = this[_constants.NG_MODEL_CTRL].$options.createChild(ngModelOptions); } // Validate the control to ensure any new parsers/formatters/validators // are run. this[_constants.NG_MODEL_CTRL].$validate(); }); /** * Register and configure the provided ngModel controller. * * @param {object} ngModelCtrl */ _interfaces.RegisterNgModel.implementedBy(FormationControl).as(function (ngModelCtrl) { // Configure the control. if (this[_constants.COMPONENT_CONFIGURATION]) { this[_interfaces.Configure](); } if (this[_constants.FORM_CONTROLLER]) { // Create a reference to the control's ngModel controller. this[_constants.NG_MODEL_CTRL] = ngModelCtrl; // Register the control with the form. this[_constants.FORM_CONTROLLER][_interfaces.RegisterControl](this); } }); /** * Returns the control's model value. * * @example * * vm.myForm.getControl('age').getModelValue() => 42 * * @return {*} */ _interfaces.GetModelValue.implementedBy(FormationControl).as(function () { return this[_constants.FORM_CONTROLLER].$getModelValue(this.$getName()); }); /** * If the provided value is undefined, sets the control's model value. * * @example * * vm.myForm.getControl('name').setModelValue('Frodo'); * * @param {*} newValue - Value to set. */ _interfaces.SetModelValue.implementedBy(FormationControl).as(function (modelValue) { if (modelValue !== undefined) { this[_constants.FORM_CONTROLLER].$setModelValue(this.$getName(), (0, _ramda.clone)(modelValue)); } }); /** * Sets a custom error on the control and sets the "custom" validity state to * false. * * @private * * @param {string} errorMessage */ _interfaces.SetCustomErrorMessage.implementedBy(FormationControl).as(function (errorMessage) { if (errorMessage) { (0, _utils.assertType)('FormationControl', String, 'error message', errorMessage); this[_constants.FORM_CONTROLLER].$debug('Setting custom error "' + errorMessage + '" on control "' + this.$getName() + '".'); this[CUSTOM_ERROR_MESSAGE_KEY] = errorMessage; this[_constants.NG_MODEL_CTRL].$setValidity(_constants.CUSTOM_ERROR_KEY, false); } }); /** * Sets the "custom" validity state of the provided control to true, and * clears the custom error message. * * @private * * @param {object} control */ _interfaces.ClearCustomErrorMessage.implementedBy(FormationControl).as(function () { if ((0, _ramda.path)([_constants.NG_MODEL_CTRL, '$error', _constants.CUSTOM_ERROR_KEY], this)) { this[_constants.FORM_CONTROLLER].$debug('Clearing custom error on control "' + this.$getName() + '".'); this[_constants.NG_MODEL_CTRL].$setValidity(_constants.CUSTOM_ERROR_KEY, true); Reflect.deleteProperty(this, CUSTOM_ERROR_MESSAGE_KEY); } }); /** * Resets the contol to an untouched, pristine state. * * @private * * @param {object} control */ _interfaces.Reset.implementedBy(FormationControl).as(function (modelValue) { if (this[_constants.NG_MODEL_CTRL]) { this[_constants.NG_MODEL_CTRL].$setUntouched(); this[_constants.NG_MODEL_CTRL].$setPristine(); if (modelValue !== undefined) { this.setModelValue(modelValue); } this[_constants.NG_MODEL_CTRL].$validate(); } }); exports.default = { FormationControl: FormationControl };