remix-validated-form 4.4.2 → 4.5.0-beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/browser/ValidatedForm.js +24 -39
  3. package/browser/hooks.d.ts +1 -1
  4. package/browser/internal/hooks.d.ts +3 -2
  5. package/browser/internal/hooks.js +1 -0
  6. package/browser/internal/state/controlledFieldStore.d.ts +23 -21
  7. package/browser/internal/state/controlledFieldStore.js +32 -19
  8. package/browser/internal/state/controlledFields.d.ts +3 -3
  9. package/browser/internal/state/controlledFields.js +19 -21
  10. package/browser/internal/state/createFormStore.d.ts +16 -8
  11. package/browser/internal/state/createFormStore.js +62 -8
  12. package/browser/internal/state/storeHooks.d.ts +1 -3
  13. package/browser/internal/state/storeHooks.js +2 -8
  14. package/browser/internal/state/types.d.ts +1 -0
  15. package/browser/internal/state/types.js +1 -0
  16. package/browser/unreleased/formStateHooks.d.ts +8 -2
  17. package/browser/unreleased/formStateHooks.js +12 -2
  18. package/browser/userFacingFormContext.d.ts +8 -2
  19. package/browser/userFacingFormContext.js +15 -4
  20. package/dist/remix-validated-form.cjs.js +3 -3
  21. package/dist/remix-validated-form.cjs.js.map +1 -1
  22. package/dist/remix-validated-form.es.js +169 -113
  23. package/dist/remix-validated-form.es.js.map +1 -1
  24. package/dist/remix-validated-form.umd.js +3 -3
  25. package/dist/remix-validated-form.umd.js.map +1 -1
  26. package/dist/types/hooks.d.ts +1 -1
  27. package/dist/types/internal/hooks.d.ts +3 -2
  28. package/dist/types/internal/state/controlledFieldStore.d.ts +23 -21
  29. package/dist/types/internal/state/controlledFields.d.ts +3 -3
  30. package/dist/types/internal/state/createFormStore.d.ts +16 -8
  31. package/dist/types/internal/state/storeHooks.d.ts +1 -3
  32. package/dist/types/internal/state/types.d.ts +1 -0
  33. package/dist/types/unreleased/formStateHooks.d.ts +8 -2
  34. package/dist/types/userFacingFormContext.d.ts +8 -2
  35. package/package.json +4 -4
  36. package/src/ValidatedForm.tsx +41 -56
  37. package/src/internal/hooks.ts +4 -1
  38. package/src/internal/state/controlledFieldStore.ts +95 -74
  39. package/src/internal/state/controlledFields.ts +38 -26
  40. package/src/internal/state/createFormStore.ts +199 -115
  41. package/src/internal/state/storeHooks.ts +3 -16
  42. package/src/internal/state/types.ts +1 -0
  43. package/src/unreleased/formStateHooks.ts +24 -3
  44. package/src/userFacingFormContext.ts +38 -13
  45. package/dist/types/internal/state/cleanup.d.ts +0 -2
  46. package/dist/types/internal/state/storeFamily.d.ts +0 -9
  47. package/src/internal/state/cleanup.ts +0 -8
  48. package/src/internal/state/storeFamily.ts +0 -24
@@ -1,15 +1,18 @@
1
- $ vite build
2
- vite v2.9.5 building for production...
1
+ $ vite build
2
+ vite v2.9.5 building for production...
3
3
  transforming...
4
- 321 modules transformed.
4
+ ✓ 319 modules transformed.
5
5
  rendering chunks...
