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.
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
@@ -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;
@@ -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,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,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,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;
@@ -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<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.
@@ -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<void>;
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.4.2",
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": "^17.0.37",
44
+ "@types/react": "^18.0.9",
45
45
  "fetch-blob": "^3.1.3",
46
- "react": "^17.0.2",
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.0"
55
+ "zustand": "^4.0.0-rc.1"
56
56
  }
57
57
  }
@@ -23,12 +23,12 @@ import {
23
23
  useSetFieldErrors,
24
24
  } from "./internal/hooks";
25
25
  import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
26
- import { cleanupFormState } from "./internal/state/cleanup";
27
- import { SyncedFormProps } from "./internal/state/createFormStore";
26
+ import { useControlledFieldStore } from "./internal/state/controlledFieldStore";
28
27
  import {
29
- useControlledFieldStore,
30
- useFormStore,
31
- } from "./internal/state/storeHooks";
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
- useEffect(() => {
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
- let clickedButtonRef = React.useRef<any>();
296
- useEffect(() => {
297
- let form = formRef.current;
298
- if (!form) return;
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 = (e as unknown as HTMLSubmitEvent).nativeEvent
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 || e.currentTarget, { method, replace });
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, setFormElementInState])}
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(e);
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
- <FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
378
- {subaction && (
379
- <input type="hidden" value={subaction} name="subaction" />
380
- )}
381
- {id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
382
- {children}
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
  );
@@ -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 { storeFamily } from "./storeFamily";
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
- fields: {
8
- [fieldName: string]:
9
- | {
10
- refCount: number;
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
- setValue: (fieldName: string, value: unknown) => void;
22
- hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
23
- awaitValueUpdate: (fieldName: string) => Promise<void>;
24
- reset: () => void;
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 controlledFieldStore = storeFamily(() =>
28
- create<ControlledFieldState>()(
29
- immer((set, get, api) => ({
30
- fields: {},
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
- register: (field) =>
33
- set((state) => {
34
- if (state.fields[field]) {
35
- state.fields[field]!.refCount++;
36
- } else {
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
- unregister: (field) =>
48
- set((state) => {
49
- const fieldState = state.fields[field];
50
- if (!fieldState) return;
68
+ fieldState.refCount--;
69
+ if (fieldState.refCount === 0) delete formState[field];
70
+ }),
51
71
 
52
- fieldState.refCount--;
53
- if (fieldState.refCount === 0) delete state.fields[field];
54
- }),
72
+ getField: (formId, field) => {
73
+ return get().forms?.[formId]?.[field];
74
+ },
55
75
 
56
- setValue: (field, value) =>
57
- set((state) => {
58
- const fieldState = state.fields[field];
59
- if (!fieldState) return;
76
+ setValue: (formId, field, value) =>
77
+ set((state) => {
78
+ const fieldState = state.forms?.[formId]?.[field];
79
+ if (!fieldState) return;
60
80
 
61
- fieldState.value = value;
62
- const promise = new Promise<void>((resolve) => {
63
- fieldState.resolveValueUpdate = resolve;
64
- });
65
- fieldState.valueUpdatePromise = promise;
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
- hydrateWithDefault: (field, defaultValue) =>
69
- set((state) => {
70
- const fieldState = state.fields[field];
71
- if (!fieldState) return;
88
+ hydrateWithDefault: (formId, field, defaultValue) =>
89
+ set((state) => {
90
+ const fieldState = state.forms[formId]?.[field];
91
+ if (!fieldState) return;
72
92
 
73
- fieldState.value = defaultValue;
74
- fieldState.defaultValue = defaultValue;
75
- fieldState.hydrated = true;
76
- }),
93
+ fieldState.value = defaultValue;
94
+ fieldState.defaultValue = defaultValue;
95
+ fieldState.hydrated = true;
96
+ }),
77
97
 
78
- awaitValueUpdate: async (field) => {
79
- await get().fields[field]?.valueUpdatePromise;
80
- },
98
+ awaitValueUpdate: async (formId, field) => {
99
+ await get().getField(formId, field)?.valueUpdatePromise;
100
+ },
81
101
 
82
- reset: () =>
83
- set((state) => {
84
- Object.values(state.fields).forEach((field) => {
85
- if (!field) return;
86
- field.value = field.defaultValue;
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
  );