@explita/formly 0.1.0 → 0.1.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.
@@ -0,0 +1,2 @@
1
+ import { UseFormInitializationProps } from "../types/utils";
2
+ export declare function useFormInitialization({ defaultValues, persistKey, savedFormFirst, generatePlaceholders, computed, draftListeners, compute, onReady, setValues, createHandlerContext, }: UseFormInitializationProps): void;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFormInitialization = useFormInitialization;
4
+ const array_helpers_1 = require("../lib/array-helpers");
5
+ const utils_1 = require("../lib/utils");
6
+ const react_1 = require("react");
7
+ const utils_2 = require("../utils");
8
+ function useFormInitialization({ defaultValues, persistKey, savedFormFirst, generatePlaceholders, computed, draftListeners, compute, onReady, setValues, createHandlerContext, }) {
9
+ const previousDefaultValuesRef = (0, react_1.useRef)({});
10
+ (0, react_1.useEffect)(() => {
11
+ var _a, _b;
12
+ const saved = persistKey
13
+ ? (0, utils_2.readDraft)(persistKey)
14
+ : {};
15
+ const merged = (0, utils_1.mergeInitialValues)({
16
+ saved,
17
+ defaults: defaultValues,
18
+ savedFormFirst,
19
+ generatePlaceholders,
20
+ });
21
+ // Restore persisted state
22
+ (_b = (_a = draftListeners.current).restore) === null || _b === void 0 ? void 0 : _b.call(_a, merged);
23
+ // Notify readiness
24
+ onReady === null || onReady === void 0 ? void 0 : onReady(merged, createHandlerContext(merged));
25
+ const flattenedDefaults = (0, utils_1.flattenFormValues)(defaultValues);
26
+ const defaultsUnchanged = (0, utils_1.shallowEqual)(previousDefaultValuesRef.current, flattenedDefaults);
27
+ if (defaultsUnchanged)
28
+ return;
29
+ previousDefaultValuesRef.current = flattenedDefaults;
30
+ // Run computed fields
31
+ if (computed) {
32
+ for (const key in computed) {
33
+ const { deps, fn } = computed[key];
34
+ if (key.includes("*")) {
35
+ const parts = key.split("*");
36
+ if (parts.length !== 2)
37
+ continue;
38
+ const [arrayName, fieldName] = parts;
39
+ const arrLength = (0, array_helpers_1.getArrayKeys)(arrayName, merged).length;
40
+ for (let i = 0; i < arrLength; i++) {
41
+ const computedKey = `${arrayName}.${i}.${fieldName}`;
42
+ const fieldDeps = (0, array_helpers_1.extraxtArrayPrefixies)(arrayName, i, deps);
43
+ compute(computedKey, fieldDeps, (vals) => fn(vals, i));
44
+ }
45
+ }
46
+ else {
47
+ compute(key, deps || [], fn);
48
+ }
49
+ }
50
+ }
51
+ // Commit values as source of truth
52
+ setValues({ ...merged }, { overwrite: true }, true);
53
+ }, [defaultValues]);
54
+ }
@@ -1,43 +1,3 @@
1
1
  import type { z } from "zod";
2
2
  import { FormInstance, FormValues } from "../types/utils";