6
- dist/remix-validated-form.cjs.js 43.88 KiB / gzip: 16.70 KiB
7
- dist/remix-validated-form.es.js 98.54 KiB / gzip: 23.35 KiB
8
- dist/remix-validated-form.umd.js 44.13 KiB / gzip: 16.84 KiB
9
-
10
- [vite:dts] Start generate declaration files...
11
- [vite:dts] Declaration files built in 1804ms.
12
-
6
+ dist/remix-validated-form.cjs.js  45.48 KiB / gzip: 17.10 KiB
7
+ dist/remix-validated-form.cjs.js.map 252.52 KiB
8
+ dist/remix-validated-form.es.js  101.16 KiB / gzip: 23.79 KiB
9
+ dist/remix-validated-form.es.js.map 260.47 KiB
10
+ dist/remix-validated-form.umd.js  45.73 KiB / gzip: 17.22 KiB
11
+ dist/remix-validated-form.umd.js.map 252.50 KiB
12
+ 
13
+ [vite:dts] Start generate declaration files...
14
+ [vite:dts] Declaration files built in 1849ms.
15
+ 
13
16
  No name was provided for external module 'react' in output.globals – guessing 'React'
14
17
  No name was provided for external module '@remix-run/react' in output.globals – guessing 'react'
15
18
  No name was provided for external module '@remix-run/server-runtime' in output.globals – guessing 'serverRuntime'
@@ -1,14 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Form as RemixForm, useSubmit } from "@remix-run/react";
3
3
  import uniq from "lodash/uniq";
4
- import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
5
5
  import { useIsSubmitting, useIsValid } from "./hooks";
6
6
  import { FORM_ID_FIELD } from "./internal/constants";
7
7
  import { InternalFormContext, } from "./internal/formContext";
8
8
  import { useDefaultValuesFromLoader, useErrorResponseForForm, useHasActiveFormSubmit, useSetFieldErrors, } from "./internal/hooks";
9
9
  import { useMultiValueMap } from "./internal/MultiValueMap";
10
- import { cleanupFormState } from "./internal/state/cleanup";
11
- import { useControlledFieldStore, useFormStore, } from "./internal/state/storeHooks";
10
+ import { useControlledFieldStore } from "./internal/state/controlledFieldStore";
11
+ import { useRootFormStore, } from "./internal/state/createFormStore";
12
+ import { useFormStore } from "./internal/state/storeHooks";
12
13
  import { useSubmitComplete } from "./internal/submissionCallbacks";
13
14
  import { mergeRefs, useDeepEqualsMemo, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
14
15
  const getDataFromForm = (el) => new FormData(el);
@@ -114,16 +115,13 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
114
115
  const setFieldErrors = useSetFieldErrors(formId);
115
116
  const setFieldError = useFormStore(formId, (state) => state.setFieldError);
116
117
  const reset = useFormStore(formId, (state) => state.reset);
117
- const resetControlledFields = useControlledFieldStore(formId, (state) => state.reset);
118
+ const resetControlledFields = useControlledFieldStore((state) => state.reset);
118
119
  const startSubmit = useFormStore(formId, (state) => state.startSubmit);
119
120
  const endSubmit = useFormStore(formId, (state) => state.endSubmit);
120
121
  const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
121
- const setHydrated = useFormStore(formId, (state) => state.setHydrated);
122
122
  const setFormElementInState = useFormStore(formId, (state) => state.setFormElement);
123
- useEffect(() => {
124
- setHydrated();
125
- return () => cleanupFormState(formId);
126
- }, [formId, setHydrated]);
123
+ const cleanupForm = useRootFormStore((state) => state.cleanupForm);
124
+ const registerForm = useRootFormStore((state) => state.registerForm);
127
125
  const customFocusHandlers = useMultiValueMap();
128
126
  const registerReceiveFocus = useCallback((fieldName, handler) => {
129
127
  customFocusHandlers().add(fieldName, handler);
@@ -131,6 +129,12 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
131
129
  customFocusHandlers().remove(fieldName, handler);
132
130
  };
133
131
  }, [customFocusHandlers]);
