1 | import * as React from "react";
|
2 | import { ArrayValue } from "reakit-utils/types";
|
3 | import { useUpdateEffect } from "reakit-utils/useUpdateEffect";
|
4 | import { isPromise } from "reakit-utils/isPromise";
|
5 | import {
|
6 | SealedInitialState,
|
7 | useSealedState,
|
8 | } from "reakit-utils/useSealedState";
|
9 | import { isEmpty } from "reakit-utils/isEmpty";
|
10 | import { useLiveRef } from "reakit-utils/useLiveRef";
|
11 | import {
|
12 | unstable_IdState,
|
13 | unstable_IdActions,
|
14 | unstable_IdInitialState,
|
15 | unstable_useIdState,
|
16 | } from "../Id/IdState";
|
17 | import { DeepPartial, DeepMap, DeepPath, DeepPathValue } from "./__utils/types";
|
18 | import { filterAllEmpty } from "./__utils/filterAllEmpty";
|
19 | import { hasMessages } from "./__utils/hasMessages";
|
20 | import { unstable_setAllIn } from "./utils/setAllIn";
|
21 | import { unstable_getIn } from "./utils/getIn";
|
22 | import { unstable_setIn } from "./utils/setIn";
|
23 |
|
24 | type Messages<V> = DeepPartial<DeepMap<V, string | null | void>>;
|
25 |
|
26 | type ValidateOutput<V> = Messages<V> | null | void;
|
27 | type ValidateReturn<V> = Promise<ValidateOutput<V>> | ValidateOutput<V>;
|
28 |
|
29 | interface Update<V> {
|
30 | <P extends DeepPath<V, P>>(name: P, value: DeepPathValue<V, P>): void;
|
31 | <P extends DeepPath<V, P>>(
|
32 | name: P,
|
33 | value: (value: DeepPathValue<V, P>) => DeepPathValue<V, P>
|
34 | ): void;
|
35 | }
|
36 |
|
37 | export type unstable_FormState<V> = unstable_IdState & {
|
38 | |
39 |
|
40 |
|
41 | values: V;
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 | touched: DeepPartial<DeepMap<V, boolean>>;
|
48 | |
49 |
|
50 |
|
51 |
|
52 | messages: Messages<V>;
|
53 | |
54 |
|
55 |
|
56 |
|
57 | errors: Messages<V>;
|
58 | |
59 |
|
60 |
|
61 | validating: boolean;
|
62 | |
63 |
|
64 |
|
65 | valid: boolean;
|
66 | |
67 |
|
68 |
|
69 | submitting: boolean;
|
70 | |
71 |
|
72 |
|
73 | submitSucceed: number;
|
74 | |
75 |
|
76 |
|
77 | submitFailed: number;
|
78 | };
|
79 |
|
80 | export type unstable_FormActions<V> = unstable_IdActions & {
|
81 | |
82 |
|
83 |
|
84 | reset: () => void;
|
85 | |
86 |
|
87 |
|
88 |
|
89 | validate: (values?: V) => ValidateReturn<V>;
|
90 | |
91 |
|
92 |
|
93 | submit: () => void;
|
94 | |
95 |
|
96 |
|
97 | update: Update<V>;
|
98 | |
99 |
|
100 |
|
101 | blur: <P extends DeepPath<V, P>>(name: P) => void;
|
102 | |
103 |
|
104 |
|
105 | push: <P extends DeepPath<V, P>>(
|
106 | name: P,
|
107 | value?: ArrayValue<DeepPathValue<V, P>>
|
108 | ) => void;
|
109 | |
110 |
|
111 |
|
112 | remove: <P extends DeepPath<V, P>>(name: P, index: number) => void;
|
113 | };
|
114 |
|
115 | export type unstable_FormInitialState<V> = unstable_IdInitialState &
|
116 | Partial<Pick<unstable_FormState<V>, "values">> & {
|
117 | |
118 |
|
119 |
|
120 | validateOnBlur?: boolean;
|
121 | |
122 |
|
123 |
|
124 | validateOnChange?: boolean;
|
125 | |
126 |
|
127 |
|
128 | resetOnSubmitSucceed?: boolean;
|
129 | |
130 |
|
131 |
|
132 |
|
133 | resetOnUnmount?: boolean;
|
134 | |
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | onValidate?: (values: V) => ValidateReturn<V>;
|
141 | |
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | onSubmit?: (values: V) => ValidateReturn<V>;
|
150 | };
|
151 |
|
152 | export type unstable_FormStateReturn<V> = unstable_FormState<V> &
|
153 | unstable_FormActions<V>;
|
154 |
|
155 | type ReducerState<V> = Omit<unstable_FormState<V>, keyof unstable_IdState> & {
|
156 | initialValues: V;
|
157 | };
|
158 |
|
159 | type ReducerAction =
|
160 | | { type: "reset" }
|
161 | | { type: "startValidate" }
|
162 | | { type: "endValidate"; errors?: any; messages?: any }
|
163 | | { type: "startSubmit" }
|
164 | | { type: "endSubmit"; errors?: any; messages?: any }
|
165 | | { type: "update"; name: any; value: any }
|
166 | | { type: "blur"; name: any }
|
167 | | { type: "push"; name: any; value: any }
|
168 | | { type: "remove"; name: any; index: number };
|
169 |
|
170 | function getMessages<V>(
|
171 | stateMessages: Messages<V>,
|
172 | actionMessages: Messages<V>
|
173 | ) {
|
174 | return !isEmpty(actionMessages)
|
175 | ? actionMessages
|
176 | : isEmpty(stateMessages)
|
177 | ? stateMessages
|
178 | : {};
|
179 | }
|
180 |
|
181 | function reducer<V>(
|
182 | state: ReducerState<V>,
|
183 | action: ReducerAction
|
184 | ): ReducerState<V> {
|
185 | switch (action.type) {
|
186 | case "reset": {
|
187 | return {
|
188 | ...state,
|
189 | values: state.initialValues,
|
190 | touched: {},
|
191 | errors: {},
|
192 | messages: {},
|
193 | valid: true,
|
194 | validating: false,
|
195 | submitting: false,
|
196 | submitFailed: 0,
|
197 | submitSucceed: 0,
|
198 | };
|
199 | }
|
200 | case "startValidate": {
|
201 | return {
|
202 | ...state,
|
203 | validating: true,
|
204 | };
|
205 | }
|
206 | case "endValidate": {
|
207 | return {
|
208 | ...state,
|
209 | validating: false,
|
210 | errors: getMessages(state.errors, action.errors),
|
211 | messages: getMessages(state.messages, action.messages),
|
212 | valid: !hasMessages(action.errors),
|
213 | };
|
214 | }
|
215 | case "startSubmit": {
|
216 | return {
|
217 | ...state,
|
218 |
|
219 | touched: unstable_setAllIn(state.values, true),
|
220 | submitting: true,
|
221 | };
|
222 | }
|
223 | case "endSubmit": {
|
224 | const valid = !hasMessages(action.errors);
|
225 | return {
|
226 | ...state,
|
227 | valid,
|
228 | submitting: false,
|
229 | errors: getMessages(state.errors, action.errors),
|
230 | messages: getMessages(state.messages, action.messages),
|
231 | submitSucceed: valid ? state.submitSucceed + 1 : state.submitSucceed,
|
232 | submitFailed: valid ? state.submitFailed : state.submitFailed + 1,
|
233 | };
|
234 | }
|
235 | case "update": {
|
236 | const { name, value } = action;
|
237 | const nextValue =
|
238 | typeof value === "function"
|
239 | ? value(unstable_getIn(state.values, name))
|
240 | : value;
|
241 | return {
|
242 | ...state,
|
243 | values: unstable_setIn(
|
244 | state.values,
|
245 | name,
|
246 | nextValue != null ? nextValue : ""
|
247 | ),
|
248 | };
|
249 | }
|
250 | case "blur": {
|
251 | return {
|
252 | ...state,
|
253 | touched: unstable_setIn(state.touched, action.name, true),
|
254 | };
|
255 | }
|
256 | case "push": {
|
257 | const array = unstable_getIn(state.values, action.name, []);
|
258 | return {
|
259 | ...state,
|
260 | values: unstable_setIn(state.values, action.name, [
|
261 | ...array,
|
262 | action.value,
|
263 | ]),
|
264 | };
|
265 | }
|
266 | case "remove": {
|
267 | const array = unstable_getIn(state.values, action.name, []).slice();
|
268 | delete array[action.index];
|
269 | return {
|
270 | ...state,
|
271 | values: unstable_setIn(state.values, action.name, array),
|
272 | };
|
273 | }
|
274 | default: {
|
275 | throw new Error();
|
276 | }
|
277 | }
|
278 | }
|
279 |
|
280 | export function unstable_useFormState<V = Record<any, any>>(
|
281 | initialState: SealedInitialState<unstable_FormInitialState<V>> = {}
|
282 | ): unstable_FormStateReturn<V> {
|
283 | const {
|
284 | values: initialValues = {} as V,
|
285 | validateOnBlur = true,
|
286 | validateOnChange = true,
|
287 | resetOnSubmitSucceed = false,
|
288 | resetOnUnmount = true,
|
289 | onValidate,
|
290 | onSubmit,
|
291 | ...sealed
|
292 | } = useSealedState(initialState);
|
293 | const onValidateRef = useLiveRef(
|
294 | typeof initialState !== "function" ? initialState.onValidate : onValidate
|
295 | );
|
296 | const onSubmitRef = useLiveRef(
|
297 | typeof initialState !== "function" ? initialState.onSubmit : onSubmit
|
298 | );
|
299 |
|
300 | const id = unstable_useIdState(sealed);
|
301 |
|
302 | const [{ initialValues: _, ...state }, dispatch] = React.useReducer(reducer, {
|
303 | initialValues,
|
304 | values: initialValues,
|
305 | touched: {},
|
306 | errors: {},
|
307 | messages: {},
|
308 | valid: true,
|
309 | validating: false,
|
310 | submitting: false,
|
311 | submitFailed: 0,
|
312 | submitSucceed: 0,
|
313 | });
|
314 |
|
315 | const validate = React.useCallback(
|
316 | (vals = state.values) =>
|
317 | new Promise<any>((resolve) => {
|
318 | if (onValidateRef.current) {
|
319 | const response = onValidateRef.current(vals);
|
320 | if (isPromise(response)) {
|
321 | dispatch({ type: "startValidate" });
|
322 | }
|
323 |
|
324 | resolve(
|
325 | Promise.resolve(response).then((messages) => {
|
326 | dispatch({ type: "endValidate", messages });
|
327 | return messages;
|
328 | })
|
329 | );
|
330 | } else {
|
331 | resolve(undefined);
|
332 | }
|
333 | }).catch((errors) => {
|
334 | dispatch({ type: "endValidate", errors });
|
335 | throw errors;
|
336 | }),
|
337 | [state.values]
|
338 | );
|
339 |
|
340 | useUpdateEffect(() => {
|
341 | if (validateOnChange) {
|
342 | validate().catch(() => {});
|
343 | }
|
344 | }, [validate, validateOnChange]);
|
345 |
|
346 | React.useEffect(() => {
|
347 | if (resetOnUnmount) {
|
348 | return () => {
|
349 | dispatch({ type: "reset" });
|
350 | };
|
351 | }
|
352 | return undefined;
|
353 | }, [resetOnUnmount]);
|
354 |
|
355 | return {
|
356 | ...id,
|
357 | ...state,
|
358 | values: state.values as V,
|
359 | validate,
|
360 | reset: React.useCallback(() => dispatch({ type: "reset" }), []),
|
361 | submit: React.useCallback(() => {
|
362 | dispatch({ type: "startSubmit" });
|
363 | return validate()
|
364 | .then((validateMessages) => {
|
365 | if (onSubmitRef.current) {
|
366 | return Promise.resolve(
|
367 | onSubmitRef.current(filterAllEmpty(state.values as V))
|
368 | ).then((submitMessages) => {
|
369 | const messages = { ...validateMessages, ...submitMessages };
|
370 | dispatch({ type: "endSubmit", messages });
|
371 | });
|
372 | }
|
373 | return dispatch({ type: "endSubmit", messages: validateMessages });
|
374 | })
|
375 | .then(() => {
|
376 | if (resetOnSubmitSucceed) {
|
377 | dispatch({ type: "reset" });
|
378 | }
|
379 | })
|
380 | .catch((errors) => {
|
381 | dispatch({ type: "endSubmit", errors });
|
382 | });
|
383 | }, [validate]),
|
384 | update: React.useCallback(
|
385 | (name: any, value: any) => dispatch({ type: "update", name, value }),
|
386 | []
|
387 | ),
|
388 | blur: React.useCallback(
|
389 | (name) => {
|
390 | dispatch({ type: "blur", name });
|
391 | if (validateOnBlur) {
|
392 | validate().catch(() => {});
|
393 | }
|
394 | },
|
395 | [validate]
|
396 | ),
|
397 | push: React.useCallback(
|
398 | (name, value) => dispatch({ type: "push", name, value }),
|
399 | []
|
400 | ),
|
401 | remove: React.useCallback(
|
402 | (name, index) => dispatch({ type: "remove", name, index }),
|
403 | []
|
404 | ),
|
405 | };
|
406 | }
|
407 |
|
\ | No newline at end of file |