Skip to content

Commit d44706a

Browse files
committed
main 🧊 v1.0.9
1 parent 44321ca commit d44706a

34 files changed

Lines changed: 2720 additions & 246 deletions

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@siberiacancode/reactuse",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"description": "The ultimate collection of react hooks",
55
"author": {
66
"name": "SIBERIA CAN CODE 🧊",

packages/core/src/bundle/hooks/state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './useCycleList/useCycleList';
88
export * from './useDefault/useDefault';
99
export * from './useDisclosure/useDisclosure';
1010
export * from './useField/useField';
11+
export * from './useForm/useForm';
1112
export * from './useHash/useHash';
1213
export * from './useList/useList';
1314
export * from './useLocalStorage/useLocalStorage';

packages/core/src/bundle/hooks/useField/useField.js

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,34 @@ import { useRerender } from '../useRerender/useRerender';
88
*
99
* @template Value The input value
1010
* @template Type The input value type
11-
* @param {Value} [params.initialValue = ""] Initial value
12-
* @param {boolean} [params.initialTouched=false] Initial touched state
13-
* @param {boolean} [params.autoFocus=false] Auto focus
14-
* @param {boolean} [params.validateOnChange=false] Validate on change
15-
* @param {boolean} [params.validateOnBlur=false] Validate on blur
16-
* @param {boolean} [params.validateOnMount=false] Validate on mount
11+
* @param {Value} [initialValue = ""] Initial value
12+
* @param {boolean} [options.initialTouched=false] Initial touched state
13+
* @param {boolean} [options.autoFocus=false] Auto focus
14+
* @param {boolean} [options.validateOnChange=false] Validate on change
15+
* @param {boolean} [options.validateOnBlur=false] Validate on blur
16+
* @param {boolean} [options.validateOnMount=false] Validate on mount
17+
* @param {string} [options.required] Required validation message
18+
* @param {object} [options.min] Min value validation
19+
* @param {object} [options.max] Max value validation
20+
* @param {object} [options.minLength] Min length validation
21+
* @param {object} [options.maxLength] Max length validation
22+
* @param {object} [options.pattern] Pattern validation
23+
* @param {Function} [options.validate] Custom validation
1724
* @returns {UseFieldReturn<Value>} An object containing input information
1825
*
1926
* @example
2027
* const { register, getValue, setValue, reset, dirty, error, setError, clearError, touched, focus, watch } = useField();
2128
*/
2229
export const useField = (initialValue = '', options) => {
2330
const inputRef = useRef(null);
31+
const initializedRef = useRef(null);
2432
const watchingRef = useRef(false);
2533
const rerender = useRerender();
2634
const [dirty, setDirty] = useState(false);
2735
const [touched, setTouched] = useState(options?.initialTouched ?? false);
2836
const [error, setError] = useState(undefined);
37+
const optionsRef = useRef(options);
38+
optionsRef.current = options;
2939
const getValue = () => {
3040
if (
3141
inputRef.current &&
@@ -55,68 +65,71 @@ export const useField = (initialValue = '', options) => {
5565
setError(undefined);
5666
};
5767
const focus = () => inputRef.current.focus();
58-
const validate = (params) => {
59-
if (params.required && !inputRef.current.value) {
60-
return setError(params.required);
61-
}
62-
if (params.min && Number(inputRef.current.value) < params.min.value) {
63-
return setError(params.min.message);
64-
}
65-
if (params.max && Number(inputRef.current.value) > params.max.value) {
66-
return setError(params.max.message);
67-
}
68-
if (params.minLength && inputRef.current.value.length < params.minLength.value) {
68+
const validate = async (params) => {
69+
const hasRules =
70+
params.required ||
71+
params.min ||
72+
params.max ||
73+
params.minLength ||
74+
params.maxLength ||
75+
params.pattern ||
76+
params.validate;
77+
if (!hasRules) return;
78+
const value = inputRef.current.value;
79+
if (params.required && !value) return setError(params.required);
80+
if (params.min && Number(value) < params.min.value) return setError(params.min.message);
81+
if (params.max && Number(value) > params.max.value) return setError(params.max.message);
82+
if (params.minLength && value.length < params.minLength.value)
6983
return setError(params.minLength.message);
70-
}
71-
if (params.maxLength && inputRef.current.value.length > params.maxLength.value) {
84+
if (params.maxLength && value.length > params.maxLength.value)
7285
return setError(params.maxLength.message);
73-
}
74-
if (params.pattern && !params.pattern.value.test(inputRef.current.value)) {
86+
if (params.pattern && !params.pattern.value.test(value))
7587
return setError(params.pattern.message);
76-
}
7788
if (params.validate) {
78-
const error = params.validate(inputRef.current.value);
79-
if (typeof error === 'string') return setError(error);
89+
const result = await params.validate(value);
90+
if (typeof result === 'string') return setError(result);
8091
}
8192
setError(undefined);
8293
};
83-
const register = (registerParams) => ({
94+
const register = (params) => ({
8495
ref: (node) => {
96+
const registerParams = { ...optionsRef.current, ...params };
8597
if (!node) {
8698
inputRef.current = null;
8799
return;
88100
}
89-
if (inputRef.current !== node) {
90-
if (options?.autoFocus) node.focus();
91-
inputRef.current = node;
92-
if ('checked' in node && node.type === 'radio') {
101+
inputRef.current = node;
102+
if (initializedRef.current === node) return;
103+
initializedRef.current = node;
104+
if (registerParams.autoFocus) node.focus();
105+
if (registerParams.validateOnMount) validate(registerParams);
106+
if (node instanceof HTMLInputElement) {
107+
if (node.type === 'radio') {
93108
node.defaultChecked = initialValue === node.value;
94109
return;
95110
}
96-
if ('checked' in node && node.type === 'checkbox') {
97-
node.defaultChecked = !!initialValue;
111+
if (node.type === 'checkbox') {
112+
node.defaultChecked = Boolean(initialValue);
98113
return;
99114
}
100-
if ('defaultValue' in node) {
101-
node.defaultValue = String(initialValue);
102-
} else {
103-
node.value = String(initialValue);
104-
}
105-
if (registerParams && options?.validateOnMount) validate(registerParams);
115+
node.defaultValue = String(initialValue);
116+
return;
106117
}
118+
node.value = String(initialValue);
107119
},
108120
onChange: async (event) => {
121+
const registerParams = { ...optionsRef.current, ...params };
109122
if (watchingRef.current) rerender();
110-
if (inputRef.current.value !== initialValue) setDirty(true);
111-
if (inputRef.current.value === initialValue) setDirty(false);
112-
if (registerParams && options?.validateOnChange) await validate(registerParams);
113-
if (registerParams && options?.validateOnBlur) setError(undefined);
114-
registerParams?.onChange?.(event);
123+
setDirty(getValue() !== initialValue);
124+
if (registerParams.validateOnChange) await validate(registerParams);
125+
if (registerParams.validateOnBlur) setError(undefined);
126+
registerParams.onChange?.(event);
115127
},
116128
onBlur: async (event) => {
117-
if (registerParams && options?.validateOnBlur) await validate(registerParams);
129+
const registerParams = { ...optionsRef.current, ...params };
130+
if (registerParams.validateOnBlur) await validate(registerParams);
118131
setTouched(true);
119-
registerParams?.onBlur?.(event);
132+
registerParams.onBlur?.(event);
120133
}
121134
});
122135
const watch = () => {
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { useRef, useState } from 'react';
2+
import { useRerender } from '../useRerender/useRerender';
3+
/**
4+
* @name useForm
5+
* @description - Hook to manage a form
6+
* @category State
7+
* @usage medium
8+
*
9+
* @template Values The form values
10+
* @param {Values} options.initialValues Initial values
11+
* @param {UseFormResolver<Values>} [options.resolver] Schema resolver
12+
* @param {keyof Values} [options.autoFocus] Field to focus on mount
13+
* @param {boolean} [options.validateOnChange=false] Validate on change
14+
* @param {boolean} [options.validateOnBlur=false] Validate on blur
15+
* @param {boolean} [options.validateOnMount=false] Validate on mount
16+
* @returns {UseFormReturn<Values>} An object containing form information
17+
*
18+
* @example
19+
* const form = useForm({ initialValues: { email: '' } });
20+
*/
21+
export const useForm = (options) => {
22+
const { initialValues, autoFocus, resolver, validateOnBlur, validateOnChange, validateOnMount } =
23+
options;
24+
const nodesRef = useRef(new Map());
25+
const initializedRef = useRef(new Map());
26+
const paramsRef = useRef(new Map());
27+
const initialRef = useRef(initialValues);
28+
const watchingRef = useRef(false);
29+
const rerender = useRerender();
30+
const [dirty, setDirty] = useState({});
31+
const [touched, setTouched] = useState({});
32+
const [errors, setErrors] = useState({});
33+
const [submitting, setSubmitting] = useState(false);
34+
const read = (name) => {
35+
const node = nodesRef.current.get(name);
36+
if (!node) return initialRef.current[name];
37+
if ('checked' in node && (node.type === 'radio' || node.type === 'checkbox'))
38+
return node.checked;
39+
return node.value;
40+
};
41+
const getValues = () =>
42+
Object.keys(initialRef.current).reduce((acc, key) => {
43+
acc[key] = read(key);
44+
return acc;
45+
}, {});
46+
const setValue = (name, value) => {
47+
const node = nodesRef.current.get(name);
48+
if (!node) return;
49+
if ('checked' in node && (node.type === 'radio' || node.type === 'checkbox')) {
50+
node.checked = value;
51+
if (watchingRef.current) rerender();
52+
return;
53+
}
54+
node.value = String(value);
55+
if (watchingRef.current) rerender();
56+
};
57+
const focus = (name) => nodesRef.current.get(name)?.focus();
58+
const validate = async (name) => {
59+
const params = paramsRef.current.get(name);
60+
if (!params) return undefined;
61+
const value = read(name);
62+
const string = typeof value === 'string' ? value : String(value ?? '');
63+
if (params.required && (value === false || string === '')) return params.required;
64+
if (params.min && Number(value) < params.min.value) return params.min.message;
65+
if (params.max && Number(value) > params.max.value) return params.max.message;
66+
if (params.minLength && string.length < params.minLength.value) return params.minLength.message;
67+
if (params.maxLength && string.length > params.maxLength.value) return params.maxLength.message;
68+
if (params.pattern && !params.pattern.value.test(string)) return params.pattern.message;
69+
if (params.validate) {
70+
const result = await params.validate(value, getValues());
71+
if (typeof result === 'string') return result;
72+
}
73+
return undefined;
74+
};
75+
const setError = (name, error) =>
76+
setErrors((currentErrors) => ({ ...currentErrors, [name]: error }));
77+
const reset = (values) => {
78+
initialRef.current = { ...initialRef.current, ...values };
79+
nodesRef.current.forEach((_, name) => setValue(name, initialRef.current[name]));
80+
setDirty({});
81+
setTouched({});
82+
setErrors({});
83+
rerender();
84+
};
85+
const clearErrors = (name) =>
86+
setErrors((currentErrors) => {
87+
if (!name) return {};
88+
const next = { ...currentErrors };
89+
delete next[name];
90+
return next;
91+
});
92+
const watch = () => {
93+
watchingRef.current = true;
94+
return getValues();
95+
};
96+
const runValidation = async (name) => {
97+
if (resolver) {
98+
const result = await resolver(getValues());
99+
const resolved = result.errors ?? {};
100+
if (!name) {
101+
setErrors(resolved);
102+
return resolved;
103+
}
104+
const names = Array.isArray(name) ? name : [name];
105+
const next = names.reduce((acc, key) => {
106+
if (resolved[key]) acc[key] = resolved[key];
107+
return acc;
108+
}, {});
109+
setErrors((currentErrors) => {
110+
const cleared = { ...currentErrors };
111+
names.forEach((key) => delete cleared[key]);
112+
return { ...cleared, ...next };
113+
});
114+
return next;
115+
}
116+
const names = name ? (Array.isArray(name) ? name : [name]) : [...paramsRef.current.keys()];
117+
const results = await Promise.all(names.map(validate));
118+
const next = names.reduce((acc, key, index) => {
119+
if (results[index]) acc[key] = results[index];
120+
return acc;
121+
}, {});
122+
setErrors((currentErrors) => {
123+
const cleared = { ...currentErrors };
124+
names.forEach((key) => delete cleared[key]);
125+
return { ...cleared, ...next };
126+
});
127+
return next;
128+
};
129+
const trigger = async (name, options) => {
130+
const validationErrors = await runValidation(name);
131+
const names = Object.keys(validationErrors);
132+
if (options?.shouldFocus && names.length) focus(names[0]);
133+
return !names.length;
134+
};
135+
const register = (name, params) => {
136+
paramsRef.current.set(name, params);
137+
return {
138+
name: String(name),
139+
ref: (node) => {
140+
if (!node) {
141+
nodesRef.current.delete(name);
142+
return;
143+
}
144+
nodesRef.current.set(name, node);
145+
if (initializedRef.current.get(name) === node) return;
146+
initializedRef.current.set(name, node);
147+
const initial = initialRef.current[name];
148+
if (autoFocus === name) node.focus();
149+
if (validateOnMount) runValidation();
150+
if (node instanceof HTMLInputElement) {
151+
if (node.type === 'radio') {
152+
node.defaultChecked = initial === node.value;
153+
return;
154+
}
155+
if (node.type === 'checkbox') {
156+
node.defaultChecked = Boolean(initial);
157+
return;
158+
}
159+
node.defaultValue = String(initial ?? '');
160+
return;
161+
}
162+
if (node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement) {
163+
node.value = String(initial ?? '');
164+
}
165+
},
166+
onChange: async (event) => {
167+
if (watchingRef.current) rerender();
168+
const isDirty = read(name) !== initialRef.current[name];
169+
setDirty((currentDirty) =>
170+
currentDirty[name] === isDirty ? currentDirty : { ...currentDirty, [name]: isDirty }
171+
);
172+
if (validateOnChange) await runValidation(name);
173+
else if (validateOnBlur) clearErrors(name);
174+
params?.onChange?.(event);
175+
},
176+
onBlur: async (event) => {
177+
if (validateOnBlur) await runValidation(name);
178+
setTouched((currentTouched) =>
179+
currentTouched[name] ? currentTouched : { ...currentTouched, [name]: true }
180+
);
181+
params?.onBlur?.(event);
182+
}
183+
};
184+
};
185+
const handleSubmit = (onValid, onInvalid) => async (event) => {
186+
event?.preventDefault();
187+
setSubmitting(true);
188+
const names = resolver ? Object.keys(initialRef.current) : [...paramsRef.current.keys()];
189+
setTouched(names.reduce((acc, key) => ({ ...acc, [key]: true }), {}));
190+
try {
191+
const errors = await runValidation();
192+
if (Object.keys(errors).length) {
193+
onInvalid?.(errors, event);
194+
return;
195+
}
196+
const values = resolver ? (await resolver(getValues())).values : getValues();
197+
await onValid(values, event);
198+
} finally {
199+
setSubmitting(false);
200+
}
201+
};
202+
return {
203+
register,
204+
handleSubmit,
205+
dirty,
206+
errors,
207+
submitting,
208+
touched,
209+
clearErrors,
210+
focus,
211+
getValue: read,
212+
getValues,
213+
reset,
214+
setError,
215+
setValue,
216+
trigger,
217+
watch
218+
};
219+
};

0 commit comments

Comments
 (0)