UNPKG

@darkobits/formation

Version:

[![][travis-img]][travis-url] [![][npm-img]][npm-url] [![][deps-img]][deps-url] [![][peer-deps-img]][peer-deps-url] [![][dev-deps-img]][dev-deps-url]

1,650 lines (1,398 loc) 122 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("ramda"), require("angular"), require("is-plain-object"), require("@darkobits/interface"), require("angular-messages")); else if(typeof define === 'function' && define.amd) define(["ramda", "angular", "is-plain-object", "@darkobits/interface", "angular-messages"], factory); else if(typeof exports === 'object') exports["Formation"] = factory(require("ramda"), require("angular"), require("is-plain-object"), require("@darkobits/interface"), require("angular-messages")); else root["Formation"] = factory(root["R"], root["angular"], root["isPlainObject"], root["Interface"], root[undefined]); })(this, function(__WEBPACK_EXTERNAL_MODULE_4__, __WEBPACK_EXTERNAL_MODULE_6__, __WEBPACK_EXTERNAL_MODULE_13__, __WEBPACK_EXTERNAL_MODULE_15__, __WEBPACK_EXTERNAL_MODULE_16__) { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 10); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); // ----------------------------------------------------------------------------- // ----- Shared Constants ------------------------------------------------------ // ----------------------------------------------------------------------------- /** * Named used for the Formation Angular module. * * @type {string} */ var MODULE_NAME = exports.MODULE_NAME = 'Formation'; /** * Name used for the form component (not prefixed). * * @type {string} */ var FORM_COMPONENT_NAME = exports.FORM_COMPONENT_NAME = 'fm'; /** * Name used for the form group component (not prefixed). * * @type {string} */ var FORM_GROUP_COMPONENT_NAME = exports.FORM_GROUP_COMPONENT_NAME = 'fmGroup'; /** * Prefix used for all components registered with registerComponent. * This incluedes all built-in components. * * @type {string} */ var DEFAULT_PREFIX = exports.DEFAULT_PREFIX = 'fm'; /** * Key in components' "require" definition that should reference the Formation * form controller. * @type {string} */ var FORM_CONTROLLER = exports.FORM_CONTROLLER = '$formController'; /** * Key at which controls that use ngModel assign a reference to their ngModel * controller. * * @type {string} */ var NG_MODEL_CTRL = exports.NG_MODEL_CTRL = '$ngModelCtrl'; /** * Scope expression at which Formation controls expose their ngModel * getter/setter method. * * @type {string} */ var NG_MODEL_GETTER_SETTER = exports.NG_MODEL_GETTER_SETTER = '$ngModelGetterSetter'; /** * Key in components' bindings that should contain control configuration. * * @type {string} */ var COMPONENT_CONFIGURATION = exports.COMPONENT_CONFIGURATION = '$configuration'; /** * Error validation key to use for signaling the custom error state. * * @type {string} */ var CUSTOM_ERROR_KEY = exports.CUSTOM_ERROR_KEY = '$custom'; /** * Property added to configurable validator instances that allows them to be * identified as such across execution contexts, where instanceof will fail. * * @type {string} */ var CONFIGURABLE_VALIDATOR = exports.CONFIGURABLE_VALIDATOR = '$configurableValidator'; /** * List of HTML5 attributes that should be mirrored from component elements to * form controls. * * @type {array} */ var HTML5_ATTRIBUTES = exports.HTML5_ATTRIBUTES = ['autocomplete', 'autofocus', 'max', 'maxlength', 'min', // 'multiple', 'placeholder', 'readonly', 'size', 'step', 'type', 'wrap']; /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _FormationControl = __webpack_require__(14); Object.keys(_FormationControl).forEach(function (key) { if (key === "default" || key === "__esModule") return; Object.defineProperty(exports, key, { enumerable: true, get: function get() { return _FormationControl[key]; } }); }); /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _config = __webpack_require__(19); Object.keys(_config).forEach(function (key) { if (key === "default" || key === "__esModule") return; Object.defineProperty(exports, key, { enumerable: true, get: function get() { return _config[key]; } }); }); /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.assignToScope = exports.mergeDeep = exports.assertType = undefined; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; exports.assertIsEntry = assertIsEntry; exports.isFunction = isFunction; exports.throwError = throwError; exports.capitalizeFirst = capitalizeFirst; exports.lowercaseFirst = lowercaseFirst; exports.mergeWithDeep = mergeWithDeep; exports.parseFlags = parseFlags; exports.toPairsWith = toPairsWith; exports.mergeEntries = mergeEntries; exports.invoke = invoke; exports.greaterScopeId = greaterScopeId; exports.applyToCollection = applyToCollection; var _isPlainObject = __webpack_require__(13); var _isPlainObject2 = _interopRequireDefault(_isPlainObject); var _ramda = __webpack_require__(4); var _constants = __webpack_require__(0); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // ----------------------------------------------------------------------------- // ----- Utilities ------------------------------------------------------------- // ----------------------------------------------------------------------------- /** * TODO: Consider replacing mergeDeep with webpack-merge et. al. */ /** * Throws an error if the provided value is not a [key, value] entry. Otherwise, * returns true. * * @param {any} value * @param {string} [label] - Optional label. * @return {boolean} */ function assertIsEntry(value, label) { if (!Array.isArray(value) || value.length !== 2) { throwError(['Expected ' + (label ? label + ' to be a ' : '') + '[key, value] entry,', 'but got ' + (0, _ramda.type)(value) + '.'].join(' ')); } return true; } /** * Because Jest's mocked functions fail a Function type check, we need to * additionally check their "typeof" property. * * @param {any} value * @return {boolean} */ function isFunction(value) { return (0, _ramda.is)(Function, value) || typeof value === 'function'; } /** * Throws a new error with the provided message, prefixed with the module * name. * * @param {string} message */ function throwError(message) { throw new Error('[' + _constants.MODULE_NAME + '] ' + message); } /** * Checks the type of a value and throws an error if it does not match one of * the provided types. * * @param {string} callee - Label of the method/process to use in errors. * @param {function|array} types - Constructor/class or list of constructors * and classes to check against. * @param {string} label - Label for the value being checked, used in errors. * @param {any} value - Value to check. * * @return {boolean} - True if types match, throws otherwise. */ var assertType = exports.assertType = (0, _ramda.curry)(function (callee, types, label, value) { types = [].concat(types); var match = types.reduce(function (accumulator, type) { var predicateFn = void 0; switch (type) { case Function: predicateFn = isFunction; break; case Array: predicateFn = Array.isArray; break; case undefined: predicateFn = (0, _ramda.equals)(undefined); break; case null: predicateFn = (0, _ramda.equals)(null); break; default: predicateFn = (0, _ramda.is)(type); break; } return accumulator || predicateFn(value); }, false); if (!match) { var typeNames = types.map(function (ctor) { try { return ctor.prototype.constructor.name; } catch (err) { return (0, _ramda.type)(ctor); } }).join(' or '); throwError([callee + ' expected ' + label + ' to be of type', typeNames + ',', 'but got ' + (0, _ramda.type)(value) + '.'].join(' ')); } return true; }); /** * Capitalizes the first character in the provided string. * * @param {string} str * @return {string} */ function capitalizeFirst(str) { return str && String(str).substr(0, 1).toUpperCase() + String(str).substr(1); } /** * Lowercases the first character in the provided string. * * @param {string} str * @return {string} */ function lowercaseFirst(str) { return str && String(str).substr(0, 1).toLowerCase() + String(str).substr(1); } /** * Recursive version of R.mergeWith. * * @param {function} f - Merging function. * @param {arglist} objs - Objects to merge (right to left.) * @return {object} */ function mergeWithDeep(f) { for (var _len = arguments.length, objs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { objs[_key - 1] = arguments[_key]; } if (objs.length >= 2) { var d = (0, _ramda.nth)(-2, objs) || {}; var s = (0, _ramda.nth)(-1, objs) || {}; var merged = (0, _ramda.mergeWith)(f, d, s); if (objs.length === 2) { return merged; } var rest = (0, _ramda.slice)(0, -2, objs); return mergeWithDeep.apply(undefined, [f].concat(_toConsumableArray((0, _ramda.append)(merged, rest)))); } else if (objs.length === 1) { return (0, _ramda.head)(objs); } return {}; } /** * Default merging function. * * - If values are primitives, use the value from source object, overwriting the * value in the destination object. * - If values are arrays, the source array is appended to the destination * array. (Important for merging arrays of ngMessages.) * - If values are objects, deep merge them. * * @param {object} d - Destination object. * @param {object} s - Source object. * @return {object} - Merged object. */ var DEFAULT_MERGER = function DEFAULT_MERGER(d, s) { if (Array.isArray(d) && Array.isArray(s)) { // Concat arrays. return (0, _ramda.concat)(s, d); } else if ((0, _isPlainObject2.default)(d) && (0, _isPlainObject2.default)(s)) { // Deep-merge plain objects. return mergeWithDeep(DEFAULT_MERGER, d, s); } // Otherwise, return the source value. return s; }; /** * Partially-applied version of mergeWithDeep using the default merger. * * @param {arglist} objs - Objects to merge. * @return {object} - Merged object. */ var mergeDeep = exports.mergeDeep = function mergeDeep() { for (var _len2 = arguments.length, objs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { objs[_key2] = arguments[_key2]; } return mergeWithDeep.apply(undefined, [DEFAULT_MERGER].concat(objs)); }; /** * Accepts a comma/space-delimited list of strings and returns an array of * $-prefixed strings. * * @example * * "touched, submitted" => ['$touched', '$submitted'] * * @private * * @param {string} string * @return {array} */ function parseFlags(string) { if (!string || string === '') { return; } var states = (0, _ramda.map)(function (state) { return state.length && '$' + state.replace(/[, ]/g, ''); }, String(string).split(/, ?| /g)); return (0, _ramda.filter)(_ramda.identity, states); } /** * Assigns a value to an expression on the provided scope. * * @param {object} $parse - Angular $parse service. * @param {object} scope - Angular scope to assign to. * @param {*} value - Value to assign to scope. * @param {string} expression - Expression in scope's parent to assign value to. */ var assignToScope = exports.assignToScope = (0, _ramda.curry)(function ($parse, scope, value, expression) { var setter = void 0; if (expression === '') { setter = $parse('this[""]').assign; } else { setter = $parse(expression).assign; } if (setter) { setter(scope, value); } }); /** * Generates a list of pairs/entries from a collection using the provided * key/value generation functions. * * If called with 2 arguments, they will be interpreted as [keyFn, collection], * and values will be each member in the collection. * * If called with 3 arguments, they will be interpreted as * [keyFn, valueFn, collection]. * * @param {arglist} args - Key generation function, optional value generation * function, and collection. * @return {array} */ function toPairsWith() { for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } var keyFn = _ramda.identity; var valueFn = _ramda.identity; var collection = []; switch (args.length) { case 1: throw new Error('toPairsWith expects at least 2 arguments.'); case 3: keyFn = args[0]; valueFn = args[1]; collection = args[2]; break; case 2: default: keyFn = args[0]; collection = args[1]; break; } if (!isFunction(keyFn)) { throwError('Expected key generation function to be of type "Function", but got "' + (typeof keyFn === 'undefined' ? 'undefined' : _typeof(keyFn)) + '".'); } if (!isFunction(valueFn)) { throwError('Expected key value generation function to be of type "Function", but got "' + (typeof valueFn === 'undefined' ? 'undefined' : _typeof(valueFn)) + '".'); } if (!Array.isArray(collection)) { throwError('Expected collection to be of type "Array", but got "' + (typeof collection === 'undefined' ? 'undefined' : _typeof(collection)) + '".'); } return collection.map(function () { return [String(keyFn.apply(undefined, arguments)), valueFn.apply(undefined, arguments)]; }); } /** * Provided two lists of [key, value] entries, such as those generated using * Object.entries or Map.prototype.entries, returns a list of * [key, valueA, valueB] triplets by matching each entry in the source list with * each of its corresponding entries in the destination list. Extraneous entries * in the source list will be dropped. * * @param {array} dest - Destination set of entries. * @param {array} src - Source set of entries. * @return {array} */ function mergeEntries() { var dest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var src = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var check = assertType('mergeEntries'); check(Array, 'first argument', dest); check(Array, 'second argument', src); return dest.map(function (destEntry) { assertIsEntry(destEntry); var match = src.find(function (srcEntry) { return assertIsEntry(srcEntry) && srcEntry[0] === destEntry[0]; }); return [destEntry[0], destEntry[1], match ? match[1] : undefined]; }); } /** * Invokes the named method on the provided object (if it exists), optionally * passing any additional arguments as parameters to the method. * * @param {string} method - Method name to invoke. * @param {object} obj - Target object. * @param {arglist} [args] - Additional arguments to pass to 'method'. * @return {*} */ function invoke(method, obj) { for (var _len4 = arguments.length, args = Array(_len4 > 2 ? _len4 - 2 : 0), _key4 = 2; _key4 < _len4; _key4++) { args[_key4 - 2] = arguments[_key4]; } return obj && isFunction(obj[method]) && obj[method].apply(obj, args); } /** * Provided two objects that implement a '$getScope' method, returns the * object with the greater $scope id. This is used to determine which object * is likely to be lowe in the scope hierarchy. * * @param {object} a * @param {object} b * @return {object} */ function greaterScopeId(a, b) { var aId = (0, _ramda.path)(['$id'], invoke('$getScope', a)) || 0; var bId = (0, _ramda.path)(['$id'], invoke('$getScope', b)) || 0; return Number(aId) > Number(bId) ? a : b; } /** * Applies a set of data to each member in a collection by matching data to * members and invoking a method on each member, passing it a data fragment. * * @example * * const collection = [ * { * id: '1', * setName: name => { * this.name = name; * } * }, * { * id: '2', * setName: => { * this.name = name; * } * } * ]; * * const data = { * '1': 'foo', * '2': 'bar' * }; * * // This will set the first item's name to 'foo', and the second item's name * // to 'bar', based on matching keys in 'data' to 'id' in collection members. * applyToCollection(collection, R.prop('id'), 'setName', data); * * @param {array} collection - Collection to apply data to. * @param {function} entryFn - Function to pass to toPairsWith to generate the * key (left hand side) for each entry in 'collection'. * @param {string} memberFn - The function to invoke on each member in the * collection to pass matched data fragments to. * @param {object|array} data - Data to disperse to members of 'collection'. */ function applyToCollection(collection, entryFn, memberFn, data) { // Convert collection to entries using the provided entry generation function. var collectionEntries = toPairsWith(entryFn, collection); // Convert data object to entries in the format [key, value]. var dataEntries = Object.entries(data || {}); // Correlate data to registry members by common name/key, generating // triplets in the format [name, member, data]. var mergedEntries = mergeEntries(collectionEntries, dataEntries); // For each triplet, invoke the provided method name on the collection member, // passing it its matching data. Return an entry in the format // [name, returnValue]. return (0, _ramda.map)(function (_ref) { var _ref2 = _slicedToArray(_ref, 3), name = _ref2[0], member = _ref2[1], data = _ref2[2]; return [name, invoke(memberFn, member, data)]; }, mergedEntries); } exports.default = { applyToCollection: applyToCollection, assertIsEntry: assertIsEntry, assertType: assertType, assignToScope: assignToScope, capitalizeFirst: capitalizeFirst, greaterScopeId: greaterScopeId, invoke: invoke, isFunction: isFunction, lowercaseFirst: lowercaseFirst, mergeDeep: mergeDeep, mergeEntries: mergeEntries, mergeWithDeep: mergeWithDeep, parseFlags: parseFlags, throwError: throwError, toPairsWith: toPairsWith }; /***/ }), /* 4 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE_4__; /***/ }), /* 5 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Reset = exports.SetModelValue = exports.GetModelValue = exports.ClearCustomErrorMessage = exports.SetCustomErrorMessage = exports.RegisterForm = exports.RegisterControl = exports.RegisterNgForm = exports.RegisterNgModel = exports.Configure = undefined; var _interface = __webpack_require__(15); /** * Used by controls, forms, and form groups to ingest configuration data. * * @type {Interface} */ var Configure = exports.Configure = new _interface.Interface('Configure'); /** * Called by decorated ngModel controllers to register with a Formation control * or form. * * @type {Interface} */ var RegisterNgModel = exports.RegisterNgModel = new _interface.Interface('RegisterNgModel', [Object]); /** * Called by decorated form/ngForm controllers to register with a form or form * group. * * @type {Interface} */ var RegisterNgForm = exports.RegisterNgForm = new _interface.Interface('RegisterNgForm', [Object]); /** * Called by Formation controls to register with a Formation form or form group * upon instantiation. * * @type {Interface} */ var RegisterControl = exports.RegisterControl = new _interface.Interface('RegisterControl', [Object]); /** * Used by Formation forms and form groups to register with a parent form. * * @type {Interface} */ var RegisterForm = exports.RegisterForm = new _interface.Interface('RegisterForm', [Object]); /** * Implemented by controls to set custom error messages, and by forms to ingest * custom error message data structures to delegate to controls. * * @type {Interface} */ var SetCustomErrorMessage = exports.SetCustomErrorMessage = new _interface.Interface('SetCustomErrorMessage'); /** * Implemented by controls to clear custom error messages, and by forms to * delegate clearing custom error messgaes to known controls/child forms. * * @type {Interface} */ var ClearCustomErrorMessage = exports.ClearCustomErrorMessage = new _interface.Interface('ClearCustomErrorMessage'); /** * Model value getter for forms, form groups, and controls. * * @type {Interface} */ var GetModelValue = exports.GetModelValue = new _interface.Interface('GetModelValue'); /** * Model value setter for forms, form groups, and controls. * * @type {Interface} */ var SetModelValue = exports.SetModelValue = new _interface.Interface('SetModelValue', [_interface.Any]); /** * Implemented by controls, forms, and form groups to facilitate resetting. * * @type {Interface} */ var Reset = exports.Reset = new _interface.Interface('Reset'); /***/ }), /* 6 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE_6__; /***/ }), /* 7 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _angular = __webpack_require__(6); var _angular2 = _interopRequireDefault(_angular); __webpack_require__(16); var _constants = __webpack_require__(0); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _module = _angular2.default.module(_constants.MODULE_NAME, ['ngMessages']); exports.default = _module; /***/ }), /* 8 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(process) { Object.defineProperty(exports, "__esModule", { value: true }); exports.END_SUBMIT_EVENT = exports.BEGIN_SUBMIT_EVENT = exports.NG_FORM_CONTROLLER = undefined; exports.FormController = FormController; var _angular = __webpack_require__(6); var _angular2 = _interopRequireDefault(_angular); var _ramda = __webpack_require__(4); var _config = __webpack_require__(2); var _FormGroup = __webpack_require__(9); var _FormationControl = __webpack_require__(1); var _MockControl = __webpack_require__(20); var _utils = __webpack_require__(3); var _constants = __webpack_require__(0); var _interfaces = __webpack_require__(5); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } // ----------------------------------------------------------------------------- // ----- Form Component -------------------------------------------------------- // ----------------------------------------------------------------------------- /** * Key at which the Formation form controller will store a reference to the * Angular form controller * * @private * * @type {string} */ var NG_FORM_CONTROLLER = exports.NG_FORM_CONTROLLER = '$ngFormController'; /** * Event name used to signal to child forms that a submit has begun. * * @type {string} */ var BEGIN_SUBMIT_EVENT = exports.BEGIN_SUBMIT_EVENT = '$fmInitiateSubmit'; /** * Event name used to signal to child forms that a submit has ended. * * @type {string} */ var END_SUBMIT_EVENT = exports.END_SUBMIT_EVENT = '$fmTerminateSubmit'; /** * Curried assertType. * * Remaining arguments: * * @param {string} label * @param {any} value * * @return {boolean} */ var assertIsObjectOrNil = (0, _utils.assertType)('Form', [Object, undefined]); /** * Controller for the Formation form component. */ function FormController($attrs, $compile, $element, $log, $parse, $scope, $transclude) { var _this = this; var Form = this; /** * Counter for getNextId(). This is used to assign unique IDs to controls * within the form. * * @private * * @type {number} */ var counter = -1; /** * Configured error behavior for the form. * * @private * * @type {array} */ var errorBehavior = []; /** * Control configuration data for this form and possible child forms. * * @private * * @type {object} */ var controlConfiguration = {}; /** * Tracks registered controls and child forms. * * @private * * @type {array} */ var registry = []; /** * Tracks model values for each control. * * @private * * @type {Object} */ var modelValues = new Map(); // ----- Private Methods ----------------------------------------------------- /** * Curried applyToCollection using our local registry and generating entries * using each member's 'name' property. * * Remaining arguments: * * @param {string} methodName - Method name to invoke on each member. * @param {object|array} [data] - Optional data to delegate to members. */ var applyToRegistry = function applyToRegistry() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _utils.applyToCollection.apply(undefined, [registry, (0, _ramda.prop)('name')].concat(args)); }; /** * Curried assignToScope that will assign the form controller instance to the * provided expression in the controller's parent scope. * * Remaining arguments: * * @param {string} expression - Expression to assign to. */ var assignName = (0, _utils.assignToScope)($parse)($scope.$parent)(Form); /** * Returns the next available ID. * * @private * * @return {number} */ function getNextId() { return ++counter; } /** * Returns a promise that resolves when the Angular form controller's * "$pending" flag becomes false. * * @private * * @return {promise} */ function waitForAsyncValidators() { return new Promise(function (resolve) { var watchExpression = 'Form.' + NG_FORM_CONTROLLER + '.$pending'; var cancelWatcher = $scope.$watch(watchExpression, function (isPending) { if (!isPending) { cancelWatcher(); resolve(); } }); }); } /** * Sets related form attributes to the correct state for submitting. * * @private */ function initiateSubmit() { Form[_interfaces.ClearCustomErrorMessage](); Form[NG_FORM_CONTROLLER].$setSubmitted(true); Form.$submitting = true; Form.disable(); // Note: This could be replaced with an interface. $scope.$parent.$broadcast(BEGIN_SUBMIT_EVENT); } /** * Returns the form to an editable state when a submit process is complete. * * @private */ function terminateSubmit() { Form.$submitting = false; Form.enable(); $scope.$parent.$broadcast(END_SUBMIT_EVENT); } // ----- Interfaces ---------------------------------------------------------- /** * Implement a callback that decorated form/ngForm directives will use to * register with this controller. * * @private * * @param {object} ngFormController - Form/ngForm controller instance. */ _interfaces.RegisterNgForm.implementedBy(Form).as(function (ngFormController) { if (Form[NG_FORM_CONTROLLER]) { (0, _utils.throwError)('ngForm already registered with Formation.'); } Form[NG_FORM_CONTROLLER] = ngFormController; // Expose common Angular form controller properties. (0, _ramda.forEach)(function (prop) { Reflect.defineProperty(Form, prop, { get: function get() { return Form[NG_FORM_CONTROLLER][prop]; } }); }, ['$dirty', '$invalid', '$pending', '$pristine', '$submitted', '$valid']); }); /** * Adds the provided child form to the registry and applies model values and * configuration. * * @private * * @param {object} childForm */ _interfaces.RegisterForm.implementedBy(Form).as(function (childForm) { var childFormName = childForm.name; // Ensure there is not another registered child form with the same name as // the form being registered. if (Form.getForm(childFormName)) { (0, _utils.throwError)('Cannot register child form "' + childFormName + '"; another child form with this name already exists.'); } // Ensure there is not a registered control with the same name as the form // being registered. if (Form.getControl(childFormName)) { (0, _utils.throwError)('Cannot register child form "' + childFormName + '"; a control with this name already exists.'); } Form.$debug('Registering child form "' + childFormName + '".'); registry.push(childForm); // Configure the child form/form group. (0, _utils.invoke)(_interfaces.Configure, childForm, controlConfiguration[childFormName]); }); /** * Adds the provided control to the registry and configures it. * * @private * * @param {object} control */ _interfaces.RegisterControl.implementedBy(Form).as(function (control) { var controlName = control.name || 'control'; // Ensure there is not a registered child form with the same name as the // control being registered. if (Form.getForm(controlName)) { (0, _utils.throwError)('Cannot register control "' + controlName + '"; a child form with this name already exists.'); } Form.$debug('Registering control "' + controlName + '".'); // Controls need unique IDs, as radio buttons will share the same name. control.$uid = controlName + '-' + getNextId(); registry.push(control); // Configure the control. (0, _utils.invoke)(_interfaces.Configure, control, controlConfiguration[controlName]); }); /** * Implement a callback that decorated ngModel directives will use to register * with this controller. This is used primarily to support instances of * ngModel used in a Formation form without a Formation control. * * @private * * @param {object} ngModelCtrl */ _interfaces.RegisterNgModel.implementedBy(Form).as(function (ngModelCtrl) { Form[_interfaces.RegisterControl](new _MockControl.MockControl(ngModelCtrl, Form, $scope)); }); /** * Updates the form's configuration data and (re)configures each registered * control, child form, or child form group. */ _interfaces.Configure.implementedBy(Form).as(function (config) { assertIsObjectOrNil('configuration', config); // Update our local configuration object so that controls can pull from it // as they come online. controlConfiguration = (0, _utils.mergeDeep)(controlConfiguration, config); // Delegate to each existing member's Configure method. applyToRegistry(_interfaces.Configure, controlConfiguration); }); /** * Returns the form's aggregate model values by delegating to the * GetModelValue method of each control, child form, or child form group. * * @return {object} */ _interfaces.GetModelValue.implementedBy(Form).as(function () { return (0, _ramda.fromPairs)(applyToRegistry(_interfaces.GetModelValue)); }); /** * Sets the the model value(s) for each registered control, child form, or * child form group. * * @param {object} newValues - Values to set. */ _interfaces.SetModelValue.implementedBy(Form).as(function (newValues) { assertIsObjectOrNil('model values', newValues); // TODO: Document this. Form.model = newValues; // Delegate to each member's SetModelValue method. applyToRegistry(_interfaces.SetModelValue, newValues); }); /** * Applies "$custom" errors returned from the consumer's submit handler. * Expects a mapping of field names to error messages or child forms. * * @private * * @param {object} errorMessages */ _interfaces.SetCustomErrorMessage.implementedBy(Form).as(function (errorMessages) { assertIsObjectOrNil('error messages', errorMessages); // Delegate to each member's SetCustomErrorMessage method. applyToRegistry(_interfaces.SetCustomErrorMessage, errorMessages); }); /** * Clear custom error messages on all registered controls, child forms, and * child form groups that also implement ClearCustomErrorMessage. * * @private */ _interfaces.ClearCustomErrorMessage.implementedBy(Form).as(function () { applyToRegistry(_interfaces.ClearCustomErrorMessage); }); /** * Resets each control and the form to a pristine state. Optionally resets the * model value of each control to the provided value, and validates all * controls. * * @param {object} [modelValues] */ _interfaces.Reset.implementedBy(Form).as(function (modelValues) { assertIsObjectOrNil('model values', modelValues); Form[NG_FORM_CONTROLLER].$setPristine(); // Delegate to each member's Reset method, passing related model value data. applyToRegistry(_interfaces.Reset, modelValues); }); // ----- Angular Lifecycle Hooks --------------------------------------------- /** * Determines whether to use a form or ngForm element based on whether this * instance has a parent form or not. * * @private */ Form.$postLink = function () { function transclude(template) { var elementName = _angular2.default.element(template)[0].tagName; // Compile our template using our isolate scope and append it to our element. $compile(template)($scope, function (compiledElement) { $element.append(compiledElement); }); // Handle transcluded content from the user by appending it to the above // form/ngForm template and using a new scope that inherits from our outer // scope, mimicing the default Angular behavior. $transclude(function (compiledElement, scope) { // Assign a reference to the form controller in the transclusion scope. // This allows users to reference the Form API from templates: // <div ng-if="$fm.getControl('foo').$valid"></div> scope.$fm = Form; $element.find(elementName).append(compiledElement); }); } if (Form.$$parentForm) { transclude('\n <ng-form></ng-form>\n '); } else { transclude('\n <form novalidate\n ng-submit="Form.$submit()"\n ng-model-options="{getterSetter: true}">\n </form>\n '); } }; /** * Set up form name and assign controller instance to its name attribute. * * @private */ Form.$onInit = function () { // If we have a parent form or form group, we need to register with it. We // can 'require' both types of controllers, but we want to register with our // closest ancestor. Angular gives us no simple way to determine this. But, // we can compare each controller's scope ID; the greater of the two is // almost certainly guaranteed to be our closest ancestor. There may be some // weird edge cases here if the page is creating/destroying scopes in an // exotic way, but this is not likely. Form.$parent = (0, _utils.greaterScopeId)(Form.$$parentForm, Form.$$parentFormGroup); // Auto-generate a name if one was not supplied. Form.name = Form.name || 'Form-' + (0, _config.$getNextId)(); // Merge configuration data from the "config" attribute into our local copy. controlConfiguration = (0, _utils.mergeDeep)(controlConfiguration, Form.$controlConfiguration); // Set debug mode if the "debug" attribute is present. if (Reflect.has($attrs, 'debug')) { Form.$debugging = true; } if (Form.$parent) { // If we are a child form, register with our parent form and set up submit // listeners. Form.$parent[_interfaces.RegisterForm](Form); $scope.$on(BEGIN_SUBMIT_EVENT, function () { if (!Form.$submitting) { initiateSubmit(); } }); $scope.$on(END_SUBMIT_EVENT, function () { if (Form.$submitting) { terminateSubmit(); } }); } else { // If the user has provided an onReady callback, invoke it when the form's // element is ready. This happens once all child forms have been compiled // and a user can safely call setModelValues, for example, and be sure // they will propagate down to children. // // This mechanism is necessary, as opposed to using the $postLink // lifecycle hook in a parent controller, because if the user is using // ng-repeat on a <fm> element (likely in a form group) then those forms // will not be compiled until after the parent controller's $postLink has // fired. $element.ready(function () { if ((0, _ramda.is)(Function, Form.$onReady)) { Form.$onReady(Form); } }); // If we are the top-level form, assign to parent scope expression. assignName(Form.name); } // Parse error behavior. errorBehavior = (0, _utils.parseFlags)(Form.$showErrorsOn || (0, _config.$getShowErrorsOnStr)()); }; /** * Handle changes to bindings. * * Note: This will only report reassignment to bindings, it will not * deep-watch bound objects. * * @private * * @param {object} changes */ Form.$onChanges = function (changes) { // Handle changes to name. if (changes.name && !changes.name.isFirstChange()) { var _changes$name = changes.name, currentValue = _changes$name.currentValue, previousValue = _changes$name.previousValue; Form.$debug('Name changed from "' + previousValue + '" to "' + currentValue + '".'); assignName(currentValue); } if (changes.$showErrorsOn && !changes.$showErrorsOn.isFirstChange()) { var _currentValue = changes.$showErrorsOn.currentValue; errorBehavior = (0, _utils.parseFlags)(_currentValue || (0, _config.$getShowErrorsOnStr)()); } }; /** * Handles form tear-down and cleanup. * * @private */ Form.$onDestroy = function () { if (Form.$parent) { Form.$parent.$unregisterForm(Form); } }; // ----- Semi-Public Methods ------------------------------------------------- /** * Passes provided arguments to $log.log if the "debug" attribute is * present on the form element. * * @private * * @param {...arglist} args */ Form.$debug = function () { for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } if (Form.$debugging) { $log.log.apply($log, ['[' + Form.name + ']'].concat(args)); } }; /** * Returns the form's $scope. Used to compare scope IDs for child form * registration, and passed to configurable validators. * * @return {object} */ Form.$getScope = function () { return $scope; }; /** * Returns a copy of the model value for the named control. * * @private * * @param {string} controlName * @return {*} */ Form.$getModelValue = function (controlName) { return modelValues.get(controlName); }; /** * Sets the model value for the named control to a copy of the provided value. * * @private * * @param {string} controlName * @param {*} newValue */ Form.$setModelValue = function (controlName, newValue) { // Any time we programatically update the model values map, we need to // trigger a digest cycle so that controls' ngModel getter/setters will pull // the new values. $scope.$applyAsync(function () { modelValues.set(controlName, (0, _ramda.clone)(newValue)); }); }; /** * Removes the provided control from the registry. * * @private * * @param {object} control */ Form.$unregisterControl = function (control) { if (registry.includes(control)) { Form.$debug('Unregistering control "' + control.name + '".'); registry = (0, _ramda.without)([control], registry); } }; /** * Removes the provided form from the registry. * * @private * * @param {object} control */ Form.$unregisterForm = function (childForm) { if (registry.includes(childForm)) { Form.$debug('Unregistering child form "' + childForm.name + '".'); registry = (0, _ramda.without)([childForm], registry); } }; /** * Handles form submission. * * Once all validators have finished, clears all custom errors and then * checks the form's validity. If valid, calls the consumer's submit handler * passing an object representing each control's current model value. * * If the consumer returns an object (typically from a `.catch()`) it will be * assumed to be a map of control names and error messages, which will be * applied to each control in the map. * * @private */ Form.$submit = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var customErrors; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.prev = 0; if (!Form.$submitting) { _context.next = 4; break; } Form.$debug('Submit already in progress.'); throw new Error('SUBMIT_IN_PROGRESS'); case 4: // [2] Prepare form and child forms for submit. initiateSubmit(); // [3] Wait for async validators to finish. _context.next = 7; return waitForAsyncValidators(); case 7: if (!Form[NG_FORM_CONTROLLER].$invalid) { _context.next = 9; break; } throw new Error('NG_FORM_INVALID'); case 9: if (!(typeof Form.$onSubmit === 'function')) { _context.next = 14; break; } _context.next = 12; return Promise.resolve(Form.$onSubmit(Form.getModelValues())); case 12: customErrors = _context.sent; Form[_interfaces.SetCustomErrorMessage](customErrors); case 14: _context.next = 21; break; case 16: _context.prev = 16; _context.t0 = _context['catch'](0); if (!(typeof process !== 'undefined' && (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development'))) { _context.next = 21; break; } Form.$debug('[Logged During Development Only]', _context.t0.message); throw _context.t0; case 21: _context.prev = 21; // [6] Restore forms to editable state. $apply is needed here because we're // in an async function. $scope.$apply(function () { terminateSubmit(); }); return _context.finish(21); case 24: case 'end': return _context.stop(); } } }, _callee, _this, [[0, 16, 21, 24]]); })); /** * Returns the configured error behavior for the form. * * @private * * @return {array} */ Form.$getErrorBehavior = function () { return (0, _ramda.clone)(errorBehavior); }; // ----- Public Methods ------------------------------------------------------ /** * Returns the first control whose name matches the provided value. * * @param {string} controlName * @return {object} - Control instance. */ Form.getControl = function (controlName) { var control = (0, _ramda.find)((0, _ramda.propEq)('name', controlName), registry); if ((0, _ramda.is)(_FormationControl.FormationControl, control) || (0, _ramda.is)(_MockControl.MockControl, control)) { return control; } }; /** * Returns the first child form or form group whose name matches the provided * name. * * @param {string} formName * @return {object} - Child form instance, if found. */ Form.getForm = function (formName) { var form = (0, _ramda.find)((0, _ramda.propEq)('name', formName), registry); if ((0, _ramda.is)(FormController, form) || (0, _ramda.is)(_FormGroup.FormGroupController, form)) { return form; } }; /** * Returns true if the form is disabled. * * @return {boolean} */ Form.isDisabled = function () { return Form.$disabled || Form.$ngDisabled || Form.$parent && Form.$parent.isDisabled(); }; /** * Disables the form and any controls that implment `isDisabled`. */ Form.disable = function () { Form.$disabled = true; }; /** * Enables the form and any controls that implement `isDisabled`. * * Note: The form may still remain disabled via `ngDisabled`. */ Form.enable = function () { Form.$disabled = false; }; // Expose select interfaces to the public API. Form.configure = Form[_interfaces.Configure]; Form.getModelValues = Form[_interfaces.GetModelValue]; Form.reset = Form[_interfaces.Reset]; Form.setModelValues = Form[_interfaces.SetModelValue]; } // NOTE: This might be obsolete now that it seems to be possible to use // circular dependencies. FormController[_constants.FORM_CONTROLLER] = true; FormController.$inject = ['$attrs', '$compile', '$element', '$log', '$parse', '$scope', '$transclude']; (0, _config.$registerComponent)(_constants.FORM_COMPONENT_NAME, { require: { $$parentForm: '?^^' + _constants.FORM_COMPONENT_NAME, $$parentFormGroup: '?^^' + _constants.FORM_GROUP_COMPONENT_NAME }, bindings: { name: '@', $controlConfiguration: '<controls', $onSubmit: '<onSubmit', $onReady: '<onReady', $showErrorsOn: '@showErrorsOn', $ngDisabled: '<ngDisabled' }, transclude: true, controller: FormController, controllerAs: 'Form' }); exports.default = FormController; /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(18))) /***/ }), /* 9 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.END_SU