3
- /**
4
- * useForm is a React hook that allows you to manage form state and validation.
5
- *
6
- * @param schema - a Zod schema object that defines the form structure and validation rules.
7
- * @param defaultValues - an object containing the default values for the form fields.
8
- * @param errors - an object containing the initial errors for the form fields.
9
- * @param mode - the form mode, either "controlled" or "uncontrolled".
10
- * @param errorParser - a function that takes an error and returns a string to be displayed to the user.
11
- * @param persistKey - a string that defines the key under which the form state is persisted.
12
- * @param check - a function that takes the form values and an object containing the focus method for each field.
13
- * @param onSubmit - a function that takes the form values and an object containing the focus method for each field.
14
- * @param persistKey - a string that defines the key under which the form state is persisted.
15
- * @param computed - an object containing the computed fields.
16
- * @param autoFocusOnError - a boolean that defines whether to focus on the first error field.
17
- * @param savedFormFirst - a boolean that defines whether to prioritize the saved form state.
18
- * @param validateOn - a string that defines when to validate the form.
19
- *
20
- * @returns an object containing the useForm API methods.
21
- *
22
- * @example
23
- * const form = useForm({
24
- * schema,
25
- * defaultValues,
26
- * errors,
27
- * mode,
28
- * errorParser,
29
- * persistKey,
30
- * check,
31
- * onSubmit: async(values)=>{}
32
- * });
33
- *
34
- * <Form use={form}>
35
- * {formFields.map((field) => (
36
- * <div key={field.name}>
37
- * <label>{field.label}</label>
38
- * <input type="text" {...register(field.name)} />
39
- * </div>
40
- * ))}
41
- * </Form>
42
- */
43
3
  export declare function useForm<TSchema extends z.ZodObject<any> | undefined = undefined, TDefault = TSchema extends z.ZodObject<any> ? z.infer<TSchema> : Record<string, any>>(options?: FormValues<TSchema, TDefault>): FormInstance<TSchema extends z.ZodObject<any> ? z.infer<TSchema> : TDefault>;
@@ -12,46 +12,7 @@ const group_helpers_1 = require("../lib/group-helpers");
12
12
  const components_1 = require("../components");
13
13
  const pub_sub_1 = require("../lib/pub-sub");
14
14
  const form_registry_1 = require("../lib/form-registry");