132
+ // TODO: all these hooks running at startup cause extra, unnecessary renders
133
+ // There must be a nice way to avoid this.
134
+ useLayoutEffect(() => {
135
+ registerForm(formId);
136
+ return () => cleanupForm(formId);
137
+ }, [cleanupForm, formId, registerForm]);
134
138
  useLayoutEffect(() => {
135
139
  var _a;
136
140
  syncFormProps({
@@ -149,6 +153,9 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
149
153
  backendDefaultValues,
150
154
  validator,
151
155
  ]);
156
+ useLayoutEffect(() => {
157
+ setFormElementInState(formRef.current);
158
+ }, [setFormElementInState]);
152
159
  useEffect(() => {
153
160
  var _a;
154
161
  setFieldErrors((_a = backendError === null || backendError === void 0 ? void 0 : backendError.fieldErrors) !== null && _a !== void 0 ? _a : {});
@@ -156,27 +163,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
156
163
  useSubmitComplete(hasActiveSubmission, () => {
157
164
  endSubmit();
158
165
  });
159
- let clickedButtonRef = React.useRef();
160
- useEffect(() => {
161
- let form = formRef.current;
162
- if (!form)
163
- return;
164
- function handleClick(event) {
165
- if (!(event.target instanceof HTMLElement))
166
- return;
167
- let submitButton = event.target.closest("button,input[type=submit]");
168
- if (submitButton &&
169
- submitButton.form === form &&
170
- submitButton.type === "submit") {
171
- clickedButtonRef.current = submitButton;
172
- }
173
- }
174
- window.addEventListener("click", handleClick, { capture: true });
175
- return () => {
176
- window.removeEventListener("click", handleClick, { capture: true });
177
- };
178
- }, []);
179
- const handleSubmit = async (e) => {
166
+ const handleSubmit = async (e, target, nativeEvent) => {
180
167
  startSubmit();
181
168
  const result = await validator.validate(getDataFromForm(e.currentTarget));
182
169
  if (result.error) {
@@ -193,8 +180,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
193
180
  endSubmit();
194
181
  return;
195
182
  }
196
- const submitter = e.nativeEvent
197
- .submitter;
183
+ const submitter = nativeEvent.submitter;
198
184
  // We deviate from the remix code here a bit because of our async submit.
199
185
  // In remix's `FormImpl`, they use `event.currentTarget` to get the form,
200
186
  // but we already have the form in `formRef.current` so we can just use that.
@@ -203,18 +189,17 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
203
189
  if (fetcher)
204
190
  fetcher.submit(submitter || e.currentTarget);
205
191
  else
206
- submit(submitter || e.currentTarget, { method, replace });
207
- clickedButtonRef.current = null;
192
+ submit(submitter || target, { method, replace });
208
193
  }
209
194
  };
210
- return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp, setFormElementInState]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
195
+ return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
211
196
  e.preventDefault();
212
- handleSubmit(e);
197
+ handleSubmit(e, e.currentTarget, e.nativeEvent);
213
198
  }, onReset: (event) => {
214
199
  onReset === null || onReset === void 0 ? void 0 : onReset(event);
215
200
  if (event.defaultPrevented)
216
201
  return;
217
202
  reset();
218
- resetControlledFields();
219
- }, children: _jsxs(InternalFormContext.Provider, { value: contextValue, children: [_jsx(FormResetter, { formRef: formRef, resetAfterSubmit: resetAfterSubmit }, void 0), subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), id && _jsx("input", { type: "hidden", value: id, name: FORM_ID_FIELD }, void 0), children] }, void 0) }, void 0));
203
+ resetControlledFields(formId);
204
+ }, children: _jsx(InternalFormContext.Provider, { value: contextValue, children: _jsxs(_Fragment, { children: [_jsx(FormResetter, { formRef: formRef, resetAfterSubmit: resetAfterSubmit }, void 0), subaction && (_jsx("input", { type: "hidden", value: subaction, name: "subaction" }, void 0)), id && _jsx("input", { type: "hidden", value: id, name: FORM_ID_FIELD }, void 0), children] }, void 0) }, void 0) }, void 0));
220
205
  }
@@ -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) => (fieldName: string, value: unknown) => void;
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/storeFamily";
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<void>;
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;
@@ -113,5 +113,6 @@ export const useTouchedFields = (formId) => useFormStore(formId, (state) => stat
113
113
  export const useFieldErrors = (formId) => useFormStore(formId, (state) => state.fieldErrors);
114
114
  export const useSetFieldErrors = (formId) => useFormStore(formId, (state) => state.setFieldErrors);
115
115
  export const useResetFormElement = (formId) => useFormStore(formId, (state) => state.resetFormElement);
116
+ export const useSubmitForm = (formId) => useFormStore(formId, (state) => state.submit);
116
117
  export const useFormActionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.action; });
