UNPKG

storm-validate

Version:

[![npm version](https://badge.fury.io/js/storm-validate.svg)](https://badge.fury.io/js/storm-validate)

983 lines (885 loc) 37.5 kB
/** * @name storm-validate: * @version 0.7.0: Wed, 03 Apr 2019 10:29:59 GMT * @author stormid * @license MIT */ (function(root, factory) { var mod = { exports: {} }; if (typeof exports !== 'undefined'){ mod.exports = exports factory(mod.exports) module.exports = mod.exports.default } else { factory(mod.exports); root.StormValidate = mod.exports.default } }(this, function(exports) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _reducers; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return 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); } } var ACTIONS = { SET_INITIAL_STATE: 'SET_INITIAL_STATE', CLEAR_ERRORS: 'CLEAR_ERRORS', VALIDATION_ERRORS: 'VALIDATION_ERRORS', VALIDATION_ERROR: 'VALIDATION_ERROR', CLEAR_ERROR: 'CLEAR_ERROR', ADD_VALIDATION_METHOD: 'ADD_VALIDATION_METHOD' }; //https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address var EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; //https://mathiasbynens.be/demo/url-regex var URL_REGEX = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i; var DATE_ISO_REGEX = /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/; var NUMBER_REGEX = /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/; var DIGITS_REGEX = /^\d+$/; //data-attribute added to error message span created by .NET MVC var DOTNET_ERROR_SPAN_DATA_ATTRIBUTE = 'data-valmsg-for'; //validator parameters that require DOM lookup var DOM_SELECTOR_PARAMS = ['remote-additionalfields', 'equalto-other']; //.NET MVC validator data-attribute parameters indexed by their validators //e.g. <input data-val-length="Error messge" data-val-length-min="8" data-val-length-max="10" type="text"... /> var DOTNET_PARAMS = { length: ['length-min', 'length-max'], stringlength: ['length-max'], range: ['range-min', 'range-max'], // min: ['min'],? // max: ['max'],? minlength: ['minlength-min'], maxlength: ['maxlength-max'], regex: ['regex-pattern'], equalto: ['equalto-other'], remote: ['remote-url', 'remote-additionalfields', 'remote-type'] //?? }; //.NET MVC data-attributes that identify validators var DOTNET_ADAPTORS = ['required', 'stringlength', 'regex', // 'digits', 'email', 'number', 'url', 'length', 'minlength', 'range', 'equalto', 'remote']; //classNames added/updated on .NET MVC error message span var DOTNET_CLASSNAMES = { VALID: 'field-validation-valid', ERROR: 'error-message' }; /** * All state/model-modifying operations */ var reducers = (_reducers = {}, _defineProperty(_reducers, ACTIONS.SET_INITIAL_STATE, function (state, data) { return Object.assign({}, state, data); }), _defineProperty(_reducers, ACTIONS.CLEAR_ERRORS, function (state) { return Object.assign({}, state, { groups: Object.keys(state.groups).reduce(function (acc, group) { acc[group] = Object.assign({}, state.groups[group], { errorMessages: [], valid: true }); return acc; }, {}) }); }), _defineProperty(_reducers, ACTIONS.CLEAR_ERROR, function (state, data) { var nextGroup = {}; nextGroup[data] = Object.assign({}, state.groups[data], { errorMessages: [], valid: true }); return Object.assign({}, state, { groups: Object.assign({}, state.groups, nextGroup) }); }), _defineProperty(_reducers, ACTIONS.ADD_VALIDATION_METHOD, function (state, data) { var nextGroup = {}; nextGroup[data.groupName] = Object.assign({}, state.groups[data.groupName] ? state.groups[data.groupName] : {}, state.groups[data.groupName] ? { validators: [].concat(_toConsumableArray(state.groups[data.groupName].validators), [data.validator]) } : { fields: [].slice.call(document.getElementsByName(data.groupName)), serverErrorNode: document.querySelector('[' + DOTNET_ERROR_SPAN_DATA_ATTRIBUTE + '=' + data.groupName + ']') || false, valid: false, validators: [data.validator] }); return Object.assign({}, state, { groups: Object.assign({}, state.groups, nextGroup[data.groupName]) }); }), _defineProperty(_reducers, ACTIONS.VALIDATION_ERRORS, function (state, data) { return Object.assign({}, state, { realTimeValidation: true, groups: Object.keys(state.groups).reduce(function (acc, group) { acc[group] = Object.assign({}, state.groups[group], data[group]); return acc; }, {}) }); }), _defineProperty(_reducers, ACTIONS.VALIDATION_ERROR, function (state, data) { return Object.assign({}, state, { groups: Object.assign({}, state.groups, _defineProperty({}, data.group, Object.assign({}, state.groups[data.group], { errorMessages: data.errorMessages, valid: false }))) }); }), _reducers); var createStore = function createStore() { //shared centralised validator state var state = {}; //uncomment for debugging by writing state history to window // window.__validator_history__ = []; //state getter var getState = function getState() { return state; }; /** * Create next state by invoking reducer on current state * * Execute side effects of state update, as passed in the update * * @param type [String] * @param nextState [Object] New slice of state to combine with current state to create next state * @param effects [Array] Array of side effect functions to invoke after state update (DOM, operations, cmds...) */ var dispatch = function dispatch(type, nextState, effects) { state = nextState ? reducers[type](state, nextState) : state; //uncomment for debugging by writing state history to window // window.__validator_history__.push({[type]: state}), console.log(window.__validator_history__); if (!effects) return; effects.forEach(function (effect) { effect(state); }); }; return { dispatch: dispatch, getState: getState }; }; var isCheckable = function isCheckable(field) { return (/radio|checkbox/i.test(field.type) ); }; var isFile = function isFile(field) { return field.getAttribute('type') === 'file'; }; var isSelect = function isSelect(field) { return field.nodeName.toLowerCase() === 'select'; }; var isSubmitButton = function isSubmitButton(node) { return node.getAttribute('type') === 'submit' || node.nodeName === 'BUTTON'; }; var hasNameValue = function hasNameValue(node) { return node.hasAttribute('name') && node.hasAttribute('value'); }; var isRequired = function isRequired(group) { return group.validators.filter(function (validator) { return validator.type === 'required'; }).length > 0; }; var groupIsHidden = function groupIsHidden(fields) { return fields.reduce(function (acc, field) { if (field.type === 'hidden') acc = true; return acc; }, false); }; var hasValue = function hasValue(input) { return input.value !== undefined && input.value !== null && input.value.length > 0; }; var groupValueReducer = function groupValueReducer(acc, input) { if (!isCheckable(input) && hasValue(input)) acc = input.value; if (isCheckable(input) && input.checked) { if (Array.isArray(acc)) acc.push(input.value);else acc = [input.value]; } return acc; }; var resolveGetParams = function resolveGetParams(nodeArrays) { return nodeArrays.map(function (nodes) { return nodes[0].getAttribute('name') + '=' + extractValueFromGroup(nodes); }).join('&'); }; var DOMNodesFromCommaList = function DOMNodesFromCommaList(list, input) { return list.split(',').map(function (item) { var resolvedSelector = escapeAttributeValue(appendStatePrefix(item, getStatePrefix(input.getAttribute('name')))); return [].slice.call(document.querySelectorAll('[name=' + resolvedSelector + ']')); }); }; var escapeAttributeValue = function escapeAttributeValue(value) { return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1"); }; var getStatePrefix = function getStatePrefix(fieldName) { return fieldName.substr(0, fieldName.lastIndexOf('.') + 1); }; var appendStatePrefix = function appendStatePrefix(value, prefix) { if (value.indexOf("*.") === 0) value = value.replace("*.", prefix); return value; }; var extractValueFromGroup = function extractValueFromGroup(group) { return group.hasOwnProperty('fields') ? group.fields.reduce(groupValueReducer, '') : group.reduce(groupValueReducer, ''); }; var fetch = function fetch(url, props) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(props.method || 'GET', url); if (props.headers) { Object.keys(props.headers).forEach(function (key) { xhr.setRequestHeader(key, props.headers[key]); }); } xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);else reject(xhr.statusText); }; xhr.onerror = function () { return reject(xhr.statusText); }; xhr.send(props.body); }); }; var isOptional = function isOptional(group) { return !isRequired(group) && extractValueFromGroup(group) === ''; }; var extractValidationParams = function extractValidationParams(group, type) { return group.validators.filter(function (validator) { return validator.type === type; })[0].params; }; var curryRegexMethod = function curryRegexMethod(regex) { return function (group) { return isOptional(group) || group.fields.reduce(function (acc, input) { return acc = regex.test(input.value), acc; }, false); }; }; var curryParamMethod = function curryParamMethod(type, reducer) { return function (group) { return isOptional(group) || group.fields.reduce(reducer(extractValidationParams(group, type)), false); }; }; var methods = { required: function required(group) { return extractValueFromGroup(group) !== ''; }, email: curryRegexMethod(EMAIL_REGEX), url: curryRegexMethod(URL_REGEX), date: function date(group) { return isOptional(group) || group.fields.reduce(function (acc, input) { return acc = !/Invalid|NaN/.test(new Date(input.value).toString()), acc; }, false); }, dateISO: curryRegexMethod(DATE_ISO_REGEX), number: curryRegexMethod(NUMBER_REGEX), digits: curryRegexMethod(DIGITS_REGEX), minlength: curryParamMethod('minlength', function (params) { return function (acc, input) { return acc = Array.isArray(input.value) ? input.value.length >= +params.min : +input.value.length >= +params.min, acc; }; }), maxlength: curryParamMethod('maxlength', function (params) { return function (acc, input) { return acc = Array.isArray(input.value) ? input.value.length <= +params.max : +input.value.length <= +params.max, acc; }; }), equalto: curryParamMethod('equalto', function (params) { return function (acc, input) { return acc = params.other.reduce(function (subgroupAcc, subgroup) { if (extractValueFromGroup(subgroup) !== input.value) subgroupAcc = false; return subgroupAcc; }, true), acc; }; }), pattern: curryParamMethod('pattern', function (params) { return function (acc, input) { return acc = RegExp(params.regex).test(input.value), acc; }; }), regex: curryParamMethod('regex', function (params) { return function (acc, input) { return acc = RegExp(params.regex).test(input.value), acc; }; }), min: curryParamMethod('min', function (params) { return function (acc, input) { return acc = +input.value >= +params.min, acc; }; }), max: curryParamMethod('max', function (params) { return function (acc, input) { return acc = +input.value <= +params.max, acc; }; }), length: curryParamMethod('length', function (params) { return function (acc, input) { return acc = +input.value.length >= +params.min && (params.max === undefined || +input.value.length <= +params.max), acc; }; }), range: curryParamMethod('range', function (params) { return function (acc, input) { return acc = +input.value >= +params.min && +input.value <= +params.max, acc; }; }), remote: function remote(group, params) { return new Promise(function (resolve, reject) { fetch(params.type !== 'get' ? params.url : params.url + '?' + resolveGetParams(params.additionalfields), { method: params.type.toUpperCase(), body: params.type === 'get' ? null : resolveGetParams(params.additionalfields), headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }) }).then(function (res) { return res.json(); }).then(function (data) { resolve(data); }).catch(function (res) { resolve('Server error: ' + res); }); }); }, custom: function custom(method, group) { return isOptional(group) || method(extractValueFromGroup(group), group.fields); } }; var messages = { required: function required() { return 'This field is required.'; }, email: function email() { return 'Please enter a valid email address.'; }, pattern: function pattern() { return 'The value must match the pattern.'; }, url: function url() { return 'Please enter a valid URL.'; }, date: function date() { return 'Please enter a valid date.'; }, dateISO: function dateISO() { return 'Please enter a valid date (ISO).'; }, number: function number() { return 'Please enter a valid number.'; }, digits: function digits() { return 'Please enter only digits.'; }, maxlength: function maxlength(props) { return 'Please enter no more than ' + props + ' characters.'; }, minlength: function minlength(props) { return 'Please enter at least ' + props + ' characters.'; }, max: function max(props) { return 'Please enter a value less than or equal to ' + [props] + '.'; }, min: function min(props) { return 'Please enter a value greater than or equal to ' + props + '.'; }, equalTo: function equalTo() { return 'Please enter the same value again.'; }, remote: function remote() { return 'Please fix this field.'; } }; /** * Resolve validation parameter to a string or array of DOM nodes * * @param param [String] identifier for the data-attribute `data-val-${param}` * @param input [DOM node] the node which contains the data-val- attribute * * @return validation param [Object] indexed by second part of param name (e.g., 'min' part of length-min') and array of DOM nodes or a string */ var resolveParam = function resolveParam(param, input) { var value = input.getAttribute('data-val-' + param); return _defineProperty({}, param.split('-')[1], !!~DOM_SELECTOR_PARAMS.indexOf(param) ? DOMNodesFromCommaList(value, input) : value); }; /** * Looks up the data-val property against the known .NET MVC adaptors/validation method * runs the matches against the node to find param values, and returns an Object containing all parameters for that adaptor/validation method * * @param input [DOM node] the node on which to look for matching adaptors * @param adaptor [String] .NET MVC data-attribute that identifies validator * * @return validation params [Object] Validation param object containing all validation parameters for an adaptor/validation method */ var extractParams = function extractParams(input, adaptor) { return DOTNET_PARAMS[adaptor] ? { params: DOTNET_PARAMS[adaptor].reduce(function (acc, param) { return input.hasAttribute('data-val-' + param) ? Object.assign(acc, resolveParam(param, input)) : acc; }, {}) } : false; }; /** * Reducer that takes all know .NET MVC adaptors (data-attributes that specifiy a validation method that should be papiied to the node) * and checks against a DOM node for matches, returning an array of validators * * @param input [DOM node] * * @return validators [Array], each validator compposed of * type [String] naming the validator and matching it to validation method function * message [String] the error message displayed if the validation method returns false * params [Object] (optional) */ var extractDataValValidators = function extractDataValValidators(input) { return DOTNET_ADAPTORS.reduce(function (validators, adaptor) { return !input.getAttribute('data-val-' + adaptor) ? validators : [].concat(_toConsumableArray(validators), [Object.assign({ type: adaptor, message: input.getAttribute('data-val-' + adaptor) }, extractParams(input, adaptor))]); }, []); }; /** * Checks attributes on an input to generate an array of validators the attributes describe * * @param input [DOM node] * * @return validators [Array] */ var extractAttrValidators = function extractAttrValidators(input) { var validators = []; if (input.hasAttribute('required') && input.getAttribute('required') !== 'false') { validators.push({ type: 'required' }); } if (input.getAttribute('type') === 'email') validators.push({ type: 'email' }); if (input.getAttribute('type') === 'url') validators.push({ type: 'url' }); if (input.getAttribute('type') === 'number') validators.push({ type: 'number' }); if (input.getAttribute('minlength') && input.getAttribute('minlength') !== 'false') { validators.push({ type: 'minlength', params: { min: input.getAttribute('minlength') } }); } if (input.getAttribute('maxlength') && input.getAttribute('maxlength') !== 'false') { validators.push({ type: 'maxlength', params: { min: input.getAttribute('maxlength') } }); } if (input.getAttribute('min') && input.getAttribute('min') !== 'false') { validators.push({ type: 'min', params: { min: input.getAttribute('min') } }); } if (input.getAttribute('max') && input.getAttribute('max') !== 'false') { validators.push({ type: 'max', params: { min: input.getAttribute('max') } }); } if (input.getAttribute('pattern') && input.getAttribute('pattern') !== 'false') { validators.push({ type: 'pattern', params: { regex: input.getAttribute('pattern') } }); } return validators; }; /** * Takes an input and returns the array of validators based on either .NET MVC data-val- or HTML5 attributes * * @param input [DOM node] * * @return validators [Array] */ var normaliseValidators = function normaliseValidators(input) { return input.getAttribute('data-val') === 'true' ? extractDataValValidators(input) : extractAttrValidators(input); }; /** * Calls a validation method against an input group * * @param group [Array] DOM nodes with the same name attribute * @param validator [String] The type of validator matching it to validation method function * */ var validate = function validate(group, validator) { return validator.type === 'custom' ? methods['custom'](validator.method, group) : methods[validator.type](group, validator.params); }; /** * Reducer constructing an validation Object for a group of DOM nodes * * @param input [DOM node] * * @returns validation object [Object] consisting of * valid [Boolean] the validityState of the group * validators [Array] of validator objects * fields [Array] DOM nodes in the group * serverErrorNode [DOM node] .NET MVC server-rendered error message span * */ var assembleValidationGroup = function assembleValidationGroup(acc, input) { var name = input.getAttribute('name'); return acc[name] = acc[name] ? Object.assign(acc[name], { fields: [].concat(_toConsumableArray(acc[name].fields), [input]) }) : { valid: false, validators: normaliseValidators(input), fields: [input], serverErrorNode: document.querySelector('[' + DOTNET_ERROR_SPAN_DATA_ATTRIBUTE + '="' + input.getAttribute('name') + '"]') || false }, acc; }; /** * Returns an error message property of the validator Object that has returned false or the corresponding default message * * @param validator [Object] * * @return message [String] error message * */ var extractErrorMessage = function extractErrorMessage(validator) { return validator.message || messages[validator.type](validator.params !== undefined ? validator.params : null); }; /** * Curried function that returns a reducer that reduces the resolved response from an array of validation Promises performed against a group * into an array of error messages or an empty array * * @return error messages [Array] * */ var reduceErrorMessages = function reduceErrorMessages(group, state) { return function (acc, validity, j) { return validity === true ? acc : [].concat(_toConsumableArray(acc), [typeof validity === 'boolean' ? extractErrorMessage(state.groups[group].validators[j]) : validity]); }; }; /** * From all groups found in the current form, thosethat do not require validation (have no assocated validators) are removed * * @param groups [Object] name-indexed object consisting of all groups found in the current form * * @return groups [Object] name-indexed object consisting of all validatable groups * */ var removeUnvalidatableGroups = function removeUnvalidatableGroups(groups) { var validationGroups = {}; for (var group in groups) { if (groups[group].validators.length > 0 && !groupIsHidden(groups[group].fields)) validationGroups[group] = groups[group]; }return validationGroups; }; /** * Takes a form DOM node and returns the initial form validation state - an object consisting of all the validatable input groups * with validityState, fields, validators, and associated data required to perform validation and render errors. * * @param form [DOM nodes] * * @return state [Object] consisting of groups [Object] name-indexed validation groups * */ var getInitialState = function getInitialState(form) { return { form: form, errorNodes: {}, realTimeValidation: false, groups: removeUnvalidatableGroups([].slice.call(form.querySelectorAll('input:not([type=submit]), textarea, select')).reduce(assembleValidationGroup, {})) }; }; /** * Reducer run against an array of resolved validation promises to set the overall validityState of a group * * @return validityState [Boolean] * */ var reduceGroupValidityState = function reduceGroupValidityState(acc, curr) { if (curr !== true) acc = false; return acc; }; /** * Aggregates validation promises for all groups into a single promise * * @params groups [Object] * * @return validation results [Promise] aggregated promise * */ var getValidityState = function getValidityState(groups) { return Promise.all(Object.keys(groups).map(function (group) { return getGroupValidityState(groups[group]); })); }; /** * Aggregates all of the validation promises for a sinlge group into a single promise * * @params groups [Object] * * @return validation results [Promise] aggregated promise * */ var getGroupValidityState = function getGroupValidityState(group) { var hasError = false; return Promise.all(group.validators.map(function (validator) { return new Promise(function (resolve) { if (validator.type !== 'remote') { if (validate(group, validator)) resolve(true);else { hasError = true; resolve(false); } } else if (hasError) resolve(false);else validate(group, validator).then(function (res) { resolve(res); }); }); })); }; /** * Determines the event type to be used for real-time validation a given field based on field type * * @params input [DOM node] * * @return event type [String] * */ var resolveRealTimeValidationEvent = function resolveRealTimeValidationEvent(input) { return ['input', 'change'][Number(isCheckable(input) || isSelect(input) || isFile(input))]; }; //Track error message DOM nodes in local scope => ;_; // let errorNodes = {}; /** * Hypertext DOM factory function * * @param nodeName [String] * @param attributes [Object] * @param text [String] The innerText of the new node * * @returns node [DOM node] * */ var h = function h(nodeName, attributes, text) { var node = document.createElement(nodeName); for (var prop in attributes) { node.setAttribute(prop, attributes[prop]); } if (text !== undefined && text.length) node.appendChild(document.createTextNode(text)); return node; }; /** * Creates and appends a text node error message to a error container DOM node for a group * * @param group [Object, vaidation group] * @param msg [String] The error message * * @returns node [Text node] * */ var createErrorTextNode = function createErrorTextNode(group, msg) { var node = document.createTextNode(msg); group.serverErrorNode.classList.remove(DOTNET_CLASSNAMES.VALID); group.serverErrorNode.classList.add(DOTNET_CLASSNAMES.ERROR); return group.serverErrorNode.appendChild(node); }; /** * Removes the error message DOM node, updates .NET MVC error span classNames and deletes the * error from local errorNodes tracking object * * Signature () => groupName => state => {} * (Curried groupName for ease of use as eventListener and in whole form iteration) * * @param groupName [String, vaidation group] * @param state [Object, validation state] * */ var clearError = function clearError(groupName) { return function (state) { state.errorNodes[groupName].parentNode.removeChild(state.errorNodes[groupName]); // errorNodes[groupName].parentNode.removeChild(errorNodes[groupName]); if (state.groups[groupName].serverErrorNode) { state.groups[groupName].serverErrorNode.classList.remove(DOTNET_CLASSNAMES.ERROR); state.groups[groupName].serverErrorNode.classList.add(DOTNET_CLASSNAMES.VALID); } state.groups[groupName].fields.forEach(function (field) { field.parentNode.classList.remove('is--invalid'); field.removeAttribute('aria-invalid'); }); delete state.errorNodes[groupName]; //shouldn't be doing this here... }; }; /** * Iterates over all errorNodes in local scope to remove each error prior to re-validation * * @param state [Object, validation state] * */ var clearErrors = function clearErrors(state) { state.errorNodes && Object.keys(state.errorNodes).forEach(function (name) { clearError(name)(state); }); }; /** * Iterates over all groups to render each error post-vaidation * * @param state [Object, validation state] * */ var renderErrors = function renderErrors(state) { Object.keys(state.groups).forEach(function (groupName) { if (!state.groups[groupName].valid) renderError(groupName)(state); }); }; /** * Adds an error message to the DOM and saves it to local scope * * If .NET MVC error span is present, it is used with a appended textNode, * if not a new DOM node is created * * Signature () => groupName => state => {} * (Curried groupName for ease of use as eventListener and in whole form iteration) * * @param groupName [String, validation group] * @param state [Object, validation state] * */ var renderError = function renderError(groupName) { return function (state) { if (state.errorNodes[groupName]) clearError(groupName)(state); state.errorNodes[groupName] = state.groups[groupName].serverErrorNode ? createErrorTextNode(state.groups[groupName], state.groups[groupName].errorMessages[0]) : state.groups[groupName].fields[state.groups[groupName].fields.length - 1].parentNode.appendChild(h('span', { class: DOTNET_CLASSNAMES.ERROR }, state.groups[groupName].errorMessages[0]), state.groups[groupName].fields[state.groups[groupName].fields.length - 1]); state.groups[groupName].fields.forEach(function (field) { field.parentNode.classList.add('is--invalid'); field.setAttribute('aria-invalid', 'true'); }); }; }; /** * Set focus on first invalid field after form-level validate() * * We can assume that there is a group in an invalid state, * and that the group has at least one field * * @param groups [Object, validation group slice of state] * */ var focusFirstInvalidField = function focusFirstInvalidField(state) { var firstInvalid = Object.keys(state.groups).reduce(function (acc, curr) { if (!acc && !state.groups[curr].valid) acc = state.groups[curr].fields[0]; return acc; }, false); firstInvalid && firstInvalid.focus(); }; /** * Creates a hidden field duplicate of a given field, for conferring submit button values * * @param source [Node] A submit input/button * @param form [Node] A form node * */ var createButtonValueNode = function createButtonValueNode(source, form) { var node = document.createElement('input'); node.setAttribute('type', 'hidden'); node.setAttribute('name', source.getAttribute('name')); node.setAttribute('value', source.getAttribute('value')); return form.appendChild(node); }; /** * Removes the node added in createButtonValueNode * * @param node [Node] A hidden input * */ var cleanupButtonValueNode = function cleanupButtonValueNode(node) { node.parentNode.removeChild(node); }; /** * Returns a function that extracts the validityState of the entire form * can be used as a form submit eventListener or via the API * * Submits the form if called as a submit eventListener and is valid * Dispatches error state to Store if errors * * @param form [DOM node] * * @returns [boolean] The validity state of the form * */ var validate$1 = function validate$1(Store) { return function (e) { e && e.preventDefault(); Store.dispatch(ACTIONS.CLEAR_ERRORS, null, [clearErrors]); getValidityState(Store.getState().groups).then(function (validityState) { var _ref2; if ((_ref2 = []).concat.apply(_ref2, _toConsumableArray(validityState)).reduce(reduceGroupValidityState, true)) { var buttonValueNode = false; if (isSubmitButton(document.activeElement) && hasNameValue(document.activeElement)) { buttonValueNode = createButtonValueNode(document.activeElement, Store.getState().form); } if (e && e.target) Store.getState().form.submit(); buttonValueNode && cleanupButtonValueNode(buttonValueNode); return true; } Store.getState().realTimeValidation === false && startRealTimeValidation(Store); Store.dispatch(ACTIONS.VALIDATION_ERRORS, Object.keys(Store.getState().groups).reduce(function (acc, group, i) { return acc[group] = { valid: validityState[i].reduce(reduceGroupValidityState, true), errorMessages: validityState[i].reduce(reduceErrorMessages(group, Store.getState()), []) }, acc; }, {}), [renderErrors, focusFirstInvalidField]); return false; }); }; }; /** * Adds a custom validation method to the validation model, used via the API * Dispatches add validation method to store to update the validators in a group * * @param groupName [String] The name attribute shared by the DOm nodes in the group * @param method [Function] The validation method (function that returns true or false) that us called on the group * @param message [String] Te error message displayed if the validation method returns false * */ var addMethod = function addMethod(groupName, method, message) { if (groupName === undefined || method === undefined || message === undefined || !Store.getState()[groupName] && document.getElementsByName(groupName).length === 0) return console.warn('Custom validation method cannot be added.'); Store.dispatch(ACTIONS.ADD_VALIDATION_METHOD, { groupName: groupName, validator: { type: 'custom', method: method, message: message } }); }; /** * Starts real-time validation on each group, adding an eventListener to each field * that resets the validityState for the field's group and acquires the new validity state * * The event that triggers validation is defined by the field type * * Only if the new validityState is invalid is the validation error object * dispatched to the store to update state and render the error * */ var startRealTimeValidation = function startRealTimeValidation(Store) { var handler = function handler(groupName) { return function () { if (!Store.getState().groups[groupName].valid) { Store.dispatch(ACTIONS.CLEAR_ERROR, groupName, [clearError(groupName)]); } getGroupValidityState(Store.getState().groups[groupName]).then(function (res) { if (!res.reduce(reduceGroupValidityState, true)) { Store.dispatch(ACTIONS.VALIDATION_ERROR, { group: groupName, errorMessages: res.reduce(reduceErrorMessages(groupName, Store.getState()), []) }, [renderError(groupName)]); } }); }; }; Object.keys(Store.getState().groups).forEach(function (groupName) { Store.getState().groups[groupName].fields.forEach(function (input) { input.addEventListener(resolveRealTimeValidationEvent(input), handler(groupName)); }); //;_; can do better? var equalToValidator = Store.getState().groups[groupName].validators.filter(function (validator) { return validator.type === 'equalto'; }); if (equalToValidator.length > 0) { equalToValidator[0].params.other.forEach(function (subgroup) { subgroup.forEach(function (item) { item.addEventListener('blur', handler(groupName)); }); }); } }); }; /** * Default function, sets initial state and adds form-level event listeners * * @param form [DOM node] the form to validate * * @returns [Object] The API for the instance * * */ var factory = function factory(form) { var Store = createStore(); Store.dispatch(ACTIONS.SET_INITIAL_STATE, getInitialState(form)); form.addEventListener('submit', validate$1(Store)); form.addEventListener('reset', function () { Store.update(UPDATES.CLEAR_ERRORS, null, [clearErrors]); }); return { getState: Store.getState, validate: validate$1(Store), addMethod: addMethod }; }; var init = function init(candidate, opts) { var els = void 0; //if we think candidate is a form DOM node, pass it in an Array //otherwise convert candidate to an array of Nodes using it as a DOM query if (typeof candidate !== 'string' && candidate.nodeName && candidate.nodeName === 'FORM') els = [candidate];else els = [].slice.call(document.querySelectorAll(candidate)); // if(els.length === 1 && window.__validators__ && window.__validators__[els[0]]) return window.__validators__[els[0]]; //return instance if one exists for candidate passed to init //if inititialised using StormVaidation.init({sel}) the instance already exists thanks to auto-init //but reference may be wanted for invoking API methods //also for repeat initialisations return els.reduce(function (acc, el) { if (!el.hasAttribute('novalidate')) { acc.push(Object.create(factory(el, opts))); el.setAttribute('novalidate', 'novalidate'); } return acc; }, []); }; var index = { init: init }; exports.default = index;; }));