storm-validate
Version:
[](https://badge.fury.io/js/storm-validate)
983 lines (885 loc) • 37.5 kB
JavaScript
/**
* @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;;
}));