117
118
  export const useFormSubactionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.subaction; });
@@ -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
- fields: {
3
- [fieldName: string]: {
4
- refCount: number;
5
- value: unknown;
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
- setValue: (fieldName: string, value: unknown) => void;
15
- hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
16
- awaitValueUpdate: (fieldName: string) => Promise<void>;
17
- reset: () => void;
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,14 +1,16 @@
1
1
  import create from "zustand";
2
2
  import { immer } from "zustand/middleware/immer";
3
- import { storeFamily } from "./storeFamily";
4
- export const controlledFieldStore = storeFamily(() => create()(immer((set, get, api) => ({
5
- fields: {},
6
- register: (field) => set((state) => {
7
- if (state.fields[field]) {
8
- state.fields[field].refCount++;
3
+ export const useControlledFieldStore = create()(immer((set, get) => ({
4
+ forms: {},
5
+ register: (formId, field) => set((state) => {
6
+ if (!state.forms[formId]) {
7
+ state.forms[formId] = {};
8
+ }
9
+ if (state.forms[formId][field]) {
10
+ state.forms[formId][field].refCount++;
9
11
  }
10
12
  else {
11
- state.fields[field] = {
13
+ state.forms[formId][field] = {
12
14
  refCount: 1,
13
15
  value: undefined,
14
16
  hydrated: false,
@@ -17,16 +19,23 @@ export const controlledFieldStore = storeFamily(() => create()(immer((set, get,
17
19
  };
18
20
  }
19
21
  }),
20
- unregister: (field) => set((state) => {
21
- const fieldState = state.fields[field];
22
+ unregister: (formId, field) => set((state) => {
23
+ var _a;
24
+ const formState = (_a = state.forms) === null || _a === void 0 ? void 0 : _a[formId];
25
+ const fieldState = formState === null || formState === void 0 ? void 0 : formState[field];
22
26
  if (!fieldState)
23
27
  return;
24
28
  fieldState.refCount--;
25
29
  if (fieldState.refCount === 0)
26
- delete state.fields[field];
30
+ delete formState[field];
27
31
  }),
28
- setValue: (field, value) => set((state) => {
29
- const fieldState = state.fields[field];
32
+ getField: (formId, field) => {
33
+ var _a, _b;
34
+ return (_b = (_a = get().forms) === null || _a === void 0 ? void 0 : _a[formId]) === null || _b === void 0 ? void 0 : _b[field];
35
+ },
36
+ setValue: (formId, field, value) => set((state) => {
37
+ var _a, _b;
38
+ const fieldState = (_b = (_a = state.forms) === null || _a === void 0 ? void 0 : _a[formId]) === null || _b === void 0 ? void 0 : _b[field];
30
39
  if (!fieldState)
31
40
  return;
32
41
  fieldState.value = value;
@@ -35,23 +44,27 @@ export const controlledFieldStore = storeFamily(() => create()(immer((set, get,
35
44
  });
36
45
  fieldState.valueUpdatePromise = promise;
37
46
  }),
38
- hydrateWithDefault: (field, defaultValue) => set((state) => {
39
- const fieldState = state.fields[field];
47
+ hydrateWithDefault: (formId, field, defaultValue) => set((state) => {
48
+ var _a;
49
+ const fieldState = (_a = state.forms[formId]) === null || _a === void 0 ? void 0 : _a[field];
40
50
  if (!fieldState)
41
51
  return;
42
52
  fieldState.value = defaultValue;
43
53
  fieldState.defaultValue = defaultValue;
44
54
  fieldState.hydrated = true;
45
55
  }),
46
- awaitValueUpdate: async (field) => {
56
+ awaitValueUpdate: async (formId, field) => {
47
57
  var _a;
48
- await ((_a = get().fields[field]) === null || _a === void 0 ? void 0 : _a.valueUpdatePromise);
58
+ await ((_a = get().getField(formId, field)) === null || _a === void 0 ? void 0 : _a.valueUpdatePromise);
49
59
  },
50
- reset: () => set((state) => {
51
- Object.values(state.fields).forEach((field) => {
60
+ reset: (formId) => set((state) => {
61
+ const formState = state.forms[formId];
62
+ if (!formState)
63
+ return;
64
+ Object.values(formState).forEach((field) => {
52
65
  if (!field)
53
66
  return;
54
67
  field.value = field.defaultValue;
55
68
  });
56
69
  }),
57
- }))));
70
+ })));
@@ -1,6 +1,6 @@
1
1
  import { InternalFormContextValue } from "../formContext";
2
- import { InternalFormId } from "./storeFamily";
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) => (fieldName: string, value: unknown) => void;
6
- export declare const useAwaitValue: (formId: InternalFormId) => (fieldName: string) => Promise<void>;
5
+ export declare const useUpdateControllableValue: (formId: InternalFormId) => (field: string, value: unknown) => void;
6
+ export declare const useAwaitValue: (formId: InternalFormId) => (field: string) => Promise<void>;
@@ -1,20 +1,19 @@
1
1
  import { useCallback, useEffect } from "react";
2
2
  import { useFieldDefaultValue } from "../hooks";
3
- import { controlledFieldStore } from "./controlledFieldStore";
4
- import { formStore } from "./createFormStore";
3
+ import { useControlledFieldStore } from "./controlledFieldStore";
4
+ import { useFormStore } from "./storeHooks";
5
5
  export const useControlledFieldValue = (context, field) => {
6
- const useValueStore = controlledFieldStore(context.formId);
7
- const value = useValueStore((state) => { var _a; return (_a = state.fields[field]) === null || _a === void 0 ? void 0 : _a.value; });
8
- const useFormStore = formStore(context.formId);
9
- const isFormHydrated = useFormStore((state) => state.isHydrated);
6
+ const value = useControlledFieldStore((state) => { var _a; return (_a = state.getField(context.formId, field)) === null || _a === void 0 ? void 0 : _a.value; });
7
+ const isFormHydrated = useFormStore(context.formId, (state) => state.isHydrated);
10
8
  const defaultValue = useFieldDefaultValue(field, context);
11
- const isFieldHydrated = useValueStore((state) => { var _a, _b; return (_b = (_a = state.fields[field]) === null || _a === void 0 ? void 0 : _a.hydrated) !== null && _b !== void 0 ? _b : false; });
12
- const hydrateWithDefault = useValueStore((state) => state.hydrateWithDefault);
9
+ const isFieldHydrated = useControlledFieldStore((state) => { var _a, _b; return (_b = (_a = state.getField(context.formId, field)) === null || _a === void 0 ? void 0 : _a.hydrated) !== null && _b !== void 0 ? _b : false; });
10
+ const hydrateWithDefault = useControlledFieldStore((state) => state.hydrateWithDefault);
13
11
  useEffect(() => {
14
12
  if (isFormHydrated && !isFieldHydrated) {
15
- hydrateWithDefault(field, defaultValue);
13
+ hydrateWithDefault(context.formId, field, defaultValue);
16
14
  }
17
15
  }, [
16
+ context.formId,
18
17
  defaultValue,
19
18
  field,
20
19
  hydrateWithDefault,
@@ -24,27 +23,26 @@ export const useControlledFieldValue = (context, field) => {
24
23
  return isFieldHydrated ? value : defaultValue;
25
24
  };
26
25
  export const useControllableValue = (context, field) => {
27
- const useValueStore = controlledFieldStore(context.formId);
28
- const resolveUpdate = useValueStore((state) => { var _a; return (_a = state.fields[field]) === null || _a === void 0 ? void 0 : _a.resolveValueUpdate; });
26
+ const resolveUpdate = useControlledFieldStore((state) => { var _a; return (_a = state.getField(context.formId, field)) === null || _a === void 0 ? void 0 : _a.resolveValueUpdate; });
29
27
  useEffect(() => {
30
28
  resolveUpdate === null || resolveUpdate === void 0 ? void 0 : resolveUpdate();
31
29
  }, [resolveUpdate]);
32
- const register = useValueStore((state) => state.register);
33
- const unregister = useValueStore((state) => state.unregister);
30
+ const register = useControlledFieldStore((state) => state.register);
31
+ const unregister = useControlledFieldStore((state) => state.unregister);
34
32
  useEffect(() => {
35
- register(field);
36
- return () => unregister(field);
33
+ register(context.formId, field);
34
+ return () => unregister(context.formId, field);
37
35
  }, [context.formId, field, register, unregister]);
38
- const setControlledFieldValue = useValueStore((state) => state.setValue);
39
- const setValue = useCallback((value) => setControlledFieldValue(field, value), [field, setControlledFieldValue]);
36
+ const setControlledFieldValue = useControlledFieldStore((state) => state.setValue);
37
+ const setValue = useCallback((value) => setControlledFieldValue(context.formId, field, value), [context.formId, field, setControlledFieldValue]);
40
38
  const value = useControlledFieldValue(context, field);
41
39
  return [value, setValue];
42
40
  };
43
41
  export const useUpdateControllableValue = (formId) => {
44
- const useValueStore = controlledFieldStore(formId);
45
- return useValueStore((state) => state.setValue);
42
+ const setValue = useControlledFieldStore((state) => state.setValue);
43
+ return useCallback((field, value) => setValue(formId, field, value), [formId, setValue]);
46
44
  };
47
45
  export const useAwaitValue = (formId) => {
48
- const useValueStore = controlledFieldStore(formId);
49
- return useValueStore((state) => state.awaitValueUpdate);
46
+ const awaitValue = useControlledFieldStore((state) => state.awaitValueUpdate);
47
+ return useCallback((field) => awaitValue(formId, field), [awaitValue, formId]);
50
48
  };
@@ -1,4 +1,6 @@
1
- import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
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<void>;
42
+ validate: () => Promise<ValidationResult<unknown>>;
33
43
  resetFormElement: () => void;
44
+ submit: () => void;
34
45
  };
35
- export declare const formStore: {
36
- (formId: import("./storeFamily").InternalFormId): import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<FormState>, "setState"> & {
37
- setState(nextStateOrUpdater: FormState | Partial<FormState> | ((state: import("immer/dist/internal").WritableDraft<FormState>) => void), shouldReplace?: boolean | undefined): void;
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,9 +1,37 @@
1
1
  import invariant from "tiny-invariant";
2
2
  import create from "zustand";
3
3
  import { immer } from "zustand/middleware/immer";
4
- import { controlledFieldStore } from "./controlledFieldStore";
5
- import { storeFamily } from "./storeFamily";
6
- export const formStore = storeFamily((formId) => create()(immer((set, get, api) => ({
4
+ import { useControlledFieldStore } from "./controlledFieldStore";
5
+ const noOp = () => { };
6
+ const defaultFormState = {
7
+ isHydrated: false,
8
+ isSubmitting: false,
9
+ hasBeenSubmitted: false,
10
+ touchedFields: {},
11
+ fieldErrors: {},
12
+ formElement: null,
13
+ isValid: () => true,
14
+ startSubmit: noOp,
15
+ endSubmit: noOp,
16
+ setTouched: noOp,
17
+ setFieldError: noOp,
18
+ setFieldErrors: noOp,
19
+ clearFieldError: noOp,
20
+ reset: () => noOp,
21
+ syncFormProps: noOp,
22
+ setHydrated: noOp,
23
+ setFormElement: noOp,
24
+ validateField: async () => null,
25
+ validate: async () => {
26
+ throw new Error("Validate called before form was initialized.");
27
+ },
28
+ submit: async () => {
29
+ throw new Error("Submit called before form was initialized.");
30
+ },
31
+ resetFormElement: noOp,
32
+ };
33
+ const createFormState = (formId, set, get) => ({
34
+ // It's not "hydrated" until the form props are synced
7
35
  isHydrated: false,
8
36
  isSubmitting: false,
9
37
  hasBeenSubmitted: false,
@@ -37,6 +65,7 @@ export const formStore = storeFamily((formId) => create()(immer((set, get, api)
37
65
  }),
38
66
  syncFormProps: (props) => set((state) => {
39
67
  state.formProps = props;
68
+ state.isHydrated = true;
40
69
  }),
41
70
  setHydrated: () => set((state) => {
42
71
  state.isHydrated = true;
@@ -58,7 +87,7 @@ export const formStore = storeFamily((formId) => create()(immer((set, get, api)
58
87
  invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
59
88
  const validator = (_a = get().formProps) === null || _a === void 0 ? void 0 : _a.validator;
60
89
  invariant(validator, "Cannot validator. This is probably a bug in remix-validated-form.");
61
- await ((_c = (_b = controlledFieldStore(formId).getState()).awaitValueUpdate) === null || _c === void 0 ? void 0 : _c.call(_b, field));
90
+ await ((_c = (_b = useControlledFieldStore.getState()).awaitValueUpdate) === null || _c === void 0 ? void 0 : _c.call(_b, formId, field));
62
91
  const { error } = await validator.validateField(new FormData(formElement), field);
63
92
  if (error) {
64
93
  get().setFieldError(field, error);
@@ -75,9 +104,34 @@ export const formStore = storeFamily((formId) => create()(immer((set, get, api)
75
104
  invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
76
105
  const validator = (_a = get().formProps) === null || _a === void 0 ? void 0 : _a.validator;
77
106
  invariant(validator, "Cannot validator. This is probably a bug in remix-validated-form.");
78
- const { error } = await validator.validate(new FormData(formElement));
79
- if (error)
80
- get().setFieldErrors(error.fieldErrors);
107
+ const result = await validator.validate(new FormData(formElement));
108
+ if (result.error)
109
+ get().setFieldErrors(result.error.fieldErrors);
110
+ return result;
111
+ },
112
+ submit: () => {
113
+ const formElement = get().formElement;
114
+ invariant(formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form.");
115
+ formElement.submit();
81
116
  },
82
117
  resetFormElement: () => { var _a; return (_a = get().formElement) === null || _a === void 0 ? void 0 : _a.reset(); },
83
- }))));
118
+ });
119
+ export const useRootFormStore = create()(immer((set, get) => ({
120
+ forms: {},
121
+ form: (formId) => {
122
+ var _a;
123
+ return (_a = get().forms[formId]) !== null && _a !== void 0 ? _a : defaultFormState;
124
+ },
125
+ cleanupForm: (formId) => {
126
+ set((state) => {
127
+ delete state.forms[formId];
128
+ });
129
+ },
130
+ registerForm: (formId) => {
131
+ if (get().forms[formId])
132
+ return;
133
+ set((state) => {
134
+ state.forms[formId] = createFormState(formId, (setter) => set((state) => setter(state.forms[formId])), () => get().forms[formId]);
135
+ });
136
+ },
137
+ })));
@@ -1,5 +1,3 @@
1
- import { ControlledFieldState } from "./controlledFieldStore";
2
1
  import { FormState } from "./createFormStore";
3
- import { InternalFormId } from "./storeFamily";
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;
@@ -1,10 +1,4 @@
1
- import { controlledFieldStore, } from "./controlledFieldStore";
2
- import { formStore } from "./createFormStore";
1
+ import { useRootFormStore } from "./createFormStore";
3
2
  export const useFormStore = (formId, selector) => {
4
- const useStore = formStore(formId);
5
- return useStore(selector);
6
- };
7
- export const useControlledFieldStore = (formId, selector) => {
8
- const useStore = controlledFieldStore(formId);
9
- return useStore(selector);
3
+ return useRootFormStore((state) => selector(state.form(formId)));
10
4
  };
@@ -0,0 +1 @@
1
+ export declare type InternalFormId = string | symbol;
@@ -0,0 +1 @@
1
+ export {};
@@ -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<void>;
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.