Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions packages/validate/__tests__/unit/store.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { createStore } from '../../src/lib/store';
import { ACTIONS } from '../../src/lib/constants';
import reducers from '../../src/lib/reducers';
let Store;
beforeAll(() => {
Store = createStore();
});
//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.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);
});
Expand All @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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);
});
Expand Down
2 changes: 0 additions & 2 deletions packages/validate/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
20 changes: 10 additions & 10 deletions packages/validate/src/lib/defaults/index.js
Original file line number Diff line number Diff line change
@@ -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}`;}
}
};
};
2 changes: 1 addition & 1 deletion packages/validate/src/lib/dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions packages/validate/src/lib/factory/add-method.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 } }));
};
48 changes: 23 additions & 25 deletions packages/validate/src/lib/factory/group.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
}
}]);
};
Expand All @@ -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
Expand All @@ -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));
};
21 changes: 11 additions & 10 deletions packages/validate/src/lib/factory/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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)
};
};
20 changes: 10 additions & 10 deletions packages/validate/src/lib/factory/validate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ACTIONS } from '../constants';
import reducers from '../reducers';
import {
getValidityState,
reduceGroupValidityState,
Expand All @@ -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]
);

Expand Down
30 changes: 6 additions & 24 deletions packages/validate/src/lib/store/index.js
Original file line number Diff line number Diff line change
@@ -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 };
};
4 changes: 2 additions & 2 deletions packages/validate/src/lib/validator/post-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Loading