@darkobits/formation
Version:
619 lines (528 loc) • 20.2 kB
JavaScript
'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
};