@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
JavaScript
(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