UNPKG

storm-validate

Version:

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

292 lines (273 loc) 13.2 kB
import methods from './methods'; import messages from '../constants/messages'; import { isCheckable, isSelect, isFile, DOMNodesFromCommaList, groupIsHidden } from './utils'; import { DOTNET_ADAPTORS, DOTNET_PARAMS, DOTNET_ERROR_SPAN_DATA_ATTRIBUTE, DOM_SELECTOR_PARAMS } 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 */ 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 */ const extractParams = (input, adaptor) => DOTNET_PARAMS[adaptor] ? { params: DOTNET_PARAMS[adaptor] .reduce((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) */ 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) ) ], []); /** * Checks attributes on an input to generate an array of validators the attributes describe * * @param input [DOM node] * * @return validators [Array] */ const extractAttrValidators = input => { let 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; }; /** * Validator checks to extract validators based on HTML5 attributes * * Each function is curried 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 * */ export const validate = (group, validator) => 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 * */ export const assembleValidationGroup = (acc, input) => { let name = input.getAttribute('name'); return acc[name] = acc[name] ? Object.assign(acc[name], { fields: [...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 * */ const extractErrorMessage = validator => 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] * */ export const reduceErrorMessages = (group, state) => (acc, validity, j) => { return validity === true ? acc : [...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 * */ export const removeUnvalidatableGroups = groups => { let validationGroups = {}; for(let 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 * */ export const getInitialState = form => { return { 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] * */ export const 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 * */ export const getValidityState = groups => { return Promise.all( Object.keys(groups) .map(group => 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 * */ export const getGroupValidityState = group => { let hasError = false; return Promise.all(group.validators.map(validator => { return new Promise(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(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] * */ export const resolveRealTimeValidationEvent = input => ['input', 'change'][Number(isCheckable(input) || isSelect(input) || isFile(input))];