UNPKG

@stormid/validate

Version:
316 lines (292 loc) 13.2 kB
import methods from './methods'; import { isCheckable, isSelect, isFile, isHidden, domNodesFromCommaList, groupIsDisabled, findErrors, groupIsAllHidden } from './utils'; import { DOTNET_ADAPTORS, DOTNET_PARAMS, DOTNET_ERROR_SPAN_DATA_ATTRIBUTE, DOM_SELECTOR_PARAMS, GROUP_ATTRIBUTE } from '../constants'; /** * 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 */ export const resolveParam = (param, input) => { let value = input.getAttribute(`data-val-${param}`); return ({ [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 */ export const extractParams = (input, adaptor) => DOTNET_PARAMS[adaptor] ? { params: DOTNET_PARAMS[adaptor] .reduce((acc, param) => input.hasAttribute(`data-val-${param}`) ? Object.assign(acc, resolveParam(param, input)) : acc, {}) } : false; /** * Reducer that takes all known .NET MVC adaptors (data-attributes that specify a validation method that should be applied 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) */ export const extractDataValValidators = input => DOTNET_ADAPTORS.reduce((validators, adaptor) => !input.getAttribute(`data-val-${adaptor}`) ? validators : [...validators, Object.assign({ type: adaptor, message: input.getAttribute(`data-val-${adaptor}`) }, extractParams(input, adaptor) ) ], []); /** * Looks for a data-val attribute matching the validator type and returns the value * * @param input [DOM node] * @param type [String] * * @return [Object] Error message or empty */ const resolveMessages = (input, type) => input.getAttribute(`data-val-${type}`) ? { message: input.getAttribute(`data-val-${type}`) } : {}; /** * Checks attributes on an input to generate an array of validators the attributes describe * * @param input [DOM node] * * @return validators [Array] */ export const extractAttrValidators = input => { let validators = []; if ((input.hasAttribute('required') || input.hasAttribute('aria-required')) && (input.getAttribute('required') !== 'false' || input.getAttribute('aria-required') !== 'false')){ validators.push({ type: 'required', ...resolveMessages(input, 'required') } ); } if (input.getAttribute('type') === 'email') validators.push({ type: 'email', ...resolveMessages(input, 'email') }); if (input.getAttribute('type') === 'url') validators.push({ type: 'url', ...resolveMessages(input, 'url') }); if (input.getAttribute('type') === 'number') validators.push({ type: 'number', ...resolveMessages(input, 'number') }); if ((input.getAttribute('minlength') && input.getAttribute('minlength') !== 'false')){ validators.push({ type: 'minlength', params: { min: input.getAttribute('minlength') }, ...resolveMessages(input, 'minlength') }); } if ((input.getAttribute('maxlength') && input.getAttribute('maxlength') !== 'false')){ validators.push({ type: 'maxlength', params: { max: input.getAttribute('maxlength') }, ...resolveMessages(input, 'maxlength') }); } if ((input.getAttribute('min') && input.getAttribute('min') !== 'false')){ validators.push({ type: 'min', params: { min: input.getAttribute('min') }, ...resolveMessages(input, 'min') }); } if ((input.getAttribute('max') && input.getAttribute('max') !== 'false')){ validators.push({ type: 'max', params: { max: input.getAttribute('max') }, ...resolveMessages(input, 'max') }); } if ((input.getAttribute('pattern') && input.getAttribute('pattern') !== 'false')){ validators.push({ type: 'pattern', params: { regex: input.getAttribute('pattern') }, ...resolveMessages(input, 'pattern') }); } return validators; }; /** * Validator checks to extract validators based on HTML5 attributes * * Each function is so we can seed each fn with an input and pipe the result array through each function * Signature: inputDOMNode => validatorArray => updateValidatorArray const required = input => (validators = []) => { // console.log(validators); return input.hasAttribute('required') && input.getAttribute('required') !== 'false' ? [...validators, {type: 'required'}] : validators; }; const email = input => (validators = []) => input.getAttribute('type') === 'email' ? [...validators, {type: 'email'}] : validators; const url = input => (validators = []) => input.getAttribute('type') === 'url' ? [...validators, {type: 'url'}] : validators; const number = input => (validators = []) => input.getAttribute('type') === 'number' ? [...validators, {type: 'number'}] : validators; const minlength = input => (validators = []) => (input.getAttribute('minlength') && input.getAttribute('minlength') !== 'false') ? [...validators, {type: 'minlength', params: { min: input.getAttribute('minlength')}}] : validators; const maxlength = input => (validators = []) => (input.getAttribute('maxlength') && input.getAttribute('maxlength') !== 'false') ? [...validators, {type: 'maxlength', params: { max: input.getAttribute('maxlength')}}] : validators; const min = input => (validators = []) => (input.getAttribute('min') && input.getAttribute('min') !== 'false') ? [...validators, {type: 'min', params: { min: input.getAttribute('min')}}] : validators; const max = input => (validators = []) => (input.getAttribute('max') && input.getAttribute('max') !== 'false') ? [...validators, {type: 'max', params: { max: input.getAttribute('max')}}] : validators; const pattern = input => (validators = []) => (input.getAttribute('pattern') && input.getAttribute('pattern') !== 'false') ? [...validators, {type: 'pattern', params: { regex: input.getAttribute('pattern')}}] : 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] */ export const normaliseValidators = input => 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 * * @returns validityState [Promise] * */ export const validate = (group, validator) => new Promise((resolve, reject) => { try { validator.type === 'custom' ? resolve(methods.custom(validator.method, group)) : resolve(methods[validator.type](group, validator.params)); } catch (err) { console.warn(err); resolve(err); } }); /** * 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 * */ export const assembleValidationGroup = (acc, input) => { let name = (input.getAttribute('data-val-'+GROUP_ATTRIBUTE)) ? input.getAttribute('data-val-'+GROUP_ATTRIBUTE) : input.getAttribute('name') ; if (!name) return console.warn('Missing data group or name attribute'), acc; if (acc[name] && isHidden(input)) return acc; const serverErrorNode = document.querySelector(`[${DOTNET_ERROR_SPAN_DATA_ATTRIBUTE}="${name}"]`) || false; return acc[name] = acc[name] ? Object.assign(acc[name], { fields: [...acc[name].fields, input] }) : { valid: false, validators: normaliseValidators(input), fields: [input], serverErrorNode }, 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 * */ export const extractErrorMessage = (messages, validator) => validator.message || messages[validator.type](validator.params !== undefined ? validator.params : null); /** * 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] * */ export const reduceErrorMessages = (group, state) => (acc, validity, j) => validity === true ? acc : [...acc, typeof validity === 'boolean' ? extractErrorMessage(state.settings.messages, 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 * */ export const removeUnvalidatableGroups = groups => { let validationGroups = {}; for (let group in groups){ if (groups[group].validators.length > 0 && !groupIsAllHidden(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 * */ export const getInitialState = (form, settings) => { const groups = removeUnvalidatableGroups([].slice.call(form.querySelectorAll('input:not([type=submit]), textarea, select')) .reduce(assembleValidationGroup, {})); return { form, settings, errors: findErrors(groups), realTimeValidation: false, groups }; }; /** * Reducer run against an array of resolved validation promises to set the overall validityState of a group * * @return validityState [Boolean] * */ export const reduceGroupValidityState = (acc, curr) => { if (curr !== true) acc = false; return acc; }; export const isFormValid = validityState => [].concat(...validityState).reduce(reduceGroupValidityState, true); /** * Aggregates validation promises for all groups into a single promise * * @params groups [Object] * * @return validation results [Promise] aggregated promise * */ export const getValidityState = groups => Promise.all( Object.keys(groups) .map(group => getGroupValidityState(groups[group])) ); /** * Aggregates all of the validation promises for a single group into a single promise * * @params groups [Object] * * @return validation results [Promises] aggregated promise * */ export const getGroupValidityState = group => { //check if group is disabled if (groupIsDisabled(group.fields)) return Promise.resolve([true]); return Promise.all(group.validators.map(validator => new Promise((resolve, reject) => { validate(group, validator) .then(res => { if (String(res) !== 'true') resolve(String(res) === 'false' ? false : res); else resolve(true); }) .catch(err => console.warn(err)); }))); }; /** * 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] * */ export const resolveRealTimeValidationEvent = input => ['input', 'change'][Number(isCheckable(input) || isSelect(input) || isFile(input))];