-
Notifications
You must be signed in to change notification settings - Fork 2
/
useValidation.ts
203 lines (193 loc) · 7.6 KB
/
useValidation.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import { useCallback, useMemo } from "react";
import {
validErrorValues, Errors, useErrors, ErrorHandler,
} from "./useErrors";
import {
Values, useResetableValues, MutableValues, PartialValues,
} from "./useResetableValues";
import { assert, LoggingTypes } from "./utils";
type validValidatorReturnTypes = validErrorValues | Promise<validErrorValues>;
type validSingleValidatorReturnTypes = Errors | Promise<Errors>;
export type userSuppliedValue = undefined | string | number | Date | null;
export interface SingleValidator<T> {
(values: Values<T>): validSingleValidatorReturnTypes;
}
export interface Validator {
(value?: userSuppliedValue): validValidatorReturnTypes;
}
export type Validators = Values<Validator>;
export interface ValidateHandler<T, K = string> {
(name: K, value: T): Promise<validErrorValues>;
}
export interface ValidateAllHandler<T, K = Values<T>> {
(valuesMap: K): Promise<Errors>;
}
interface UseValidatorHook<T> {
readonly errors: Errors;
readonly hasErrors: boolean;
readonly setError: ErrorHandler;
readonly validateByName: ValidateHandler<T>;
readonly validate: ValidateAllHandler<T>;
readonly isValidating: boolean;
readonly resetErrors: () => void;
}
interface UseValidatorHookPartial<T, K> {
readonly errors: PartialValues<K, Error>;
readonly hasErrors: boolean;
readonly setError: ErrorHandler<keyof K>;
readonly validateByName: ValidateHandler<T, keyof K>;
readonly validate: ValidateAllHandler<T, PartialValues<K, T>>;
readonly isValidating: boolean;
readonly resetErrors: () => void;
}
function defaultValidator(): validValidatorReturnTypes {
return "";
}
function useValidationFieldNames(
validator: Validators | SingleValidator<string>,
expectedFields?: string[],
): string[] {
return useMemo((): string[] => expectedFields || ((typeof validator === "function") ? [] : Object.keys(validator)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);
}
function assertValidator(functionName: string, name: string, validator: Function): void {
assert.error(
typeof validator === "function",
LoggingTypes.typeError,
// note: received is any bc we don't know what the validator is
// as the input could have defaulted to the defaultValidator
`(expect: function, received: any) ${functionName} expects the validator with the name (${name}) to be a function.`,
);
}
export async function validateValidators(
names: string[], validators: Validators, values: Values<userSuppliedValue>,
): Promise<Errors> {
const errorsPromiseMap = names
.map(async (name): Promise<[string, validValidatorReturnTypes]> => {
const handler = validators[name] || defaultValidator;
assertValidator(validateValidators.name, name, handler);
const currentErrors = await handler(values[name]);
return [name, currentErrors];
});
const errorsMap = await Promise.all(errorsPromiseMap);
return errorsMap.reduce((
objectMap: MutableValues<validValidatorReturnTypes>, [name, error],
): Errors => {
// eslint-disable-next-line no-param-reassign
objectMap[name] = error;
return objectMap as Errors;
}, {});
}
export function useValidation(
validator: SingleValidator<userSuppliedValue>, expectedFields?: string[],
): UseValidatorHook<userSuppliedValue>;
export function useValidation<T extends Validators>(
validator: T, expectedFields?: string[],
): UseValidatorHookPartial<userSuppliedValue, T>;
export function useValidation<T extends Validators>(
validator: T | SingleValidator<userSuppliedValue>,
expectedFields?: string[],
): UseValidatorHookPartial<userSuppliedValue, T> | UseValidatorHook<userSuppliedValue>;
/**
* A hook for performing validation.
*
* @param validator A validation map or a validation function.
* @param expectedFields Define the fields required for validation.
* This is useful if you want certain fields to always be validated (ie required fields).
* If you are using a validation map,
* then this value will default to the keys of the validation map.
* @return returns an useValidation object
*
* @example
*
* // validate using validation maps
* const {validateByName, errors} = useValidation({
* name: (value) => value ? "" : "name is required!",
* email: (value) => value ? "" : "email is required!"
* });
*
* // "email is required!"
* await validateByName("email", "");
* // {email: "email is required!"}
* console.log(errors);
*
* // validate with one validation function
* const {errors, validate} = useValidation((values) => {
* const errors = {name: "", email: ""};
* if (!values.name) {
* errors.name = "name is required!";
* }
* if (!values.email) {
* errors.email = "email is required!";
* }
* return errors;
* });
*
* // {name: "", email: "email is required!"}
* await validate({name: "John"});
* // {name: "", email: "email is required!"}
* console.log(errors);
*/
export function useValidation(
validator: Validators | SingleValidator<userSuppliedValue>,
expectedFields?: string[],
): UseValidatorHookPartial<userSuppliedValue, Validators> | UseValidatorHook<userSuppliedValue> {
const {
setError, errors, hasErrors, resetErrors, setErrors,
} = useErrors();
// this is empty if the user passes singleValidator
const fieldsToUseInValidateAll = useValidationFieldNames(validator, expectedFields);
const {
setValue: setValidationState,
hasValue: isValidating,
setValues: setValidationStates,
} = useResetableValues();
// create a validate by input name function
const validateByName = useCallback(async (
name: string, value: userSuppliedValue,
): Promise<validErrorValues> => {
let error: validErrorValues;
setValidationState(name, true);
if (typeof validator === "function") {
const localErrors = await validator({ [name]: value });
error = localErrors[name] || "";
} else {
const handler = validator[name] || defaultValidator;
assertValidator(useValidation.name, name, handler);
error = await handler(value) || "";
}
setError(name, error);
setValidationState(name, false);
return error;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setError, setValidationState, validator]);
// create validate all function
const validate = useCallback(async (values: Values<userSuppliedValue>): Promise<Errors> => {
const names = [...Object.keys(values), ...fieldsToUseInValidateAll];
const setAllValidationState = (state: boolean): void => {
const allStates = names.reduce((
states: MutableValues<boolean>, name,
): Values<boolean> => {
// eslint-disable-next-line no-param-reassign
states[name] = state;
return states;
}, {});
setValidationStates(allStates);
};
setAllValidationState(true);
let localErrors: Errors;
if (typeof validator === "function") {
localErrors = await validator(values);
} else {
localErrors = await validateValidators(names, validator, values);
}
setErrors(localErrors);
setAllValidationState(false);
return localErrors;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setValidationState, setErrors, fieldsToUseInValidateAll, validator]);
return {
validate, validateByName, errors, hasErrors, resetErrors, setError, isValidating,
};
}