remix-validated-form 4.4.2 → 4.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +13 -10
- package/browser/ValidatedForm.js +24 -39
- package/browser/hooks.d.ts +1 -1
- package/browser/internal/hooks.d.ts +3 -2
- package/browser/internal/hooks.js +1 -0
- package/browser/internal/state/controlledFieldStore.d.ts +23 -21
- package/browser/internal/state/controlledFieldStore.js +32 -19
- package/browser/internal/state/controlledFields.d.ts +3 -3
- package/browser/internal/state/controlledFields.js +19 -21
- package/browser/internal/state/createFormStore.d.ts +16 -8
- package/browser/internal/state/createFormStore.js +62 -8
- package/browser/internal/state/storeHooks.d.ts +1 -3
- package/browser/internal/state/storeHooks.js +2 -8
- package/browser/internal/state/types.d.ts +1 -0
- package/browser/internal/state/types.js +1 -0
- package/browser/unreleased/formStateHooks.d.ts +8 -2
- package/browser/unreleased/formStateHooks.js +12 -2
- package/browser/userFacingFormContext.d.ts +8 -2
- package/browser/userFacingFormContext.js +15 -4
- package/dist/remix-validated-form.cjs.js +3 -3
- package/dist/remix-validated-form.cjs.js.map +1 -1
- package/dist/remix-validated-form.es.js +169 -113
- package/dist/remix-validated-form.es.js.map +1 -1
- package/dist/remix-validated-form.umd.js +3 -3
- package/dist/remix-validated-form.umd.js.map +1 -1
- package/dist/types/hooks.d.ts +1 -1
- package/dist/types/internal/hooks.d.ts +3 -2
- package/dist/types/internal/state/controlledFieldStore.d.ts +23 -21
- package/dist/types/internal/state/controlledFields.d.ts +3 -3
- package/dist/types/internal/state/createFormStore.d.ts +16 -8
- package/dist/types/internal/state/storeHooks.d.ts +1 -3
- package/dist/types/internal/state/types.d.ts +1 -0
- package/dist/types/unreleased/formStateHooks.d.ts +8 -2
- package/dist/types/userFacingFormContext.d.ts +8 -2
- package/package.json +4 -4
- package/src/ValidatedForm.tsx +41 -56
- package/src/internal/hooks.ts +4 -1
- package/src/internal/state/controlledFieldStore.ts +95 -74
- package/src/internal/state/controlledFields.ts +38 -26
- package/src/internal/state/createFormStore.ts +199 -115
- package/src/internal/state/storeHooks.ts +3 -16
- package/src/internal/state/types.ts +1 -0
- package/src/unreleased/formStateHooks.ts +24 -3
- package/src/userFacingFormContext.ts +38 -13
- package/dist/types/internal/state/cleanup.d.ts +0 -2
- package/dist/types/internal/state/storeFamily.d.ts +0 -9
- package/src/internal/state/cleanup.ts +0 -8
- package/src/internal/state/storeFamily.ts +0 -24
package/dist/types/hooks.d.ts
CHANGED
@@ -64,4 +64,4 @@ export declare const useField: (name: string, options?: {
|
|
64
64
|
formId?: string | undefined;
|
65
65
|
} | undefined) => FieldProps;
|
66
66
|
export declare const useControlField: <T>(name: string, formId?: string | undefined) => readonly [T, (value: T) => void];
|
67
|
-
export declare const useUpdateControlledField: (formId?: string | undefined) => (
|
67
|
+
export declare const useUpdateControlledField: (formId?: string | undefined) => (field: string, value: unknown) => void;
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { FieldErrors, ValidationErrorResponseData } from "..";
|
2
2
|
import { InternalFormContextValue } from "./formContext";
|
3
3
|
import { Hydratable } from "./hydratable";
|
4
|
-
import { InternalFormId } from "./state/
|
4
|
+
import { InternalFormId } from "./state/types";
|
5
5
|
export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
|
6
6
|
export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
|
7
7
|
export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => Hydratable<FieldErrors | undefined>;
|
@@ -18,7 +18,7 @@ export declare const useInternalIsSubmitting: (formId: InternalFormId) => boolea
|
|
18
18
|
export declare const useInternalIsValid: (formId: InternalFormId) => boolean;
|
19
19
|
export declare const useInternalHasBeenSubmitted: (formId: InternalFormId) => boolean;
|
20
20
|
export declare const useValidateField: (formId: InternalFormId) => (fieldName: string) => Promise<string | null>;
|
21
|
-
export declare const useValidate: (formId: InternalFormId) => () => Promise<
|
21
|
+
export declare const useValidate: (formId: InternalFormId) => () => Promise<import("..").ValidationResult<unknown>>;
|
22
22
|
export declare const useRegisterReceiveFocus: (formId: InternalFormId) => (fieldName: string, handler: () => void) => () => void;
|
23
23
|
export declare const useSyncedDefaultValues: (formId: InternalFormId) => {
|
24
24
|
[fieldName: string]: any;
|
@@ -28,5 +28,6 @@ export declare const useTouchedFields: (formId: InternalFormId) => import("..").
|
|
28
28
|
export declare const useFieldErrors: (formId: InternalFormId) => FieldErrors;
|
29
29
|
export declare const useSetFieldErrors: (formId: InternalFormId) => (errors: FieldErrors) => void;
|
30
30
|
export declare const useResetFormElement: (formId: InternalFormId) => () => void;
|
31
|
+
export declare const useSubmitForm: (formId: InternalFormId) => () => void;
|
31
32
|
export declare const useFormActionProp: (formId: InternalFormId) => string | undefined;
|
32
33
|
export declare const useFormSubactionProp: (formId: InternalFormId) => string | undefined;
|
@@ -1,24 +1,26 @@
|
|
1
|
+
import { InternalFormId } from "./types";
|
2
|
+
export declare type FieldState = {
|
3
|
+
refCount: number;
|
4
|
+
value: unknown;
|
5
|
+
defaultValue?: unknown;
|
6
|
+
hydrated: boolean;
|
7
|
+
valueUpdatePromise: Promise<void> | undefined;
|
8
|
+
resolveValueUpdate: (() => void) | undefined;
|
9
|
+
};
|
1
10
|
export declare type ControlledFieldState = {
|
2
|
-
|
3
|
-
[
|
4
|
-
|
5
|
-
|
6
|
-
defaultValue?: unknown;
|
7
|
-
hydrated: boolean;
|
8
|
-
valueUpdatePromise: Promise<void> | undefined;
|
9
|
-
resolveValueUpdate: (() => void) | undefined;
|
10
|
-
} | undefined;
|
11
|
+
forms: {
|
12
|
+
[formId: InternalFormId]: {
|
13
|
+
[fieldName: string]: FieldState | undefined;
|
14
|
+
};
|
11
15
|
};
|
12
|
-
register: (fieldName: string) => void;
|
13
|
-
unregister: (fieldName: string) => void;
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
export declare const controlledFieldStore: {
|
20
|
-
(formId: import("./storeFamily").InternalFormId): import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ControlledFieldState>, "setState"> & {
|
21
|
-
setState(nextStateOrUpdater: ControlledFieldState | Partial<ControlledFieldState> | ((state: import("immer/dist/internal").WritableDraft<ControlledFieldState>) => void), shouldReplace?: boolean | undefined): void;
|
22
|
-
}>;
|
23
|
-
remove(formId: import("./storeFamily").InternalFormId): void;
|
16
|
+
register: (formId: InternalFormId, fieldName: string) => void;
|
17
|
+
unregister: (formId: InternalFormId, fieldName: string) => void;
|
18
|
+
getField: (formId: InternalFormId, fieldName: string) => FieldState | undefined;
|
19
|
+
setValue: (formId: InternalFormId, fieldName: string, value: unknown) => void;
|
20
|
+
hydrateWithDefault: (formId: InternalFormId, fieldName: string, defaultValue: unknown) => void;
|
21
|
+
awaitValueUpdate: (formId: InternalFormId, fieldName: string) => Promise<void>;
|
22
|
+
reset: (formId: InternalFormId) => void;
|
24
23
|
};
|
24
|
+
export declare const useControlledFieldStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<ControlledFieldState>, "setState"> & {
|
25
|
+
setState(nextStateOrUpdater: ControlledFieldState | Partial<ControlledFieldState> | ((state: import("immer/dist/internal").WritableDraft<ControlledFieldState>) => void), shouldReplace?: boolean | undefined): void;
|
26
|
+
}>;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { InternalFormContextValue } from "../formContext";
|
2
|
-
import { InternalFormId } from "./
|
2
|
+
import { InternalFormId } from "./types";
|
3
3
|
export declare const useControlledFieldValue: (context: InternalFormContextValue, field: string) => any;
|
4
4
|
export declare const useControllableValue: (context: InternalFormContextValue, field: string) => readonly [any, (value: unknown) => void];
|
5
|
-
export declare const useUpdateControllableValue: (formId: InternalFormId) => (
|
6
|
-
export declare const useAwaitValue: (formId: InternalFormId) => (
|
5
|
+
export declare const useUpdateControllableValue: (formId: InternalFormId) => (field: string, value: unknown) => void;
|
6
|
+
export declare const useAwaitValue: (formId: InternalFormId) => (field: string) => Promise<void>;
|
@@ -1,4 +1,6 @@
|
|
1
|
-
import {
|
1
|
+
import { WritableDraft } from "immer/dist/internal";
|
2
|
+
import { FieldErrors, TouchedFields, ValidationResult, Validator } from "../../validation/types";
|
3
|
+
import { InternalFormId } from "./types";
|
2
4
|
export declare type SyncedFormProps = {
|
3
5
|
formId?: string;
|
4
6
|
action?: string;
|
@@ -9,6 +11,14 @@ export declare type SyncedFormProps = {
|
|
9
11
|
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
10
12
|
validator: Validator<unknown>;
|
11
13
|
};
|
14
|
+
export declare type FormStoreState = {
|
15
|
+
forms: {
|
16
|
+
[formId: InternalFormId]: FormState;
|
17
|
+
};
|
18
|
+
form: (formId: InternalFormId) => FormState;
|
19
|
+
registerForm: (formId: InternalFormId) => void;
|
20
|
+
cleanupForm: (formId: InternalFormId) => void;
|
21
|
+
};
|
12
22
|
export declare type FormState = {
|
13
23
|
isHydrated: boolean;
|
14
24
|
isSubmitting: boolean;
|
@@ -29,12 +39,10 @@ export declare type FormState = {
|
|
29
39
|
setHydrated: () => void;
|
30
40
|
setFormElement: (formElement: HTMLFormElement | null) => void;
|
31
41
|
validateField: (fieldName: string) => Promise<string | null>;
|
32
|
-
validate: () => Promise<
|
42
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
33
43
|
resetFormElement: () => void;
|
44
|
+
submit: () => void;
|
34
45
|
};
|
35
|
-
export declare const
|
36
|
-
(
|
37
|
-
|
38
|
-
}>;
|
39
|
-
remove(formId: import("./storeFamily").InternalFormId): void;
|
40
|
-
};
|
46
|
+
export declare const useRootFormStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<FormStoreState>, "setState"> & {
|
47
|
+
setState(nextStateOrUpdater: FormStoreState | Partial<FormStoreState> | ((state: WritableDraft<FormStoreState>) => void), shouldReplace?: boolean | undefined): void;
|
48
|
+
}>;
|
@@ -1,5 +1,3 @@
|
|
1
|
-
import { ControlledFieldState } from "./controlledFieldStore";
|
2
1
|
import { FormState } from "./createFormStore";
|
3
|
-
import { InternalFormId } from "./
|
2
|
+
import { InternalFormId } from "./types";
|
4
3
|
export declare const useFormStore: <T>(formId: InternalFormId, selector: (state: FormState) => T) => T;
|
5
|
-
export declare const useControlledFieldStore: <T>(formId: InternalFormId, selector: (state: ControlledFieldState) => T) => T;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare type InternalFormId = string | symbol;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { FieldErrors, TouchedFields } from "../validation/types";
|
1
|
+
import { FieldErrors, TouchedFields, ValidationResult } from "../validation/types";
|
2
2
|
export declare type FormState = {
|
3
3
|
fieldErrors: FieldErrors;
|
4
4
|
isSubmitting: boolean;
|
@@ -33,7 +33,7 @@ export declare type FormHelpers = {
|
|
33
33
|
/**
|
34
34
|
* Validate the whole form and populate any errors.
|
35
35
|
*/
|
36
|
-
validate: () => Promise<
|
36
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
37
37
|
/**
|
38
38
|
* Clears all errors on the form.
|
39
39
|
*/
|
@@ -45,6 +45,12 @@ export declare type FormHelpers = {
|
|
45
45
|
* or clicking a button element with `type="reset"`.
|
46
46
|
*/
|
47
47
|
reset: () => void;
|
48
|
+
/**
|
49
|
+
* Submits the form, running all validations first.
|
50
|
+
*
|
51
|
+
* _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
|
52
|
+
*/
|
53
|
+
submit: () => void;
|
48
54
|
};
|
49
55
|
/**
|
50
56
|
* Returns helpers that can be used to update the form state.
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { FieldErrors, TouchedFields } from "./validation/types";
|
1
|
+
import { FieldErrors, TouchedFields, ValidationResult } from "./validation/types";
|
2
2
|
export declare type FormContextValue = {
|
3
3
|
/**
|
4
4
|
* All the errors in all the fields in the form.
|
@@ -56,7 +56,7 @@ export declare type FormContextValue = {
|
|
56
56
|
/**
|
57
57
|
* Validate the whole form and populate any errors.
|
58
58
|
*/
|
59
|
-
validate: () => Promise<
|
59
|
+
validate: () => Promise<ValidationResult<unknown>>;
|
60
60
|
/**
|
61
61
|
* Clears all errors on the form.
|
62
62
|
*/
|
@@ -68,6 +68,12 @@ export declare type FormContextValue = {
|
|
68
68
|
* or clicking a button element with `type="reset"`.
|
69
69
|
*/
|
70
70
|
reset: () => void;
|
71
|
+
/**
|
72
|
+
* Submits the form, running all validations first.
|
73
|
+
*
|
74
|
+
* _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
|
75
|
+
*/
|
76
|
+
submit: () => void;
|
71
77
|
};
|
72
78
|
/**
|
73
79
|
* Provides access to some of the internal state of the form.
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "4.
|
3
|
+
"version": "4.5.0-beta.1",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"browser": "./dist/remix-validated-form.cjs.js",
|
6
6
|
"main": "./dist/remix-validated-form.umd.js",
|
@@ -41,9 +41,9 @@
|
|
41
41
|
"@remix-run/react": "^1.2.1",
|
42
42
|
"@remix-run/server-runtime": "^1.2.1",
|
43
43
|
"@types/lodash": "^4.14.178",
|
44
|
-
"@types/react": "^
|
44
|
+
"@types/react": "^18.0.9",
|
45
45
|
"fetch-blob": "^3.1.3",
|
46
|
-
"react": "^
|
46
|
+
"react": "^18.1.0",
|
47
47
|
"tsconfig": "*",
|
48
48
|
"typescript": "^4.5.3",
|
49
49
|
"vite-config": "*"
|
@@ -52,6 +52,6 @@
|
|
52
52
|
"immer": "^9.0.12",
|
53
53
|
"lodash": "^4.17.21",
|
54
54
|
"tiny-invariant": "^1.2.0",
|
55
|
-
"zustand": "^4.0.0-rc.
|
55
|
+
"zustand": "^4.0.0-rc.1"
|
56
56
|
}
|
57
57
|
}
|
package/src/ValidatedForm.tsx
CHANGED
@@ -23,12 +23,12 @@ import {
|
|
23
23
|
useSetFieldErrors,
|
24
24
|
} from "./internal/hooks";
|
25
25
|
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
26
|
-
import {
|
27
|
-
import { SyncedFormProps } from "./internal/state/createFormStore";
|
26
|
+
import { useControlledFieldStore } from "./internal/state/controlledFieldStore";
|
28
27
|
import {
|
29
|
-
|
30
|
-
|
31
|
-
} from "./internal/state/
|
28
|
+
SyncedFormProps,
|
29
|
+
useRootFormStore,
|
30
|
+
} from "./internal/state/createFormStore";
|
31
|
+
import { useFormStore } from "./internal/state/storeHooks";
|
32
32
|
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
33
33
|
import {
|
34
34
|
mergeRefs,
|
@@ -236,23 +236,16 @@ export function ValidatedForm<DataType>({
|
|
236
236
|
const setFieldErrors = useSetFieldErrors(formId);
|
237
237
|
const setFieldError = useFormStore(formId, (state) => state.setFieldError);
|
238
238
|
const reset = useFormStore(formId, (state) => state.reset);
|
239
|
-
const resetControlledFields = useControlledFieldStore(
|
240
|
-
formId,
|
241
|
-
(state) => state.reset
|
242
|
-
);
|
239
|
+
const resetControlledFields = useControlledFieldStore((state) => state.reset);
|
243
240
|
const startSubmit = useFormStore(formId, (state) => state.startSubmit);
|
244
241
|
const endSubmit = useFormStore(formId, (state) => state.endSubmit);
|
245
242
|
const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
|
246
|
-
const setHydrated = useFormStore(formId, (state) => state.setHydrated);
|
247
243
|
const setFormElementInState = useFormStore(
|
248
244
|
formId,
|
249
245
|
(state) => state.setFormElement
|
250
246
|
);
|
251
|
-
|
252
|
-
|
253
|
-
setHydrated();
|
254
|
-
return () => cleanupFormState(formId);
|
255
|
-
}, [formId, setHydrated]);
|
247
|
+
const cleanupForm = useRootFormStore((state) => state.cleanupForm);
|
248
|
+
const registerForm = useRootFormStore((state) => state.registerForm);
|
256
249
|
|
257
250
|
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
258
251
|
const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
|
@@ -266,6 +259,13 @@ export function ValidatedForm<DataType>({
|
|
266
259
|
[customFocusHandlers]
|
267
260
|
);
|
268
261
|
|
262
|
+
// TODO: all these hooks running at startup cause extra, unnecessary renders
|
263
|
+
// There must be a nice way to avoid this.
|
264
|
+
useLayoutEffect(() => {
|
265
|
+
registerForm(formId);
|
266
|
+
return () => cleanupForm(formId);
|
267
|
+
}, [cleanupForm, formId, registerForm]);
|
268
|
+
|
269
269
|
useLayoutEffect(() => {
|
270
270
|
syncFormProps({
|
271
271
|
action,
|
@@ -284,6 +284,10 @@ export function ValidatedForm<DataType>({
|
|
284
284
|
validator,
|
285
285
|
]);
|
286
286
|
|
287
|
+
useLayoutEffect(() => {
|
288
|
+
setFormElementInState(formRef.current);
|
289
|
+
}, [setFormElementInState]);
|
290
|
+
|
287
291
|
useEffect(() => {
|
288
292
|
setFieldErrors(backendError?.fieldErrors ?? {});
|
289
293
|
}, [backendError?.fieldErrors, setFieldErrors, setFieldError]);
|
@@ -292,33 +296,11 @@ export function ValidatedForm<DataType>({
|
|
292
296
|
endSubmit();
|
293
297
|
});
|
294
298
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
function handleClick(event: MouseEvent) {
|
301
|
-
if (!(event.target instanceof HTMLElement)) return;
|
302
|
-
let submitButton = event.target.closest<
|
303
|
-
HTMLButtonElement | HTMLInputElement
|
304
|
-
>("button,input[type=submit]");
|
305
|
-
|
306
|
-
if (
|
307
|
-
submitButton &&
|
308
|
-
submitButton.form === form &&
|
309
|
-
submitButton.type === "submit"
|
310
|
-
) {
|
311
|
-
clickedButtonRef.current = submitButton;
|
312
|
-
}
|
313
|
-
}
|
314
|
-
|
315
|
-
window.addEventListener("click", handleClick, { capture: true });
|
316
|
-
return () => {
|
317
|
-
window.removeEventListener("click", handleClick, { capture: true });
|
318
|
-
};
|
319
|
-
}, []);
|
320
|
-
|
321
|
-
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
299
|
+
const handleSubmit = async (
|
300
|
+
e: FormEvent<HTMLFormElement>,
|
301
|
+
target: typeof e.currentTarget,
|
302
|
+
nativeEvent: HTMLSubmitEvent["nativeEvent"]
|
303
|
+
) => {
|
322
304
|
startSubmit();
|
323
305
|
const result = await validator.validate(getDataFromForm(e.currentTarget));
|
324
306
|
if (result.error) {
|
@@ -339,8 +321,7 @@ export function ValidatedForm<DataType>({
|
|
339
321
|
return;
|
340
322
|
}
|
341
323
|
|
342
|
-
const submitter =
|
343
|
-
.submitter as HTMLFormSubmitter | null;
|
324
|
+
const submitter = nativeEvent.submitter as HTMLFormSubmitter | null;
|
344
325
|
|
345
326
|
// We deviate from the remix code here a bit because of our async submit.
|
346
327
|
// In remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
@@ -348,15 +329,13 @@ export function ValidatedForm<DataType>({
|
|
348
329
|
// If we use `event.currentTarget` here, it will break because `currentTarget`
|
349
330
|
// will have changed since the start of the submission.
|
350
331
|
if (fetcher) fetcher.submit(submitter || e.currentTarget);
|
351
|
-
else submit(submitter ||
|
352
|
-
|
353
|
-
clickedButtonRef.current = null;
|
332
|
+
else submit(submitter || target, { method, replace });
|
354
333
|
}
|
355
334
|
};
|
356
335
|
|
357
336
|
return (
|
358
337
|
<Form
|
359
|
-
ref={mergeRefs([formRef, formRefProp
|
338
|
+
ref={mergeRefs([formRef, formRefProp])}
|
360
339
|
{...rest}
|
361
340
|
id={id}
|
362
341
|
action={action}
|
@@ -364,22 +343,28 @@ export function ValidatedForm<DataType>({
|
|
364
343
|
replace={replace}
|
365
344
|
onSubmit={(e) => {
|
366
345
|
e.preventDefault();
|
367
|
-
handleSubmit(
|
346
|
+
handleSubmit(
|
347
|
+
e,
|
348
|
+
e.currentTarget,
|
349
|
+
(e as unknown as HTMLSubmitEvent).nativeEvent
|
350
|
+
);
|
368
351
|
}}
|
369
352
|
onReset={(event) => {
|
370
353
|
onReset?.(event);
|
371
354
|
if (event.defaultPrevented) return;
|
372
355
|
reset();
|
373
|
-
resetControlledFields();
|
356
|
+
resetControlledFields(formId);
|
374
357
|
}}
|
375
358
|
>
|
376
359
|
<InternalFormContext.Provider value={contextValue}>
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
360
|
+
<>
|
361
|
+
<FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
|
362
|
+
{subaction && (
|
363
|
+
<input type="hidden" value={subaction} name="subaction" />
|
364
|
+
)}
|
365
|
+
{id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
|
366
|
+
{children}
|
367
|
+
</>
|
383
368
|
</InternalFormContext.Provider>
|
384
369
|
</Form>
|
385
370
|
);
|
package/src/internal/hooks.ts
CHANGED
@@ -6,8 +6,8 @@ import { FieldErrors, ValidationErrorResponseData } from "..";
|
|
6
6
|
import { formDefaultValuesKey } from "./constants";
|
7
7
|
import { InternalFormContext, InternalFormContextValue } from "./formContext";
|
8
8
|
import { Hydratable, hydratable } from "./hydratable";
|
9
|
-
import { InternalFormId } from "./state/storeFamily";
|
10
9
|
import { useFormStore } from "./state/storeHooks";
|
10
|
+
import { InternalFormId } from "./state/types";
|
11
11
|
|
12
12
|
export const useInternalFormContext = (
|
13
13
|
formId?: string | symbol,
|
@@ -196,6 +196,9 @@ export const useSetFieldErrors = (formId: InternalFormId) =>
|
|
196
196
|
export const useResetFormElement = (formId: InternalFormId) =>
|
197
197
|
useFormStore(formId, (state) => state.resetFormElement);
|
198
198
|
|
199
|
+
export const useSubmitForm = (formId: InternalFormId) =>
|
200
|
+
useFormStore(formId, (state) => state.submit);
|
201
|
+
|
199
202
|
export const useFormActionProp = (formId: InternalFormId) =>
|
200
203
|
useFormStore(formId, (state) => state.formProps?.action);
|
201
204
|
|
@@ -1,91 +1,112 @@
|
|
1
|
-
import invariant from "tiny-invariant";
|
2
1
|
import create from "zustand";
|
3
2
|
import { immer } from "zustand/middleware/immer";
|
4
|
-
import {
|
3
|
+
import { InternalFormId } from "./types";
|
4
|
+
|
5
|
+
export type FieldState = {
|
6
|
+
refCount: number;
|
7
|
+
value: unknown;
|
8
|
+
defaultValue?: unknown;
|
9
|
+
hydrated: boolean;
|
10
|
+
valueUpdatePromise: Promise<void> | undefined;
|
11
|
+
resolveValueUpdate: (() => void) | undefined;
|
12
|
+
};
|
5
13
|
|
6
14
|
export type ControlledFieldState = {
|
7
|
-
|
8
|
-
[
|
9
|
-
|
|
10
|
-
|
11
|
-
value: unknown;
|
12
|
-
defaultValue?: unknown;
|
13
|
-
hydrated: boolean;
|
14
|
-
valueUpdatePromise: Promise<void> | undefined;
|
15
|
-
resolveValueUpdate: (() => void) | undefined;
|
16
|
-
}
|
17
|
-
| undefined;
|
15
|
+
forms: {
|
16
|
+
[formId: InternalFormId]: {
|
17
|
+
[fieldName: string]: FieldState | undefined;
|
18
|
+
};
|
18
19
|
};
|
19
|
-
register: (fieldName: string) => void;
|
20
|
-
unregister: (fieldName: string) => void;
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
register: (formId: InternalFormId, fieldName: string) => void;
|
21
|
+
unregister: (formId: InternalFormId, fieldName: string) => void;
|
22
|
+
getField: (
|
23
|
+
formId: InternalFormId,
|
24
|
+
fieldName: string
|
25
|
+
) => FieldState | undefined;
|
26
|
+
setValue: (formId: InternalFormId, fieldName: string, value: unknown) => void;
|
27
|
+
hydrateWithDefault: (
|
28
|
+
formId: InternalFormId,
|
29
|
+
fieldName: string,
|
30
|
+
defaultValue: unknown
|
31
|
+
) => void;
|
32
|
+
awaitValueUpdate: (
|
33
|
+
formId: InternalFormId,
|
34
|
+
fieldName: string
|
35
|
+
) => Promise<void>;
|
36
|
+
reset: (formId: InternalFormId) => void;
|
25
37
|
};
|
26
38
|
|
27
|
-
export const
|
28
|
-
|
29
|
-
|
30
|
-
|
39
|
+
export const useControlledFieldStore = create<ControlledFieldState>()(
|
40
|
+
immer((set, get) => ({
|
41
|
+
forms: {},
|
42
|
+
|
43
|
+
register: (formId, field) =>
|
44
|
+
set((state) => {
|
45
|
+
if (!state.forms[formId]) {
|
46
|
+
state.forms[formId] = {};
|
47
|
+
}
|
48
|
+
|
49
|
+
if (state.forms[formId][field]) {
|
50
|
+
state.forms[formId][field]!.refCount++;
|
51
|
+
} else {
|
52
|
+
state.forms[formId][field] = {
|
53
|
+
refCount: 1,
|
54
|
+
value: undefined,
|
55
|
+
hydrated: false,
|
56
|
+
valueUpdatePromise: undefined,
|
57
|
+
resolveValueUpdate: undefined,
|
58
|
+
};
|
59
|
+
}
|
60
|
+
}),
|
31
61
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
state.fields[field] = {
|
38
|
-
refCount: 1,
|
39
|
-
value: undefined,
|
40
|
-
hydrated: false,
|
41
|
-
valueUpdatePromise: undefined,
|
42
|
-
resolveValueUpdate: undefined,
|
43
|
-
};
|
44
|
-
}
|
45
|
-
}),
|
62
|
+
unregister: (formId, field) =>
|
63
|
+
set((state) => {
|
64
|
+
const formState = state.forms?.[formId];
|
65
|
+
const fieldState = formState?.[field];
|
66
|
+
if (!fieldState) return;
|
46
67
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
if (!fieldState) return;
|
68
|
+
fieldState.refCount--;
|
69
|
+
if (fieldState.refCount === 0) delete formState[field];
|
70
|
+
}),
|
51
71
|
|
52
|
-
|
53
|
-
|
54
|
-
|
72
|
+
getField: (formId, field) => {
|
73
|
+
return get().forms?.[formId]?.[field];
|
74
|
+
},
|
55
75
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
76
|
+
setValue: (formId, field, value) =>
|
77
|
+
set((state) => {
|
78
|
+
const fieldState = state.forms?.[formId]?.[field];
|
79
|
+
if (!fieldState) return;
|
60
80
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
81
|
+
fieldState.value = value;
|
82
|
+
const promise = new Promise<void>((resolve) => {
|
83
|
+
fieldState.resolveValueUpdate = resolve;
|
84
|
+
});
|
85
|
+
fieldState.valueUpdatePromise = promise;
|
86
|
+
}),
|
67
87
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
88
|
+
hydrateWithDefault: (formId, field, defaultValue) =>
|
89
|
+
set((state) => {
|
90
|
+
const fieldState = state.forms[formId]?.[field];
|
91
|
+
if (!fieldState) return;
|
72
92
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
93
|
+
fieldState.value = defaultValue;
|
94
|
+
fieldState.defaultValue = defaultValue;
|
95
|
+
fieldState.hydrated = true;
|
96
|
+
}),
|
77
97
|
|
78
|
-
|
79
|
-
|
80
|
-
|
98
|
+
awaitValueUpdate: async (formId, field) => {
|
99
|
+
await get().getField(formId, field)?.valueUpdatePromise;
|
100
|
+
},
|
81
101
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
102
|
+
reset: (formId) =>
|
103
|
+
set((state) => {
|
104
|
+
const formState = state.forms[formId];
|
105
|
+
if (!formState) return;
|
106
|
+
Object.values(formState).forEach((field) => {
|
107
|
+
if (!field) return;
|
108
|
+
field.value = field.defaultValue;
|
109
|
+
});
|
110
|
+
}),
|
111
|
+
}))
|
91
112
|
);
|