UNPKG

@stormid/validate

Version:
1 lines 71.2 kB
{"version":3,"file":"index.modern.mjs","sources":["../src/lib/store/index.js","../src/lib/constants/index.js","../src/lib/reducers/index.js","../src/lib/validator/utils.js","../src/lib/validator/methods.js","../src/lib/validator/index.js","../src/lib/dom/index.js","../src/lib/validator/real-time-validation.js","../src/lib/factory/validate.js","../src/lib/validator/post-validation.js","../src/lib/factory/add-method.js","../src/lib/factory/group.js","../src/lib/factory/index.js","../src/lib/defaults/index.js","../src/index.js"],"sourcesContent":["export const createStore = () => {\n let state = {};\n \n const getState = () => state;\n\n const update = (nextState, effects) => {\n state = nextState ?? state;\n if (!effects) return;\n effects.forEach(effect => effect(state));\n };\n \n return { update, getState };\n};","export const PREHOOK_DELAY = 16;\n\nexport const ACTIONS = {\n SET_INITIAL_STATE: 'SET_INITIAL_STATE',\n CLEAR_ERRORS: 'CLEAR_ERRORS',\n VALIDATION_ERRORS: 'VALIDATION_ERRORS',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n CLEAR_ERROR: 'CLEAR_ERROR',\n ADD_VALIDATION_METHOD: 'ADD_VALIDATION_METHOD',\n ADD_GROUP: 'ADD_GROUP',\n REMOVE_GROUP: 'REMOVE_GROUP',\n START_REALTIME: 'START_REALTIME'\n};\n\n//https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address\nexport const EMAIL_REGEX = /^[A-Za-zŽžÀ-ÿŠ0-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])?)*$/;\n\n//https://mathiasbynens.be/demo/url-regex\nexport const 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;\n\nexport const DATE_ISO_REGEX = /^\\d{4}[/-](0?[1-9]|1[012])[/-](0?[1-9]|[12][0-9]|3[01])$/;\n\nexport const NUMBER_REGEX = /^(?:-?\\d+|-?\\d{1,3}(?:,\\d{3})+)?(?:\\.\\d+)?$/;\n\nexport const DIGITS_REGEX = /^\\d+$/;\n\n//data-attribute added to error message span created by .NET MVC\nexport const DOTNET_ERROR_SPAN_DATA_ATTRIBUTE = 'data-valmsg-for';\n\n//validator parameters that require DOM lookup\nexport const DOM_SELECTOR_PARAMS = ['remote-additionalfields', 'equalto-other'];\n\n//.NET MVC validator data-attribute parameters indexed by their validators\n//e.g. <input data-val-length=\"Error messge\" data-val-length-min=\"8\" data-val-length-max=\"10\" type=\"text\"... />\nexport const DOTNET_PARAMS = {\n length: ['length-min', 'length-max'],\n stringlength: ['length-max'],\n range: ['range-min', 'range-max'],\n min: ['min-min'],\n max: ['max-max'],\n minlength: ['minlength-min'],\n maxlength: ['maxlength-max'],\n regex: ['regex-pattern'],\n equalto: ['equalto-other'],\n remote: ['remote-url', 'remote-additionalfields', 'remote-type']\n};\n\n//.NET MVC data-attributes that identify validators\nexport const DOTNET_ADAPTORS = [\n 'required',\n 'stringlength',\n 'dateISO',\n 'regex',\n 'digits',\n 'email',\n 'number',\n 'url',\n 'length',\n 'min',\n 'max',\n 'minlength',\n 'maxlength',\n 'range',\n 'equalto',\n // 'password' //-> map to min, nonalphamain, and regex methods\n 'remote'//should be last\n];\n\n//classNames added/updated on .NET MVC error message span\nexport const DOTNET_CLASSNAMES = {\n VALID: 'field-validation-valid',\n ERROR: 'error-message'\n};\n\nexport const GROUP_ATTRIBUTE = 'group';\n\nexport const TOKENS = {\n VALUE: '{{value}}'\n};","import { ACTIONS, DOTNET_ERROR_SPAN_DATA_ATTRIBUTE, GROUP_ATTRIBUTE } from '../constants';\n\nexport default {\n [ACTIONS.SET_INITIAL_STATE]: (state, data) => Object.assign({}, state, data),\n [ACTIONS.CLEAR_ERRORS]: state => Object.assign({}, state, {\n groups: Object.keys(state.groups).reduce((acc, group) => {\n acc[group] = Object.assign({}, state.groups[group], {\n errorMessages: [],\n valid: true\n });\n return acc;\n }, {})\n }),\n [ACTIONS.CLEAR_ERROR]: (state, data) => {\n const nextGroup = {};\n nextGroup[data] = Object.assign({}, state.groups[data], {\n errorMessages: [],\n valid: true\n });\n return Object.assign({}, state, {\n groups: Object.assign({}, state.groups, nextGroup)\n });\n },\n [ACTIONS.ADD_GROUP]: (state, groups, errors) => Object.assign({}, state, {\n groups: Object.assign({}, state.groups, groups),\n errors: Object.assign({}, state.errors, errors)\n }),\n [ACTIONS.REMOVE_GROUP]: (state, groupName) => Object.assign({}, state, {\n groups: Object.keys(state.groups).reduce((acc, group) => {\n if (group !== groupName) acc[group] = state.groups[group];\n return acc;\n }, {}),\n ...(state.errors !== undefined ? {\n errors: Object.keys(state.errors).reduce((acc, error) => {\n if (error !== groupName) acc[error] = state.errors[error];\n return acc;\n }, {})\n } : {})\n }),\n [ACTIONS.ADD_VALIDATION_METHOD]: (state, data) => {\n const nextGroup = Object.assign({},\n state.groups[data.groupName]\n ? state.groups[data.groupName]\n : {},\n state.groups[data.groupName]\n ? { validators: [...state.groups[data.groupName].validators, data.validator] }\n : {\n fields: data.fields || (document.querySelector(`[data-val-${GROUP_ATTRIBUTE}=\"${data.groupName}\"]`) ? [].slice.call(document.querySelectorAll(`[data-val-${GROUP_ATTRIBUTE}=\"${data.groupName}\"]`)) : [].slice.call(document.getElementsByName(data.groupName))),\n serverErrorNode: document.querySelector(`[${DOTNET_ERROR_SPAN_DATA_ATTRIBUTE}=\"${data.groupName}\"]`) || false,\n valid: false,\n validators: [data.validator],\n });\n\n return Object.assign({}, state, {\n groups: Object.assign({}, state.groups, { [data.groupName]: nextGroup })\n });\n },\n [ACTIONS.VALIDATION_ERRORS]: (state, data) => Object.assign({}, state, {\n realTimeValidation: true,\n groups: Object.keys(state.groups).reduce((acc, group) => {\n acc[group] = Object.assign({}, state.groups[group], data[group]);\n return acc;\n }, {})\n }),\n [ACTIONS.VALIDATION_ERROR]: (state, data) => {\n return Object.assign({}, state, {\n groups: Object.assign({}, state.groups, {\n [data.group]: Object.assign({}, state.groups[data.group], {\n errorMessages: data.errorMessages,\n valid: false\n })\n })\n });\n },\n [ACTIONS.START_REALTIME]: (state, data) => Object.assign({}, state, data),\n};\n\n ","export const isCheckable = field => (/radio|checkbox/i).test(field.type);\n\nexport const isFile = field => field.getAttribute('type') === 'file';\n\nexport const isHidden = field => field.getAttribute('type') === 'hidden';\n\nexport const isSelect = field => field.nodeName.toLowerCase() === 'select';\n\nexport const isSubmitButton = node => node.getAttribute('type') === 'submit' || node.nodeName === 'BUTTON';\n\nexport const hasNameValue = node => node.hasAttribute('name') && node.hasAttribute('value');\n\nexport const hasFormactionValue = node => node.hasAttribute('formaction') && node.getAttribute('formaction') !== '';\n\nexport const isRequired = group => group.validators.filter(validator => validator.type === 'required').length > 0;\n\nexport const groupIsAllHidden = fields => fields.reduce((acc, field) => {\n if (field.type !== 'hidden') acc = false;\n return acc;\n}, true);\n\nexport const groupIsDisabled = fields => fields.reduce((acc, field) => {\n if (field.hasAttribute('disabled') && field.getAttribute('disabled') !== \"false\") acc = true;\n return acc;\n}, false);\n\nexport const hasValue = input => (input.value !== undefined && input.value !== null && input.value.length > 0);\n\nexport const groupValueReducer = (acc, input) => {\n if (!isCheckable(input) && !isHidden(input) && hasValue(input)) acc = input.value.trim();\n if (isCheckable(input) && input.checked) {\n if (Array.isArray(acc)) acc.push(input.value.trim());\n else acc = [input.value.trim()];\n }\n return acc;\n};\n\nexport const resolveGetParams = nodeArrays => nodeArrays.map(nodes => `${encodeURIComponent(nodes[0].getAttribute('name'))}=${encodeURIComponent(extractValueFromGroup(nodes))}`).join('&');\n\nexport const domNodesFromCommaList = list => list.split(',')\n .map(item => {\n // const resolvedSelector = escapeAttributeValue(appendStatePrefix(item, getStatePrefix(input.getAttribute('name'))));\n return [].slice.call(document.querySelectorAll(`[name=${escapeAttributeValue(item)}]`));\n });\n\nexport const escapeAttributeValue = value => value.replace(/([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^`{|}~])/g, '\\\\$1');\n\n/*\n * Only require below functions and resolvedSelector in domNodesFromCommaList if supporting *. params\n */\n// const getStatePrefix = fieldName => fieldName.substr(0, fieldName.lastIndexOf('.') + 1);\n\n// const appendStatePrefix = (value, prefix) => {\n// if (value.indexOf(\"*.\") === 0) value = value.replace(\"*.\", prefix);\n// return value;\n// };\n\nexport const extractValueFromGroup = group => Object.prototype.hasOwnProperty.call(group, 'fields')\n ? group.fields.reduce(groupValueReducer, '')\n : group.reduce(groupValueReducer, '');\n\n\n/* istanbul ignore next */\nexport const fetch = (url, props) =>\n new Promise((resolve, reject) => {\n let xhr = new XMLHttpRequest();\n xhr.open(props.method || 'GET', url);\n if (props.headers) {\n Object.keys(props.headers).forEach(key => {\n xhr.setRequestHeader(key, props.headers[key]);\n });\n }\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);\n else reject(xhr.statusText);\n };\n xhr.onerror = () => reject(xhr.statusText);\n xhr.send(props.body);\n });\n\nexport const findErrors = groups => Object.keys(groups).reduce((errors, groupName) => {\n if (groups[groupName].serverErrorNode){\n const serverErrorText = groups[groupName].serverErrorNode.textContent;\n if (serverErrorText) {\n errors[groupName] = serverErrorText;\n }\n }\n return errors;\n}, {});\n\n\n/*\n * Converts a passed selector which can be of varying types into an array of DOM Objects\n *\n * @param selector, Can be a string, Array of DOM nodes, a NodeList or a single DOM element.\n */\nexport const getSelection = selector => {\n\n if (typeof selector === 'string') return [].slice.call(document.querySelectorAll(selector));\n if (selector instanceof Array) return selector;\n if (Object.prototype.isPrototypeOf.call(NodeList.prototype, selector)) return [].slice.call(selector);\n if (selector instanceof HTMLElement) return [selector];\n return [];\n};","import {\n EMAIL_REGEX,\n URL_REGEX,\n DATE_ISO_REGEX,\n NUMBER_REGEX,\n DIGITS_REGEX\n} from '../constants';\nimport {\n fetch,\n isRequired,\n extractValueFromGroup,\n resolveGetParams\n} from './utils';\n\nconst isOptional = group => !isRequired(group) && extractValueFromGroup(group) === '';\n\nconst extractValidationParams = (group, type) => group.validators.filter(validator => validator.type === type)[0].params;\n\nconst regexMethod = regex => group => isOptional(group)|| group.fields.reduce((acc, input) => (acc = regex.test(input.value), acc), false);\n\nconst paramMethod = (type, reducer) => group => isOptional(group) || group.fields.reduce(reducer(extractValidationParams(group, type)), false);\n\nconst shouldValidateByParam = param => param !== undefined;\n\nexport default {\n required: group => extractValueFromGroup(group) !== '',\n email: regexMethod(EMAIL_REGEX),\n url: regexMethod(URL_REGEX),\n dateISO: regexMethod(DATE_ISO_REGEX),\n number: regexMethod(NUMBER_REGEX),\n digits: regexMethod(DIGITS_REGEX),\n minlength: paramMethod(\n 'minlength',\n params => (acc, input) => (acc = +input.value.length >= +params.min, acc)\n ),\n maxlength: paramMethod(\n 'maxlength',\n params => (acc, input) => (acc = +input.value.length <= +params.max, acc)\n ),\n equalto: paramMethod('equalto', params => (acc, input) => (acc = params.other.reduce((subgroupAcc, subgroup) => {\n if (extractValueFromGroup(subgroup) !== input.value) subgroupAcc = false;\n return subgroupAcc;\n }, true), acc)),\n pattern: paramMethod('pattern', params => (acc, input) => (acc = RegExp(params.regex).test(input.value), acc)),\n regex: paramMethod('regex', params => (acc, input) => (acc = RegExp(params.pattern).test(input.value), acc)),\n min: paramMethod('min', params => (acc, input) => (acc = !isNaN(parseInt(input.value, 10)) && +input.value >= +params.min, acc)),\n max: paramMethod('max', params => (acc, input) => (acc = !isNaN(parseInt(input.value, 10)) && +input.value <= +params.max, acc)),\n stringlength: paramMethod('stringlength', params => (acc, input) => (acc = +input.value.length <= +params.max, acc)),\n length: paramMethod('length', params => (acc, input) => (acc = (+input.value.length >= +params.min && (params.max === undefined || +input.value.length <= +params.max)), acc)),\n range: paramMethod('range', params => (acc, input) => (acc = ((!shouldValidateByParam(params.min) || +input.value >= +params.min) && (!shouldValidateByParam(params.max) || +input.value <= +params.max)), acc)),\n remote: (group, params) => new Promise((resolve, reject) => {\n const value = extractValueFromGroup(group);\n fetch((params.type !== 'get' ? params.url : `${params.url}?${group.fields[0].name}=${value}&${resolveGetParams(params.additionalfields)}`), {\n method: params.type && params.type.toUpperCase() || 'POST',\n body: params.type !== 'get'\n ? JSON.stringify({ [group.fields[0].name]: value })\n : resolveGetParams(params.additionalfields),\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'\n }\n })\n .then(data => resolve(data));\n }),\n custom: (method, group) => isOptional(group) || method(extractValueFromGroup(group), group.fields)\n};","import methods from './methods';\nimport {\n isCheckable,\n isSelect,\n isFile,\n isHidden,\n domNodesFromCommaList,\n groupIsDisabled,\n findErrors,\n groupIsAllHidden\n} from './utils';\nimport {\n DOTNET_ADAPTORS,\n DOTNET_PARAMS,\n DOTNET_ERROR_SPAN_DATA_ATTRIBUTE,\n DOM_SELECTOR_PARAMS,\n GROUP_ATTRIBUTE\n} from '../constants';\n\n/**\n * Resolve validation parameter to a string or array of DOM nodes\n * \n * @param param [String] identifier for the data-attribute `data-val-${param}`\n * @param input [DOM node] the node which contains the data-val- attribute\n * \n * @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\n */\nexport const resolveParam = (param, input) => {\n let value = input.getAttribute(`data-val-${param}`);\n return ({\n [param.split('-')[1]]: ~DOM_SELECTOR_PARAMS.indexOf(param) ? domNodesFromCommaList(value, input): value\n });\n};\n\n/**\n * Looks up the data-val property against the known .NET MVC adaptors/validation method\n * runs the matches against the node to find param values, and returns an Object containing all parameters for that adaptor/validation method\n * \n * @param input [DOM node] the node on which to look for matching adaptors\n * @param adaptor [String] .NET MVC data-attribute that identifies validator\n * \n * @return validation params [Object] Validation param object containing all validation parameters for an adaptor/validation method\n */\nexport const extractParams = (input, adaptor) => DOTNET_PARAMS[adaptor]\n ? {\n params: DOTNET_PARAMS[adaptor]\n .reduce((acc, param) => input.hasAttribute(`data-val-${param}`) ? Object.assign(acc, resolveParam(param, input)) : acc, {})\n }\n : false;\n\n/**\n * Reducer that takes all known .NET MVC adaptors (data-attributes that specify a validation method that should be applied to the node)\n * and checks against a DOM node for matches, returning an array of validators\n * \n * @param input [DOM node]\n * \n * @return validators [Array], each validator compposed of \n * type [String] naming the validator and matching it to validation method function\n * message [String] the error message displayed if the validation method returns false\n * params [Object] (optional) \n */\nexport const extractDataValValidators = input => DOTNET_ADAPTORS.reduce((validators, adaptor) =>\n !input.getAttribute(`data-val-${adaptor}`)\n ? validators\n : [...validators,\n Object.assign({\n type: adaptor,\n message: input.getAttribute(`data-val-${adaptor}`) },\n extractParams(input, adaptor)\n )\n ],\n[]);\n\n/**\n * Looks for a data-val attribute matching the validator type and returns the value\n * \n * @param input [DOM node]\n * @param type [String]\n * \n * @return [Object] Error message or empty\n */\nconst resolveMessages = (input, type) => input.getAttribute(`data-val-${type}`) ? { message: input.getAttribute(`data-val-${type}`) } : {};\n\n/**\n * Checks attributes on an input to generate an array of validators the attributes describe\n * \n * @param input [DOM node]\n * \n * @return validators [Array]\n */\nexport const extractAttrValidators = input => {\n let validators = [];\n if ((input.hasAttribute('required') || input.hasAttribute('aria-required')) && (input.getAttribute('required') !== 'false' || input.getAttribute('aria-required') !== 'false')){\n validators.push({ type: 'required', ...resolveMessages(input, 'required') } );\n }\n if (input.getAttribute('type') === 'email') validators.push({ type: 'email', ...resolveMessages(input, 'email') });\n if (input.getAttribute('type') === 'url') validators.push({ type: 'url', ...resolveMessages(input, 'url') });\n if (input.getAttribute('type') === 'number') validators.push({ type: 'number', ...resolveMessages(input, 'number') });\n if ((input.getAttribute('minlength') && input.getAttribute('minlength') !== 'false')){\n validators.push({ type: 'minlength', params: { min: input.getAttribute('minlength') }, ...resolveMessages(input, 'minlength') });\n }\n if ((input.getAttribute('maxlength') && input.getAttribute('maxlength') !== 'false')){\n validators.push({ type: 'maxlength', params: { max: input.getAttribute('maxlength') }, ...resolveMessages(input, 'maxlength') });\n }\n if ((input.getAttribute('min') && input.getAttribute('min') !== 'false')){\n validators.push({ type: 'min', params: { min: input.getAttribute('min') }, ...resolveMessages(input, 'min') });\n }\n if ((input.getAttribute('max') && input.getAttribute('max') !== 'false')){\n validators.push({ type: 'max', params: { max: input.getAttribute('max') }, ...resolveMessages(input, 'max') });\n }\n if ((input.getAttribute('pattern') && input.getAttribute('pattern') !== 'false')){\n validators.push({ type: 'pattern', params: { regex: input.getAttribute('pattern') }, ...resolveMessages(input, 'pattern') });\n }\n return validators;\n};\n\n/**\n * Validator checks to extract validators based on HTML5 attributes\n * \n * Each function is so we can seed each fn with an input and pipe the result array through each function\n * Signature: inputDOMNode => validatorArray => updateValidatorArray\n\nconst required = input => (validators = []) => {\n // console.log(validators);\n return input.hasAttribute('required') && input.getAttribute('required') !== 'false' ? [...validators, {type: 'required'}] : validators;\n};\nconst email = input => (validators = []) => input.getAttribute('type') === 'email' ? [...validators, {type: 'email'}] : validators;\nconst url = input => (validators = []) => input.getAttribute('type') === 'url' ? [...validators, {type: 'url'}] : validators;\nconst number = input => (validators = []) => input.getAttribute('type') === 'number' ? [...validators, {type: 'number'}] : validators;\nconst minlength = input => (validators = []) => (input.getAttribute('minlength') && input.getAttribute('minlength') !== 'false') ? [...validators, {type: 'minlength', params: { min: input.getAttribute('minlength')}}] : validators;\nconst maxlength = input => (validators = []) => (input.getAttribute('maxlength') && input.getAttribute('maxlength') !== 'false') ? [...validators, {type: 'maxlength', params: { max: input.getAttribute('maxlength')}}] : validators;\nconst min = input => (validators = []) => (input.getAttribute('min') && input.getAttribute('min') !== 'false') ? [...validators, {type: 'min', params: { min: input.getAttribute('min')}}] : validators;\nconst max = input => (validators = []) => (input.getAttribute('max') && input.getAttribute('max') !== 'false') ? [...validators, {type: 'max', params: { max: input.getAttribute('max')}}] : validators;\nconst pattern = input => (validators = []) => (input.getAttribute('pattern') && input.getAttribute('pattern') !== 'false') ? [...validators, {type: 'pattern', params: { regex: input.getAttribute('pattern')}}] : validators;\n */\n\n/**\n * Takes an input and returns the array of validators based on either .NET MVC data-val- or HTML5 attributes\n * \n * @param input [DOM node]\n * \n * @return validators [Array]\n */\nexport const normaliseValidators = input => input.getAttribute('data-val') === 'true'\n ? extractDataValValidators(input)\n : extractAttrValidators(input);\n\n/**\n * Calls a validation method against an input group\n * \n * @param group [Array] DOM nodes with the same name attribute\n * @param validator [String] The type of validator matching it to validation method function\n * \n * @returns validityState [Promise]\n * \n */\nexport const validate = (group, validator) => new Promise((resolve, reject) => {\n try {\n validator.type === 'custom'\n ? resolve(methods.custom(validator.method, group))\n : resolve(methods[validator.type](group, validator.params));\n } catch (err) {\n console.warn(err);\n resolve(err);\n }\n});\n\n/**\n * Reducer constructing an validation Object for a group of DOM nodes\n * \n * @param input [DOM node]\n * \n * @returns validation object [Object] consisting of\n * valid [Boolean] the validityState of the group\n * validators [Array] of validator objects\n * fields [Array] DOM nodes in the group\n * serverErrorNode [DOM node] .NET MVC server-rendered error message span\n * \n */\nexport const assembleValidationGroup = (acc, input) => {\n let name = (input.getAttribute('data-val-'+GROUP_ATTRIBUTE)) ? input.getAttribute('data-val-'+GROUP_ATTRIBUTE) : input.getAttribute('name') ;\n if (!name) return console.warn('Missing data group or name attribute'), acc;\n\n if (acc[name] && isHidden(input)) return acc;\n\n const serverErrorNode = document.querySelector(`[${DOTNET_ERROR_SPAN_DATA_ATTRIBUTE}=\"${name}\"]`) || false;\n\n return acc[name] = acc[name] ? Object.assign(acc[name], { fields: [...acc[name].fields, input] })\n : {\n valid: false,\n validators: normaliseValidators(input),\n fields: [input],\n serverErrorNode\n }, acc;\n};\n\n/**\n * Returns an error message property of the validator Object that has returned false or the corresponding default message\n * \n * @param validator [Object] \n * \n * @return message [String] error message\n * \n */\nexport const extractErrorMessage = (messages, validator) => validator.message || messages[validator.type](validator.params !== undefined ? validator.params : null);\n\n/**\n * Returns a reducer that reduces the resolved response from an array of validation Promises performed against a group\n * into an array of error messages or an empty array\n * \n * @return error messages [Array]\n * \n */\nexport const reduceErrorMessages = (group, state) => (acc, validity, j) => validity === true\n ? acc\n : [...acc, typeof validity === 'boolean'\n ? extractErrorMessage(state.settings.messages, state.groups[group].validators[j])\n : validity];\n\n/**\n * From all groups found in the current form, thosethat do not require validation (have no assocated validators) are removed\n * \n * @param groups [Object] name-indexed object consisting of all groups found in the current form\n * \n * @return groups [Object] name-indexed object consisting of all validatable groups\n * \n */\nexport const removeUnvalidatableGroups = groups => {\n let validationGroups = {};\n\n for (let group in groups){\n if (groups[group].validators.length > 0 && !groupIsAllHidden(groups[group].fields)){\n validationGroups[group] = groups[group];\n }\n }\n return validationGroups;\n};\n\n/**\n * Takes a form DOM node and returns the initial form validation state - an object consisting of all the validatable input groups\n * with validityState, fields, validators, and associated data required to perform validation and render errors.\n * \n * @param form [DOM nodes] \n * \n * @return state [Object] consisting of groups [Object] name-indexed validation groups\n * \n */\nexport const getInitialState = (form, settings) => {\n const groups = removeUnvalidatableGroups([].slice.call(form.querySelectorAll('input:not([type=submit]), textarea, select'))\n .reduce(assembleValidationGroup, {}));\n return {\n form,\n settings,\n errors: findErrors(groups),\n realTimeValidation: false,\n groups\n };\n};\n\n/**\n * Reducer run against an array of resolved validation promises to set the overall validityState of a group\n * \n * @return validityState [Boolean] \n * \n */\nexport const reduceGroupValidityState = (acc, curr) => {\n if (curr !== true) acc = false;\n return acc;\n};\n\n\nexport const isFormValid = validityState => [].concat(...validityState).reduce(reduceGroupValidityState, true);\n\n/**\n * Aggregates validation promises for all groups into a single promise\n * \n * @params groups [Object]\n * \n * @return validation results [Promise] aggregated promise\n * \n */\nexport const getValidityState = groups => Promise.all(\n Object.keys(groups)\n .map(group => getGroupValidityState(groups[group]))\n);\n\n/**\n * Aggregates all of the validation promises for a single group into a single promise\n * \n * @params groups [Object]\n * \n * @return validation results [Promises] aggregated promise\n * \n */\nexport const getGroupValidityState = group => {\n //check if group is disabled\n if (groupIsDisabled(group.fields)) return Promise.resolve([true]);\n return Promise.all(group.validators.map(validator => new Promise((resolve, reject) => {\n validate(group, validator)\n .then(res => {\n if (String(res) !== 'true') resolve(String(res) === 'false' ? false : res);\n else resolve(true);\n })\n .catch(err => console.warn(err));\n })));\n};\n\n/**\n * Determines the event type to be used for real-time validation a given field based on field type\n * \n * @params input [DOM node]\n * \n * @return event type [String]\n * \n */\nexport const resolveRealTimeValidationEvent = input => ['input', 'change'][Number(isCheckable(input) || isSelect(input) || isFile(input))];","import { DOTNET_CLASSNAMES, TOKENS } from '../constants';\n\n/**\n * Hypertext DOM factory function\n * \n * @param nodeName [String]\n * @param attributes [Object]\n * @param text [String] The innerText of the new node\n * \n * @returns node [DOM node]\n * \n */\nexport const h = (nodeName, attributes, text) => {\n let node = document.createElement(nodeName);\n\n for (let prop in attributes) {\n node.setAttribute(prop, attributes[prop]);\n }\n if (text !== undefined && text.length) node.appendChild(document.createTextNode(text));\n\n return node;\n};\n\n/**\n * Creates and appends a text node error message to a error container DOM node for a group\n * \n * @param group [Object, vaidation group] \n * @param msg [String] The error message\n * \n * @returns node [Text node]\n * \n */\nexport const createErrorTextNode = (group, msg) => {\n\n let node = document.createTextNode(msg);\n group.serverErrorNode.classList.remove(DOTNET_CLASSNAMES.VALID);\n group.serverErrorNode.classList.add(DOTNET_CLASSNAMES.ERROR);\n \n return group.serverErrorNode.appendChild(node);\n};\n\n/**\n * Removes the error message, updates .NET MVC error span classNames and deletes the \n * error from local errors tracking object\n * \n * Signature () => groupName => state => {}\n * (groupName for ease of use as eventListener and in whole form iteration)\n * \n * @param groupName [String, vaidation group] \n * @param state [Object, validation state]\n * \n */\nexport const clearError = groupName => state => {\n if (state.groups[groupName].serverErrorNode) {\n state.groups[groupName].serverErrorNode.innerHTML = '';\n state.groups[groupName].serverErrorNode.classList.remove(DOTNET_CLASSNAMES.ERROR);\n state.groups[groupName].serverErrorNode.classList.add(DOTNET_CLASSNAMES.VALID);\n } else {\n state.errors[groupName].parentNode.removeChild(state.errors[groupName]);\n }\n state.groups[groupName].fields.forEach(field => {\n field.parentNode.classList.remove('is--invalid');\n field.removeAttribute('aria-invalid');\n const describedbyid = ((state.groups[groupName].serverErrorNode || state.errors[groupName]).id);\n\n //check whether the aria-describedby matches the id, if not another id must be present, only replace the removed error id\n if (field.hasAttribute('aria-describedby')) {\n if (field.getAttribute('aria-describedby') === describedbyid) field.removeAttribute('aria-describedby');\n else field.setAttribute('aria-describedby', field.getAttribute('aria-describedby').replace(` ${describedbyid}`, ''));\n }\n });\n delete state.errors[groupName];//shouldn't be doing this here...\n};\n\n/**\n * Iterates over all errors in local scope to remove each error prior to re-validation\n * \n * @param state [Object, validation state]\n * \n */\nexport const clearErrors = state => {\n state.errors && Object.keys(state.errors).forEach(name => {\n clearError(name)(state);\n });\n};\n\n/**\n * Iterates over all groups to render each error post-vaidation\n * \n * @param state [Object, validation state]\n * \n */\nexport const renderErrors = state => {\n Object.keys(state.groups).forEach(groupName => {\n if (!state.groups[groupName].valid) renderError(groupName)(state);\n });\n};\n\n\n/**\n * Looks for any value tokens and replaces them within the error message\n * \n * @param state [Object, validation state]\n * @param groupName [String, validation group] \n * \n */\nexport const updateMessageValues = (state, groupName) => {\n let msg = state.groups[groupName].errorMessages[0];\n\n let values = state.groups[groupName].fields.reduce((newMsg, field, index, array) => {\n if (index === array.length-1) return newMsg + field.value;\n return newMsg = field.value + ', ';\n }, '');\n\n return msg.replace(TOKENS.VALUE, values);\n};\n\n\n/**\n * Adds an error message to the DOM and saves it to local scope\n * \n * If .NET MVC error span is present, it is used with a appended textNode,\n * if not a new DOM node is created\n * \n * Signature () => groupName => state => {}\n * (groupName for ease of use as eventListener and in whole form iteration)\n * \n * @param groupName [String, validation group] \n * @param state [Object, validation state]\n * \n */\nexport const renderError = groupName => state => {\n if (state.errors[groupName]) clearError(groupName)(state);\n\n let msg = updateMessageValues(state, groupName);\n\n //shouldn't be updating state here...\n //to do: refactor to update state as a side effect afterwards?\n //would need to pass store instead of state\n if (state.groups[groupName].serverErrorNode) {\n state.errors[groupName] = createErrorTextNode(state.groups[groupName], msg);\n } else {\n //No server error node found, so attempt to render inside the label. If no label found, log error to console.\n const label = document.querySelector(`[for=\"${state.groups[groupName].fields[state.groups[groupName].fields.length-1].getAttribute('id')}\"]`);\n\n if (label !== null) {\n state.errors[groupName] = label.parentNode.insertBefore(h('span', { class: DOTNET_CLASSNAMES.ERROR, id: `${groupName}-error-message` }, msg), label.nextSibling);\n } else {\n console.error(`No matching HTML label or server error node found for validation group: ${groupName}. Error message: '${msg}' cannot be displayed. Form will not be submitted.`);\n return;\n }\n }\n\n const errorContainer = state.groups[groupName].serverErrorNode || state.errors[groupName];\n\t\t\t\t\t\t\n state.groups[groupName].fields.forEach(field => {\n field.parentNode.classList.add('is--invalid');\n field.setAttribute('aria-invalid', 'true');\n if (!field.hasAttribute('aria-describedby') || !hasAriaDescribedbyValue(field, errorContainer.getAttribute('id'))) {\n field.setAttribute('aria-describedby', (field.hasAttribute('aria-describedby')\n ? `${field.getAttribute('aria-describedby')} ${errorContainer.getAttribute('id')}`\n : errorContainer.getAttribute('id'))\n );\n }\n });\n};\n\n\nexport const hasAriaDescribedbyValue = (field, value) => {\n const describedby = field.getAttribute('aria-describedby').split(' ');\n return describedby.length > 0\n && describedby.reduce((acc, curr) => (acc || curr === value), false);\n};\n\n\n/**\n * Set focus on first invalid field after form-level validate()\n * \n * We can assume that there is a group in an invalid state,\n * and that the group has at least one field\n * \n * @param groups [Object, validation group slice of state]\n * \n */\nexport const focusFirstInvalidField = state => {\n const firstInvalid = Object.keys(state.groups).reduce((acc, curr) => {\n if (!acc && !state.groups[curr].valid) acc = state.groups[curr].fields[0];\n return acc;\n }, false);\n firstInvalid && firstInvalid.focus();\n};\n\n/**\n * Creates a hidden field duplicate of a given field, for conferring submit button values\n * \n * @param source [Node] A submit input/button\n * @param form [Node] A form node\n * \n */\nexport const createButtonValueNode = (source, form) => {\n const node = document.createElement('input');\n node.setAttribute('type', 'hidden');\n node.setAttribute('name', source.getAttribute('name'));\n node.setAttribute('value', source.getAttribute('value'));\n return form.appendChild(node);\n};\n\n/**\n * Removes the node added in createButtonValueNode\n * \n * @param node [Node] A hidden input\n * \n */\nexport const cleanupButtonValueNode = node => {\n node.parentNode.removeChild(node);\n};\n\n/**\n * Add aria-required attribute to fields if appropriate (has required/data-val-required, is not a checkbox or radio group) \n * \n * @param fields [Array of DOMElements]\n * \n * @returns fields\n */\nexport const addAriaRequired = fields => {\n fields.forEach(field => {\n if (\n (field.hasAttribute('required') || field.hasAttribute('data-val-required'))\n && ((field.getAttribute('type') !== 'radio' && field.getAttribute('type') !== 'checkbox')\n || (field.getAttribute('type') === 'checkbox' && fields.length === 1))\n ) {\n field.setAttribute('aria-required', 'true');\n }\n });\n\n return fields;\n};\n\n/**\n * Adds attributes to input and error nodes to help accessibility\n * \n * @param state [Object]\n */\nexport const addAXAttributes = state => {\n Object.keys(state.groups).forEach(groupName => {\n //ensure error message has an id for aria-describedby\n if (state.groups[groupName].serverErrorNode && !state.groups[groupName].serverErrorNode.hasAttribute('id')) state.groups[groupName].serverErrorNode.setAttribute('id', `${groupName}-error-message`);\n\n //Add aria-required to inputs that are not radios, nor checkbox groups (single checkbox gets the attribute added)\n addAriaRequired(state.groups[groupName].fields);\n });\n};","import { ACTIONS } from '../constants';\nimport reducers from '../reducers';\nimport {\n getGroupValidityState,\n resolveRealTimeValidationEvent,\n reduceGroupValidityState,\n reduceErrorMessages\n} from './';\nimport {\n clearError,\n renderError\n} from '../dom';\n\n/**\n * Starts real-time validation on each group, adding an eventListener to each field \n * that resets the validityState for the field's group and acquires the new validity state\n * \n * The event that triggers validation is defined by the field type\n * \n * Only if the new validityState is invalid is the validation error object \n * dispatched to the store to update state and render the error\n * \n */\nexport const initRealTimeValidation = store => {\n const handler = groupName => () => {\n const { groups, errors } = store.getState();\n \n if (!groups[groupName].valid && errors[groupName]) {\n store.update(reducers[ACTIONS.CLEAR_ERROR](store.getState(), groupName), [ clearError(groupName) ]);\n }\n getGroupValidityState(groups[groupName])\n .then(res => {\n if (!res.reduce(reduceGroupValidityState, true)) {\n store.update(\n reducers[ACTIONS.VALIDATION_ERROR](store.getState(),\n {\n group: groupName,\n errorMessages: res.reduce(reduceErrorMessages(groupName, store.getState()), [])\n }),\n [ renderError(groupName) ]\n );\n }\n });\n };\n\n Object.keys(store.getState().groups).forEach(groupName => {\n\n const { groups } = store.getState();\n const groupUpdate = { ...groups };\n \n if (!groupUpdate[groupName].hasEvent) {\n groupUpdate[groupName].fields.forEach(input => {\n input.addEventListener(resolveRealTimeValidationEvent(input), handler(groupName));\n });\n\n //;_; can do better?\n const equalToValidator = groupUpdate[groupName].validators.filter(validator => validator.type === 'equalto');\n \n if (equalToValidator.length > 0){\n equalToValidator[0].params.other.forEach(subgroup => {\n subgroup.forEach(item => {\n item.addEventListener('blur', handler(groupName));\n });\n });\n }\n \n groupUpdate[groupName].hasEvent = true;\n }\n \n store.update(reducers[ACTIONS.START_REALTIME](store.getState(), {\n groups: groupUpdate\n }));\n });\n};","import { ACTIONS } from '../constants';\nimport reducers from '../reducers';\nimport {\n getValidityState,\n reduceGroupValidityState,\n isFormValid,\n reduceErrorMessages\n} from '../validator';\nimport { postValidation } from '../validator/post-validation';\nimport { initRealTimeValidation } from '../validator/real-time-validation';\nimport {\n clearErrors,\n renderErrors,\n focusFirstInvalidField\n} from '../dom';\n\n/**\n * Returns a function that extracts the validityState of the entire form\n * can be used as a form submit eventListener or via the API\n * \n * Submits the form if called as a submit eventListener and is valid\n * Dispatches error state to store if errors\n * \n * @param form [DOM node]\n * \n * @returns [Promise] Resolves with boolean validityState of the form\n * \n */\nexport const validate = store => event => {\n event && event.preventDefault();\n store.update(reducers[ACTIONS.CLEAR_ERRORS](store.getState()), [clearErrors]);\n\n return new Promise(resolve => {\n const state = store.getState();\n const { groups, realTimeValidation } = state;\n getValidityState(groups)\n .then(validityState => {\n if (isFormValid(validityState)) return postValidation(event, resolve, store);\n\n if (realTimeValidation === false) initRealTimeValidation(store);\n\n store.update(\n reducers[ACTIONS.VALIDATION_ERRORS](store.getState(), Object.keys(groups)\n .reduce((acc, group, i) => (acc[group] = {\n valid: validityState[i].reduce(reduceGroupValidityState, true),\n errorMessages: validityState[i].reduce(reduceErrorMessages(group, state), [])\n }, acc), {})),\n [renderErrors, focusFirstInvalidField]\n );\n\n return resolve(false);\n }).catch(err => console.warn(err));\n });\n};","import { isSubmitButton, hasNameValue, hasFormactionValue } from './utils';\nimport {\n createButtonValueNode,\n cleanupButtonValueNode\n} from '../dom';\nimport { PREHOOK_DELAY } from '../constants';\n\nexport const postValidation = (event, resolve, store) => {\n const { settings, form } = store.getState();\n let buttonValueNode = false;\n let cachedAction = false;\n const submit = () => {\n if (settings.submit) settings.submit();\n else form.submit();\n\n buttonValueNode && cleanupButtonValueNode(buttonValueNode);\n cachedAction && form.setAttribute('action', cachedAction);\n };\n\n const formSubmitButtons = Array.from(form.querySelectorAll('[type=\"submit\"]'));\n formSubmitButtons.forEach(formSubmitButton => {\n if (hasNameValue(formSubmitButton)) {\n buttonValueNode = createButtonValueNode(formSubmitButton, form);\n }\n if (hasFormactionValue(formSubmitButton)) {\n cachedAction = form.getAttribute('action');\n form.setAttribute('action', formSubmitButton.getAttribute('formaction'));\n }\n });\n\n if (event && event.target) {\n if (settings.preSubmitHook) {\n settings.preSubmitHook();\n window.setTimeout(submit, PREHOOK_DELAY);\n } else submit();\n }\n \n return resolve(true);\n};","import { ACTIONS } from '../constants';\nimport reducers from '../reducers';\n\n/**\n * Adds a custom validation method to the validation model, used via the API\n * Dispatches add validation method to store to update the validators in a group\n * \n * @param groupName [String] The name attribute shared by the DOM nodes in the group\n * @param method [Function] The validation method (function that returns true or false) that is called on the group\n * @param message [String] Te error message displayed if the validation method returns false\n * \n */\nexport const addMethod = store => (groupName, method, message, fields) => {\n if ((groupName === undefined || method === undefined || message === undefined) || !store.getState()[groupName] && (document.getElementsByName(groupName).length === 0 && [].slice.call(document.querySelectorAll(`[data-val-group=\"${groupName}\"]`)).length === 0) && !fields) {\n return console.warn('Custom validation method cannot be added.');\n }\n store.update(reducers[ACTIONS.ADD_VALIDATION_METHOD](store.getState(), { groupName, fields, validator: { type: 'custom', method, message } }));\n};","import {\n removeUnvalidatableGroups,\n assembleValidationGroup,\n getGroupValidityState,\n reduceGroupValidityState,\n reduceErrorMessages } from '../validator';\nimport { initRealTimeValidation } from '../validator/real-time-validation';\nimport { renderError, clearError, addAXAttributes } from '../dom';\nimport { findErrors } from '../validator/utils';\nimport { ACTIONS } from '../constants';\nimport reducers from '../reducers';\n\n/**\n * Adds a group to the validation model, used via the API\n * Dispatches add group method to store \n * \n * @param nodes [Array], nodes comprising the group\n * \n */\nexport const addGroup = store => nodes => {\n const groups = removeUnvalidatableGroups(nodes.reduce(assembleValidationGroup, {}));\n if (Object.keys(groups).length === 0) return console.warn('Group cannot be added.');\n\n store.update(reducers[ACTIONS.ADD_GROUP](store.getState(), groups, findErrors(groups)), [ addAXAttributes, () => {\n if (store.getState().realTimeValidation) {\n //if we're already in realtime validation then we need to re-start it with the newly added group\n initRealTimeValidation(store);\n }\n }]);\n};\n\n/**\n * Validates a group individually, used via the API\n * \n * @param groupName, nodes comprising the group to be validated\n * \n * @returns [Promise] Resolves with boolean validityState of the group\n */\nexport const validateGroup = store => groupName => new Promise(resolve => {\n if (!store.getState().groups[groupName].valid && store.getState().errors[groupName]) {\n store.update(reducers[ACTIONS.CLEAR_ERROR](store.getState(), groupName), [clearError(groupName)]);\n }\n getGroupValidityState(store.getState().groups[groupName])\n .then(res => {\n if (!res.reduce(reduceGroupValidityState, true)) {\n store.update(\n reducers[ACTIONS.VALIDATION_ERROR](store.getState(), {\n group: groupName,\n errorMessages: res.reduce(reduceErrorMessages(groupName, store.getState()), [])\n }),\n [renderError(groupName)]\n );\n return resolve(false);\n }\n return resolve(true);\n });\n});\n\n/**\n * Removes a group from the validation model, used via the API\n * Dispatches remove group method to store \n * \n * @param groupName, nodes comprising the group\n * \n */\nexport const removeGroup = store => groupName => {\n const state = store.getState();\n if (state.errors[groupName]) clearError(groupName)(state);\n store.update(reducers[ACTIONS.REMOVE_GROUP](store.getState(), groupName));\n};","import { createStore } from '../store';\nimport { ACTIONS } from '../constants';\nimport reducers from '../reducers';\nimport { getInitialState } from '../validator';\nimport { validate } from './validate';\nimport { clearErrors, addAXAttributes } from '../dom';\nimport { addMethod } from './add-method';\nimport { addGroup, validateGroup, removeGroup } from './group';\n\n\n/**\n * Default function, sets initial state and adds form-level event listeners\n * \n * @param form [DOM node] the form to validate\n * \n * @returns [Object] The API for the instance\n * *\n */\nexport default (form, settings) => {\n const store = createStore();\n store.update(reducers[ACTIONS.SET_INITIAL_STATE](getInitialState(form, settings)), [ addAXAttributes ]);\n form.addEventListener('submit', validate(store));\n form.addEventListener('reset', () => store.update(reducers[ACTIONS.CLEAR_ERRORS](store.getState()), [ clearErrors ]));\n\n return {\n getState: store.getState,\n validate: validate(store),\n addMethod: addMethod(store),\n addGroup: addGroup(store),\n validateGroup: validateGroup(store),\n removeGroup: removeGroup(store)\n };\n};","export default {\n messages: {\n required() { return 'You must answer this question.'; } ,\n email() { return 'Enter a valid email address, for example: example@example.com.'; },\n pattern() { return 'The value must match the pattern'; },\n url(){ return 'Enter a valid URL'; },\n number() { return 'Enter a valid number'; },\n maxlength(props) { return `Enter no more than ${props.max} characters`; },\n minlength(props) { return `Enter at least ${props.min} characters`; },\n max(props){ return `Enter a number lower than or equal to ${props.max}`; },\n min(props){ return `Enter a number higher than or equal to ${props.min}`;}\n }\n};","import factory from './lib/factory';\nimport defaults from './lib/defaults';\nimport { getSelection } from './lib/validator/utils';\n\n/*\n * Returns an array of objects augmenting DOM elements that match a selector\n *\n * @param selector, Can be a string, Array of DOM nodes, a NodeList or a single DOM element.\