15
- /**
16
- * useForm is a React hook that allows you to manage form state and validation.
17
- *
18
- * @param schema - a Zod schema object that defines the form structure and validation rules.
19
- * @param defaultValues - an object containing the default values for the form fields.
20
- * @param errors - an object containing the initial errors for the form fields.
21
- * @param mode - the form mode, either "controlled" or "uncontrolled".
22
- * @param errorParser - a function that takes an error and returns a string to be displayed to the user.
23
- * @param persistKey - a string that defines the key under which the form state is persisted.
24
- * @param check - a function that takes the form values and an object containing the focus method for each field.
25
- * @param onSubmit - a function that takes the form values and an object containing the focus method for each field.
26
- * @param persistKey - a string that defines the key under which the form state is persisted.
27
- * @param computed - an object containing the computed fields.
28
- * @param autoFocusOnError - a boolean that defines whether to focus on the first error field.
29
- * @param savedFormFirst - a boolean that defines whether to prioritize the saved form state.
30
- * @param validateOn - a string that defines when to validate the form.
31
- *
32
- * @returns an object containing the useForm API methods.
33
- *
34
- * @example
35
- * const form = useForm({
36
- * schema,
37
- * defaultValues,
38
- * errors,
39
- * mode,
40
- * errorParser,
41
- * persistKey,
42
- * check,
43
- * onSubmit: async(values)=>{}
44
- * });
45
- *
46
- * <Form use={form}>
47
- * {formFields.map((field) => (
48
- * <div key={field.name}>
49
- * <label>{field.label}</label>
50
- * <input type="text" {...register(field.name)} />
51
- * </div>
52
- * ))}
53
- * </Form>
54
- */
15
+ const use_form_initialization_1 = require("./use-form-initialization");
55
16
  function useForm(options) {
56
17
  var _a;
57
18
  const { schema, validateOn = "change-submit", defaultValues = {}, errors = {}, mode = "controlled", errorParser, check, computed, onSubmit, onReady, autoFocusOnError = true, savedFormFirst = true, id, } = options || {};
@@ -93,6 +54,7 @@ function useForm(options) {
93
54
  // optional: track unregister for each field
94
55
  const unregisteredRef = (0, react_1.useRef)({});
95
56
  const visibilitySubscribersRef = (0, react_1.useRef)({});
57
+ const previousErrorsRef = (0, react_1.useRef)({});
96
58
  // placeholders
97
59
  const generatePlaceholders = (0, react_1.useMemo)(() => {
98
60
  if (mode === "controlled")
@@ -127,36 +89,13 @@ function useForm(options) {
127
89
  }, [schema]);
128
90
  //set errors on errors change
129
91
  (0, react_1.useEffect)(() => {
92
+ const flattenedErrors = (0, utils_2.flattenFormValues)(errors);
93
+ const errorsUnchanged = (0, utils_2.shallowEqual)(previousErrorsRef.current, flattenedErrors);
94
+ if (errorsUnchanged)
95
+ return;
96
+ previousErrorsRef.current = flattenedErrors;
130
97
  setErrors(errors);
131
- }, []);
132
- //set default values on mount
133
- (0, react_1.useEffect)(() => {
134
- var _a, _b;
135
- const saved = persistKey ? (0, utils_1.readDraft)(persistKey) : {};
136
- const primary = savedFormFirst ? saved : defaultValues;
137
- const secondary = savedFormFirst ? defaultValues : saved;
138
- const merged = (0, utils_2.mergeValues)((0, utils_2.mergeValues)(primary || {}, secondary || {}), generatePlaceholders);
139
- (_b = (_a = draftListeners.current).restore) === null || _b === void 0 ? void 0 : _b.call(_a, merged);
140
- onReady === null || onReady === void 0 ? void 0 : onReady(merged);
141
- if (computed) {
142
- for (const key in computed) {
143
- const { deps, fn } = computed[key];
144
- if (key.includes("*")) {
145
- const [arrayName, fieldName] = key.split("*");
146
- const arrLength = (0, array_helpers_1.getArrayKeys)(arrayName, merged).length;
147
- for (let i = 0; i < arrLength; i++) {
148
- const computedKey = `${arrayName}.${i}.${fieldName}`;
149
- const fieldDeps = (0, array_helpers_1.extraxtArrayPrefixies)(arrayName, i, deps);
150
- compute(computedKey, fieldDeps, (vals) => fn(vals, i));
151
- }
152
- }
153
- else {
154
- compute(key, deps || [], fn);
155
- }
156
- }
157
- }
158
- setValues({ ...merged }, { overwrite: true }, true);
159
- }, [defaultValues]);
98
+ }, [errors]);
160
99
  // -----------------------------
161
100
  // Draft hooks
162
101
  // -----------------------------
@@ -769,6 +708,17 @@ function useForm(options) {
769
708
  const markDirty = (0, react_1.useCallback)((name) => {
770
709
  dirtyFieldsRef.current[name] = true;
771
710
  }, []);
711
+ function createHandlerContext(data) {
712
+ return {
713
+ setValues,
714
+ setErrors,
715
+ mapErrors: (errors, path) => setErrors((0, utils_2.mapErrors)(errors, path)),
716
+ reset,
717
+ focus,
718
+ array: (path) => (0, array_helpers_1.handlerArrayHelpers)(path, data),
719
+ meta: formMetadata,
720
+ };
721
+ }
772
722
  const handleSubmit = (0, react_1.useCallback)((onValid) => {
773
723
  return async (event) => {
774
724
  if (event)
@@ -779,15 +729,8 @@ function useForm(options) {
779
729
  return;
780
730
  setIsSubmitting(true);
781
731
  const data = structuredClone(validatedData);
782
- await onValid(validatedData, {
783
- setValues,
784
- setErrors,
785
- mapErrors: (errors, path) => setErrors((0, utils_2.mapErrors)(errors, path)),
786
- reset,
787
- focus,
788
- //@ts-ignore
789
- array: (path) => (0, array_helpers_1.handlerArrayHelpers)(path, data),
790
- });
732
+ //@ts-ignore
733
+ await onValid(validatedData, createHandlerContext(data));
791
734
  }
792
735
  finally {
793
736
  setIsSubmitting(false);
@@ -875,8 +818,10 @@ function useForm(options) {
875
818
  get(key) {
876
819
  return metaRef.current.get(key);
877
820
  },
878
- set(key, value) {
821
+ set(key, value, opts) {
879
822
  metaRef.current.set(key, value);
823
+ if (!(opts === null || opts === void 0 ? void 0 : opts.silent))
824
+ triggerRerender();
880
825
  },
881
826
  delete(key) {
882
827
  metaRef.current.delete(key);
@@ -894,6 +839,23 @@ function useForm(options) {
894
839
  metaRef.current.clear();
895
840
  },
896
841
  };
842
+ //initialize form
843
+ // Intentionally depends only on defaultValues.
844
+ // Draft restoration & computed logic are internally guarded.
845
+ (0, use_form_initialization_1.useFormInitialization)({
846
+ //@ts-ignore
847
+ defaultValues,
848
+ persistKey,
849
+ savedFormFirst,
850
+ generatePlaceholders,
851
+ //@ts-ignore
852
+ computed,
853
+ draftListeners,
854
+ onReady,
855
+ setValues,
856
+ createHandlerContext,
857
+ compute,
858
+ });
897
859
  const values = {
898
860
  register,
899
861
  validate: async () => {
@@ -929,8 +891,8 @@ function useForm(options) {
929
891
  get errors() {
930
892
  return getErrors();
931
893
  },
932
- isSubmitting,
933
- isValidated,
894
+ submitting: isSubmitting,
895
+ validated: isValidated,
934
896
  isDirty,
935
897
  markDirty,
936
898
  isTouched,
@@ -15,3 +15,10 @@ export declare function flattenFormValues<T extends any>(obj: T, prefix?: string
15
15
  export declare function nestFormValues<T extends any>(flat: T): T;
16
16
  export declare function mergeValues<T extends any>(defaultValues: T, saved: T): T;
17
17
  export declare function determineDirtyFields<T extends any>(defaultValues: T, saved: T): T;
18
+ export declare function shallowEqual(a: Record<string, any>, b: Record<string, any>): boolean;
19
+ export declare function mergeInitialValues({ saved, defaults, savedFormFirst, generatePlaceholders, }: {
20
+ saved: Record<string, any>;
21
+ defaults: Record<string, any>;
22
+ generatePlaceholders: Record<string, any>;
23
+ savedFormFirst?: boolean;
24
+ }): Record<string, any>;
package/dist/lib/utils.js CHANGED
@@ -7,6 +7,8 @@ exports.flattenFormValues = flattenFormValues;
7
7
  exports.nestFormValues = nestFormValues;
8
8
  exports.mergeValues = mergeValues;
9
9
  exports.determineDirtyFields = determineDirtyFields;
10
+ exports.shallowEqual = shallowEqual;
11
+ exports.mergeInitialValues = mergeInitialValues;
10
12
  /**
11
13
  * Combines multiple class names into a single string.
12
14
  * This function takes an array of class names and returns a single string
@@ -188,3 +190,13 @@ function determineDirtyFields(defaultValues, saved) {
188
190
  }
189
191
  return result;
190
192
  }
193
+ function shallowEqual(a, b) {
194
+ const aKeys = Object.keys(a);
195
+ const bKeys = Object.keys(b);
196
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
197
+ }
198
+ function mergeInitialValues({ saved, defaults, savedFormFirst, generatePlaceholders, }) {
199
+ const primary = savedFormFirst ? saved : defaults;
200
+ const secondary = savedFormFirst ? defaults : saved;
201
+ return mergeValues(mergeValues(primary || {}, secondary || {}), generatePlaceholders);
202
+ }
@@ -62,16 +62,28 @@ export type HandlerContext<T> = {
62
62
  reset: () => void;
63
63
  focus: (name: Path<T>) => void;
64
64
  array: <P extends Path<T>>(path: P) => HandlerArrayHelpers<ArrayItem<T, P>>;
65
+ meta: FormMeta;
66
+ };
67
+ export type FormMeta = {
68
+ get<T = unknown>(key: string): T | undefined;
69
+ set: (key: string, value: unknown, options?: {
70
+ silent?: boolean;
71
+ }) => void;
72
+ delete: (key: string) => void;
73
+ has: (key: string) => boolean;
74
+ keys: () => MapIterator<string>;
75
+ values: () => MapIterator<unknown>;
76
+ clear: () => void;
65
77
  };
66
78
  export type FormInstance<T> = {
67
79
  /**
68
80
  * Indicates whether the form is currently submitting.
69
81
  */
70
- isSubmitting: boolean;
82
+ submitting: boolean;
71
83
  /**
72
84
  * Indicates whether the form is validated.
73
85
  */
74
- isValidated: boolean;
86
+ validated: boolean;
75
87
  /**
76
88
  * Retrieves the current values of the form.
77
89
  */
@@ -240,15 +252,7 @@ export type FormInstance<T> = {
240
252
  };
241
253
  Field: typeof Field<T>;
242
254
  channel: ReturnType<typeof createFormBus<T>>["channel"];
243
- meta: {
244
- get<T = unknown>(key: string): T | undefined;
245
- set: (key: string, value: unknown) => void;
246
- delete: (key: string) => void;
247
- has: (key: string) => boolean;
248
- keys: () => MapIterator<string>;
249
- values: () => MapIterator<unknown>;
250
- clear: () => void;
251
- };
255
+ meta: FormMeta;
252
256
  };
253
257
  export type SetValues<T> = (values: Partial<T>, options?: {
254
258
  overwrite?: boolean;
@@ -262,7 +266,7 @@ export type FormOptions<TSchema extends ZodObject<any> | undefined, TValues> = {
262
266
  check?: CheckFn<SchemaType<TSchema, TValues>>;
263
267
  computed?: Record<string, Computed<SchemaType<TSchema, TValues>>>;
264
268
  onSubmit?: (values: SchemaType<TSchema, TValues>, ctx: HandlerContext<SchemaType<TSchema, TValues>>) => void;
265
- onReady?: (values: SchemaType<TSchema, TValues>) => void;
269
+ onReady?: (values: SchemaType<TSchema, TValues>, ctx: HandlerContext<SchemaType<TSchema, TValues>>) => void;
266
270
  mode?: "controlled" | "uncontrolled";
267
271
  /**
268
272
  * The validation trigger.
@@ -307,4 +311,23 @@ export type ConditionalConfig<T> = {
307
311
  then?: Partial<ConditionalEffects>;
308
312
  else?: Partial<ConditionalEffects>;
309
313
  };
314
+ export type UseFormInitializationProps = {
315
+ defaultValues: Record<string, any>;
316
+ persistKey?: string;
317
+ savedFormFirst?: boolean;
318
+ generatePlaceholders: Record<string, any>;
319
+ computed?: Record<string, {
320
+ deps?: string[];
321
+ fn: (values: any, index?: number) => any;
322
+ }>;
323
+ compute: (key: string, deps: string[], fn: (values: any) => any) => void;
324
+ draftListeners: React.RefObject<{
325
+ restore?: (values: any) => void;
326
+ }>;
327
+ onReady?: (values: any, ctx: any) => void;
328
+ setValues: (values: Record<string, any>, options?: {
329
+ overwrite?: boolean;
330
+ }, silent?: boolean) => void;
331
+ createHandlerContext: (values: Record<string, any>) => any;
332
+ };
310
333
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explita/formly",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A lightweight form toolkit for React built with developer ergonomics in mind. Includes a flexible Form component, `useForm`, `useField`, and `useFormContext` hooks for managing form state and validation with ease. Designed to simplify complex forms while remaining unopinionated and extensible.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",