From 10f73b00fca3ca6cbb504ea039f0e5d99cabceaf Mon Sep 17 00:00:00 2001 From: Mick Date: Fri, 14 Feb 2025 15:29:44 +0000 Subject: [PATCH 1/2] Update store implementation --- packages/validate/__tests__/unit/store.js | 15 +++--- packages/validate/src/index.js | 2 - packages/validate/src/lib/defaults/index.js | 20 ++++---- packages/validate/src/lib/dom/index.js | 2 +- .../validate/src/lib/factory/add-method.js | 7 +-- packages/validate/src/lib/factory/group.js | 48 +++++++++---------- packages/validate/src/lib/factory/index.js | 21 ++++---- packages/validate/src/lib/factory/validate.js | 20 ++++---- packages/validate/src/lib/store/index.js | 30 +++--------- .../src/lib/validator/post-validation.js | 4 +- .../src/lib/validator/real-time-validation.js | 31 ++++++------ 11 files changed, 91 insertions(+), 109 deletions(-) diff --git a/packages/validate/__tests__/unit/store.js b/packages/validate/__tests__/unit/store.js index ee87a4d9..5b46560d 100644 --- a/packages/validate/__tests__/unit/store.js +++ b/packages/validate/__tests__/unit/store.js @@ -1,5 +1,6 @@ import { createStore } from '../../src/lib/store'; import { ACTIONS } from '../../src/lib/constants'; +import reducers from '../../src/lib/reducers'; let Store; beforeAll(() => { Store = createStore(); @@ -9,8 +10,8 @@ describe('Validate > Unit > Store > createStore', () => { it('should create a store object with dispatch and get functions', async () => { expect.assertions(5); expect(Store).not.toBeUndefined(); - expect(Store.dispatch).not.toBeUndefined(); - expect(typeof Store.dispatch === 'function').toEqual(true); + expect(Store.update).not.toBeUndefined(); + expect(typeof Store.update === 'function').toEqual(true); expect(Store.getState).not.toBeUndefined(); expect(typeof Store.getState === 'function').toEqual(true); }); @@ -24,14 +25,14 @@ describe('Validate > Unit > Store > getState', () => { }); }); -//dispatch -describe('Validate > Unit > Store > dispatch', () => { +//update +describe('Validate > Unit > Store > update', () => { it('should update state using reducers and nextState payload', async () => { expect.assertions(1); const nextState = { newProp: true }; - Store.dispatch(ACTIONS.SET_INITIAL_STATE, nextState); + Store.update(reducers[ACTIONS.SET_INITIAL_STATE](Store.getState(), nextState)); expect(Store.getState()).toEqual(nextState); }); @@ -42,7 +43,7 @@ describe('Validate > Unit > Store > dispatch', () => { const sideEffect = () => { flag = true; }; - Store.dispatch(ACTIONS.SET_INITIAL_STATE, nextState, [sideEffect]); + Store.update(reducers[ACTIONS.SET_INITIAL_STATE](Store.getState(), nextState), [sideEffect]); expect(flag).toEqual(true); }); @@ -54,7 +55,7 @@ describe('Validate > Unit > Store > dispatch', () => { const sideEffect = () => { flag = true; }; - Store.dispatch(ACTIONS.SET_INITIAL_STATE, false, [sideEffect]); + Store.update(reducers[ACTIONS.SET_INITIAL_STATE](Store.getState(), false), [sideEffect]); expect(flag).toEqual(true); expect(Store.getState()).toEqual(priorState); }); diff --git a/packages/validate/src/index.js b/packages/validate/src/index.js index be5d9d13..0aa7ce42 100644 --- a/packages/validate/src/index.js +++ b/packages/validate/src/index.js @@ -9,8 +9,6 @@ import { getSelection } from './lib/validator/utils'; * @params options, Object, to be merged with defaults to become the settings propery of each returned object */ export default (selector, options) => { - //Array.from isnt polyfilled - //https://github.com/babel/babel/issues/5682 let nodes = getSelection(selector); return nodes.reduce((acc, el) => { diff --git a/packages/validate/src/lib/defaults/index.js b/packages/validate/src/lib/defaults/index.js index c6d2bd56..1780830b 100644 --- a/packages/validate/src/lib/defaults/index.js +++ b/packages/validate/src/lib/defaults/index.js @@ -1,13 +1,13 @@ export default { messages: { - required() { return 'You must answer this question.'; } , - email() { return 'Enter a valid email address, for example: example@example.com.'; }, - pattern() { return 'The value must match the pattern'; }, - url(){ return 'Enter a valid URL'; }, - number() { return 'Enter a valid number'; }, - maxlength(props) { return `Enter no more than ${props.max} characters`; }, - minlength(props) { return `Enter at least ${props.min} characters`; }, - max(props){ return `Enter a number lower than or equal to ${props.max}`; }, - min(props){ return `Enter a number higher than or equal to ${props.min}`;} + required() { return 'You must answer this question.'; } , + email() { return 'Enter a valid email address, for example: example@example.com.'; }, + pattern() { return 'The value must match the pattern'; }, + url(){ return 'Enter a valid URL'; }, + number() { return 'Enter a valid number'; }, + maxlength(props) { return `Enter no more than ${props.max} characters`; }, + minlength(props) { return `Enter at least ${props.min} characters`; }, + max(props){ return `Enter a number lower than or equal to ${props.max}`; }, + min(props){ return `Enter a number higher than or equal to ${props.min}`;} } - }; \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/validate/src/lib/dom/index.js b/packages/validate/src/lib/dom/index.js index 088bea53..dfcbabd7 100644 --- a/packages/validate/src/lib/dom/index.js +++ b/packages/validate/src/lib/dom/index.js @@ -136,7 +136,7 @@ export const renderError = groupName => state => { //shouldn't be updating state here... //to do: refactor to update state as a side effect afterwards? - //would need to pass Store instead of state + //would need to pass store instead of state if (state.groups[groupName].serverErrorNode) { state.errors[groupName] = createErrorTextNode(state.groups[groupName], msg); } else { diff --git a/packages/validate/src/lib/factory/add-method.js b/packages/validate/src/lib/factory/add-method.js index 0706e8ed..d9aad61d 100644 --- a/packages/validate/src/lib/factory/add-method.js +++ b/packages/validate/src/lib/factory/add-method.js @@ -1,4 +1,5 @@ import { ACTIONS } from '../constants'; +import reducers from '../reducers'; /** * Adds a custom validation method to the validation model, used via the API @@ -9,9 +10,9 @@ import { ACTIONS } from '../constants'; * @param message [String] Te error message displayed if the validation method returns false * */ -export const addMethod = Store => (groupName, method, message, fields) => { - 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) { +export const addMethod = store => (groupName, method, message, fields) => { + 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) { return console.warn('Custom validation method cannot be added.'); } - Store.dispatch(ACTIONS.ADD_VALIDATION_METHOD, { groupName, fields, validator: { type: 'custom', method, message } }); + store.update(reducers[ACTIONS.ADD_VALIDATION_METHOD](store.getState(), { groupName, fields, validator: { type: 'custom', method, message } })); }; \ No newline at end of file diff --git a/packages/validate/src/lib/factory/group.js b/packages/validate/src/lib/factory/group.js index 23511cb6..de14e1c9 100644 --- a/packages/validate/src/lib/factory/group.js +++ b/packages/validate/src/lib/factory/group.js @@ -1,12 +1,13 @@ -import { - removeUnvalidatableGroups, - assembleValidationGroup, - getGroupValidityState, +import { + removeUnvalidatableGroups, + assembleValidationGroup, + getGroupValidityState, reduceGroupValidityState, reduceErrorMessages } from '../validator'; import { initRealTimeValidation } from '../validator/real-time-validation'; import { renderError, clearError, addAXAttributes } from '../dom'; import { ACTIONS } from '../constants'; +import reducers from '../reducers'; /** * Adds a group to the validation model, used via the API @@ -15,14 +16,14 @@ import { ACTIONS } from '../constants'; * @param nodes [Array], nodes comprising the group * */ -export const addGroup = Store => nodes => { +export const addGroup = store => nodes => { const groups = removeUnvalidatableGroups(nodes.reduce(assembleValidationGroup, {})); if (Object.keys(groups).length === 0) return console.warn('Group cannot be added.'); - Store.dispatch(ACTIONS.ADD_GROUP, groups, [ addAXAttributes, () => { - if (Store.getState().realTimeValidation) { + store.update(reducers[ACTIONS.ADD_GROUP](store.getState(), groups), [ addAXAttributes, () => { + if (store.getState().realTimeValidation) { //if we're already in realtime validation then we need to re-start it with the newly added group - initRealTimeValidation(Store); + initRealTimeValidation(store); } }]); }; @@ -34,28 +35,25 @@ export const addGroup = Store => nodes => { * * @returns [Promise] Resolves with boolean validityState of the group */ -export const validateGroup = Store => groupName => { - return new Promise(resolve => { - if(!Store.getState().groups[groupName].valid && Store.getState().errors[groupName]) { - Store.dispatch(ACTIONS.CLEAR_ERROR, groupName, [clearError(groupName)]); - } - getGroupValidityState(Store.getState().groups[groupName]) +export const validateGroup = store => groupName => new Promise(resolve => { + if (!store.getState().groups[groupName].valid && store.getState().errors[groupName]) { + store.update(reducers[ACTIONS.CLEAR_ERROR](store.getState(), groupName), [clearError(groupName)]); + } + getGroupValidityState(store.getState().groups[groupName]) .then(res => { - if(!res.reduce(reduceGroupValidityState, true)) { - Store.dispatch( - ACTIONS.VALIDATION_ERROR, - { + if (!res.reduce(reduceGroupValidityState, true)) { + store.update( + reducers[ACTIONS.VALIDATION_ERROR](store.getState(), { group: groupName, - errorMessages: res.reduce(reduceErrorMessages(groupName, Store.getState()), []) - }, + errorMessages: res.reduce(reduceErrorMessages(groupName, store.getState()), []) + }), [renderError(groupName)] ); return resolve(false); } return resolve(true); }); - }) -}; +}); /** * Removes a group from the validation model, used via the API @@ -64,8 +62,8 @@ export const validateGroup = Store => groupName => { * @param groupName, nodes comprising the group * */ -export const removeGroup = Store => groupName => { - const state = Store.getState(); +export const removeGroup = store => groupName => { + const state = store.getState(); if (state.errors[groupName]) clearError(groupName)(state); - Store.dispatch(ACTIONS.REMOVE_GROUP, groupName); + store.update(reducers[ACTIONS.REMOVE_GROUP](store.getState(), groupName)); }; \ No newline at end of file diff --git a/packages/validate/src/lib/factory/index.js b/packages/validate/src/lib/factory/index.js index 05e6f7ac..3489bf5e 100644 --- a/packages/validate/src/lib/factory/index.js +++ b/packages/validate/src/lib/factory/index.js @@ -1,5 +1,6 @@ import { createStore } from '../store'; import { ACTIONS } from '../constants'; +import reducers from '../reducers'; import { getInitialState } from '../validator'; import { validate } from './validate'; import { clearErrors, addAXAttributes } from '../dom'; @@ -16,17 +17,17 @@ import { addGroup, validateGroup, removeGroup } from './group'; * * */ export default (form, settings) => { - const Store = createStore(); - Store.dispatch(ACTIONS.SET_INITIAL_STATE, getInitialState(form, settings), [ addAXAttributes ]); - form.addEventListener('submit', validate(Store)); - form.addEventListener('reset', () => Store.dispatch(ACTIONS.CLEAR_ERRORS, {}, [ clearErrors ])); + const store = createStore(); + store.update(reducers[ACTIONS.SET_INITIAL_STATE](getInitialState(form, settings)), [ addAXAttributes ]); + form.addEventListener('submit', validate(store)); + form.addEventListener('reset', () => store.update(reducers[ACTIONS.CLEAR_ERRORS](store.getState()), [ clearErrors ])); return { - getState: Store.getState, - validate: validate(Store), - addMethod: addMethod(Store), - addGroup: addGroup(Store), - validateGroup: validateGroup(Store), - removeGroup: removeGroup(Store) + getState: store.getState, + validate: validate(store), + addMethod: addMethod(store), + addGroup: addGroup(store), + validateGroup: validateGroup(store), + removeGroup: removeGroup(store) }; }; \ No newline at end of file diff --git a/packages/validate/src/lib/factory/validate.js b/packages/validate/src/lib/factory/validate.js index 92ed5b9f..d86df8cb 100644 --- a/packages/validate/src/lib/factory/validate.js +++ b/packages/validate/src/lib/factory/validate.js @@ -1,4 +1,5 @@ import { ACTIONS } from '../constants'; +import reducers from '../reducers'; import { getValidityState, reduceGroupValidityState, @@ -18,33 +19,32 @@ import { * 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 + * Dispatches error state to store if errors * * @param form [DOM node] * * @returns [Promise] Resolves with boolean validityState of the form * */ -export const validate = Store => event => { +export const validate = store => event => { event && event.preventDefault(); - Store.dispatch(ACTIONS.CLEAR_ERRORS, null, [clearErrors]); + store.update(reducers[ACTIONS.CLEAR_ERRORS](store.getState()), [clearErrors]); return new Promise(resolve => { - const state = Store.getState(); + const state = store.getState(); const { groups, realTimeValidation } = state; getValidityState(groups) .then(validityState => { - if (isFormValid(validityState)) return postValidation(event, resolve, Store); + if (isFormValid(validityState)) return postValidation(event, resolve, store); - if (realTimeValidation === false) initRealTimeValidation(Store); + if (realTimeValidation === false) initRealTimeValidation(store); - Store.dispatch( - ACTIONS.VALIDATION_ERRORS, - Object.keys(groups) + store.update( + reducers[ACTIONS.VALIDATION_ERRORS](store.getState(), Object.keys(groups) .reduce((acc, group, i) => (acc[group] = { valid: validityState[i].reduce(reduceGroupValidityState, true), errorMessages: validityState[i].reduce(reduceErrorMessages(group, state), []) - }, acc), {}), + }, acc), {})), [renderErrors, focusFirstInvalidField] ); diff --git a/packages/validate/src/lib/store/index.js b/packages/validate/src/lib/store/index.js index 48b43859..23bba601 100644 --- a/packages/validate/src/lib/store/index.js +++ b/packages/validate/src/lib/store/index.js @@ -1,31 +1,13 @@ -import reducers from '../reducers'; - export const createStore = () => { - //shared centralised validator state let state = {}; - - //uncomment for debugging by writing state history to window - // window.__validator_history__ = []; - - //state getter + const getState = () => 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...) - */ - const 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__); + const update = (nextState, effects) => { + state = nextState ?? state; if (!effects) return; - effects.forEach(effect => { effect(state); }); + effects.forEach(effect => effect(state)); }; - - return { dispatch, getState }; + + return { update, getState }; }; \ No newline at end of file diff --git a/packages/validate/src/lib/validator/post-validation.js b/packages/validate/src/lib/validator/post-validation.js index a7955061..61ad8d69 100644 --- a/packages/validate/src/lib/validator/post-validation.js +++ b/packages/validate/src/lib/validator/post-validation.js @@ -5,8 +5,8 @@ import { } from '../dom'; import { PREHOOK_DELAY } from '../constants'; -export const postValidation = (event, resolve, Store) => { - const { settings, form } = Store.getState(); +export const postValidation = (event, resolve, store) => { + const { settings, form } = store.getState(); let buttonValueNode = false; let cachedAction = false; const submit = () => { diff --git a/packages/validate/src/lib/validator/real-time-validation.js b/packages/validate/src/lib/validator/real-time-validation.js index 6f46a7ab..d9cdbbc4 100644 --- a/packages/validate/src/lib/validator/real-time-validation.js +++ b/packages/validate/src/lib/validator/real-time-validation.js @@ -1,4 +1,5 @@ import { ACTIONS } from '../constants'; +import reducers from '../reducers'; import { getGroupValidityState, resolveRealTimeValidationEvent, @@ -20,34 +21,34 @@ import { * dispatched to the store to update state and render the error * */ -export const initRealTimeValidation = Store => { +export const initRealTimeValidation = store => { const handler = groupName => () => { - const { groups, errors } = Store.getState(); + const { groups, errors } = store.getState(); if (!groups[groupName].valid && errors[groupName]) { - Store.dispatch(ACTIONS.CLEAR_ERROR, groupName, [ clearError(groupName) ]); + store.update(reducers[ACTIONS.CLEAR_ERROR](store.getState(), groupName), [ clearError(groupName) ]); } getGroupValidityState(groups[groupName]) .then(res => { if (!res.reduce(reduceGroupValidityState, true)) { - Store.dispatch( - ACTIONS.VALIDATION_ERROR, - { - group: groupName, - errorMessages: res.reduce(reduceErrorMessages(groupName, Store.getState()), []) - }, + store.update( + reducers[ACTIONS.VALIDATION_ERROR](store.getState(), + { + group: groupName, + errorMessages: res.reduce(reduceErrorMessages(groupName, store.getState()), []) + }), [ renderError(groupName) ] ); } }); }; - Object.keys(Store.getState().groups).forEach(groupName => { + Object.keys(store.getState().groups).forEach(groupName => { - const { groups } = Store.getState(); - const groupUpdate = {...groups}; + const { groups } = store.getState(); + const groupUpdate = { ...groups }; - if(!groupUpdate[groupName].hasEvent) { + if (!groupUpdate[groupName].hasEvent) { groupUpdate[groupName].fields.forEach(input => { input.addEventListener(resolveRealTimeValidationEvent(input), handler(groupName)); }); @@ -66,8 +67,8 @@ export const initRealTimeValidation = Store => { groupUpdate[groupName].hasEvent = true; } - Store.dispatch(ACTIONS.START_REALTIME, { + store.update(reducers[ACTIONS.START_REALTIME](store.getState(), { groups: groupUpdate - }); + })); }); }; \ No newline at end of file From 17a87f2a59a1b3a46a9b52fd10c1a5f97de1d2dc Mon Sep 17 00:00:00 2001 From: Mick Date: Mon, 3 Mar 2025 18:23:44 +0000 Subject: [PATCH 2/2] Update test title --- packages/validate/__tests__/unit/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validate/__tests__/unit/store.js b/packages/validate/__tests__/unit/store.js index 5b46560d..f54b4d43 100644 --- a/packages/validate/__tests__/unit/store.js +++ b/packages/validate/__tests__/unit/store.js @@ -7,7 +7,7 @@ beforeAll(() => { }); //createStore describe('Validate > Unit > Store > createStore', () => { - it('should create a store object with dispatch and get functions', async () => { + it('should create a store object with update and get functions', async () => { expect.assertions(5); expect(Store).not.toBeUndefined(); expect(Store.update).not.toBeUndefined();