@explita/formly 0.1.0 → 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.2] - 2026-01-16
9
+
10
+ ### Added
11
+
12
+ - **Field Arrays**: Full support for dynamic lists with a comprehensive API (`push`, `insert`, `remove`, `swap`, `move`, `moveUp`, `moveDown`, etc.).
13
+ - **Form persistence (Drafts)**: Built-in support for saving and restoring form progress locally using `persistKey`.
14
+ - **Computed Fields**: Support for fields that derive their value from other fields, including wildcard support for arrays (`array.*.field`).
15
+ - **Form Registry**: Ability to access and control any form instance globally by its unique ID.
16
+ - **Improved Performance**: Switched to a flat-state internal representation with precise pub/sub notifications to minimize re-renders.
17
+ - **Deep Path Utilities**: Enhanced handling of nested objects and arrays in form state.
18
+
19
+ ### Changed
20
+
21
+ - Refactored internal state Management for better scalability and performance.
22
+ - Simplified `useForm` initialization logic.
23
+
24
+ ## [0.1.1] - 2026-01-11
25
+
26
+ ### Fixed
27
+
28
+ - Initial maintenance and stabilization improvements.
29
+
30
+ ## [0.1.0] - 2026-01-11
31
+
32
+ ### Added
33
+
34
+ - Initial release of `@explita/formly`.
35
+ - **Core Hooks**:
36
+ - `useForm`: Core hook for form state management, validation, and submission.
37
+ - `useField`: Hook for managing individual field state and interactions.
38
+ - `useFormContext`: Hook for accessing form state within the `Form` provider.
39
+ - `useFormById`: Utility hook to access a form instance globally by its ID.
40
+ - **Components**:
41
+ - `Form`: Provider component for the form context.
42
+ - `Field`: Declarative component for field management.
43
+ - `FieldError`: Component for displaying field validation messages.
44
+ - `FormSpy`: Component to monitor form state changes without triggering global re-renders.
45
+ - `Label`: Accessible label component integrated with form field state.
46
+ - **Validation**:
47
+ - First-class support for `zod` schema validation.
48
+ - **Features**:
49
+ - Support for complex, nested data structures.
50
+ - Optimized performance through precise subscription-based updates.
51
+ - Fully type-safe API for form values, errors, and handlers.
@@ -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,58 @@
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
+ const hasInitializedRef = (0, react_1.useRef)(false);
11
+ (0, react_1.useEffect)(() => {
12
+ var _a, _b;
13
+ const saved = persistKey
14
+ ? (0, utils_2.readDraft)(persistKey)
15
+ : {};
16
+ const merged = (0, utils_1.mergeInitialValues)({
17
+ saved,
18
+ defaults: defaultValues,
19
+ savedFormFirst,
20
+ generatePlaceholders,
21
+ });
22
+ const flattenedDefaults = (0, utils_1.flattenFormValues)(defaultValues);
23
+ const defaultsUnchanged = (0, utils_1.shallowEqual)(previousDefaultValuesRef.current, flattenedDefaults);
24
+ if (defaultsUnchanged)
25
+ return;
26
+ previousDefaultValuesRef.current = flattenedDefaults;
27
+ if (!hasInitializedRef.current) {
28
+ // Restore persisted state
29
+ (_b = (_a = draftListeners.current).restore) === null || _b === void 0 ? void 0 : _b.call(_a, merged);
30
+ // Notify readiness
31
+ onReady === null || onReady === void 0 ? void 0 : onReady(merged, createHandlerContext(merged));
32
+ hasInitializedRef.current = true;
33
+ }
34
+ // Run computed fields
35
+ if (computed) {
36
+ for (const key in computed) {
37
+ const { deps, fn } = computed[key];
38
+ if (key.includes("*")) {
39
+ const parts = key.split("*");
40
+ if (parts.length !== 2)
41
+ continue;
42
+ const [arrayName, fieldName] = parts;
43
+ const arrLength = (0, array_helpers_1.getArrayKeys)(arrayName, merged).length;
44
+ for (let i = 0; i < arrLength; i++) {
45
+ const computedKey = `${arrayName}.${i}.${fieldName}`;
46
+ const fieldDeps = (0, array_helpers_1.extraxtArrayPrefixies)(arrayName, i, deps);
47
+ compute(computedKey, fieldDeps, (vals) => fn(vals, i));
48
+ }
49
+ }
50
+ else {
51
+ compute(key, deps || [], fn);
52
+ }
53
+ }
54
+ }
55
+ // Commit values as source of truth
56
+ setValues({ ...merged }, { overwrite: true }, true);
57
+ }, [defaultValues]);
58
+ }
@@ -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,8 @@ 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");
16
+ const meta_context_1 = require("../lib/meta-context");
55
17
  function useForm(options) {
56
18
  var _a;
57
19
  const { schema, validateOn = "change-submit", defaultValues = {}, errors = {}, mode = "controlled", errorParser, check, computed, onSubmit, onReady, autoFocusOnError = true, savedFormFirst = true, id, } = options || {};
@@ -93,6 +55,7 @@ function useForm(options) {
93
55
  // optional: track unregister for each field
94
56
  const unregisteredRef = (0, react_1.useRef)({});
95
57
  const visibilitySubscribersRef = (0, react_1.useRef)({});
58
+ const previousErrorsRef = (0, react_1.useRef)({});
96
59
  // placeholders
97
60
  const generatePlaceholders = (0, react_1.useMemo)(() => {
98
61
  if (mode === "controlled")
@@ -127,36 +90,13 @@ function useForm(options) {
127
90
  }, [schema]);
128
91
  //set errors on errors change
129
92
  (0, react_1.useEffect)(() => {
93
+ const flattenedErrors = (0, utils_2.flattenFormValues)(errors);
94
+ const errorsUnchanged = (0, utils_2.shallowEqual)(previousErrorsRef.current, flattenedErrors);
95
+ if (errorsUnchanged)
96
+ return;
97
+ previousErrorsRef.current = flattenedErrors;
130
98
  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]);
99
+ }, [errors]);
160
100
  // -----------------------------
161
101
  // Draft hooks
162
102
  // -----------------------------
@@ -769,6 +709,17 @@ function useForm(options) {
769
709
  const markDirty = (0, react_1.useCallback)((name) => {
770
710
  dirtyFieldsRef.current[name] = true;
771
711
  }, []);
712
+ function createHandlerContext(data) {
713
+ return {
714
+ setValues,
715
+ setErrors,
716
+ mapErrors: (errors, path) => setErrors((0, utils_2.mapErrors)(errors, path)),
717
+ reset,
718
+ focus,
719
+ array: (path) => (0, array_helpers_1.handlerArrayHelpers)(path, data),
720
+ meta: formMetadata,
721
+ };
722
+ }
772
723
  const handleSubmit = (0, react_1.useCallback)((onValid) => {
773
724
  return async (event) => {
774
725
  if (event)
@@ -779,15 +730,8 @@ function useForm(options) {
779
730
  return;
780
731
  setIsSubmitting(true);
781
732
  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
- });
733
+ //@ts-ignore
734
+ await onValid(validatedData, createHandlerContext(data));
791
735
  }
792
736
  finally {
793
737
  setIsSubmitting(false);
@@ -871,29 +815,24 @@ function useForm(options) {
871
815
  },
872
816
  };
873
817
  }, []);
874
- const formMetadata = {
875
- get(key) {
876
- return metaRef.current.get(key);
877
- },
878
- set(key, value) {
879
- metaRef.current.set(key, value);
880
- },
881
- delete(key) {
882
- metaRef.current.delete(key);
883
- },
884
- has(key) {
885
- return metaRef.current.has(key);
886
- },
887
- keys() {
888
- return metaRef.current.keys();
889
- },
890
- values() {
891
- return metaRef.current.values();
892
- },
893
- clear() {
894
- metaRef.current.clear();
895
- },
896
- };
818
+ const formMetadata = (0, meta_context_1.createMetaContext)(metaRef, triggerRerender);
819
+ //initialize form
820
+ // Intentionally depends only on defaultValues.
821
+ // Draft restoration & computed logic are internally guarded.
822
+ (0, use_form_initialization_1.useFormInitialization)({
823
+ //@ts-ignore
824
+ defaultValues,
825
+ persistKey,
826
+ savedFormFirst,
827
+ generatePlaceholders,
828
+ //@ts-ignore
829
+ computed,
830
+ draftListeners,
831
+ onReady,
832
+ setValues,
833
+ createHandlerContext,
834
+ compute,
835
+ });
897
836
  const values = {
898
837
  register,
899
838
  validate: async () => {
@@ -929,8 +868,8 @@ function useForm(options) {
929
868
  get errors() {
930
869
  return getErrors();
931
870
  },
932
- isSubmitting,
933
- isValidated,
871
+ submitting: isSubmitting,
872
+ validated: isValidated,
934
873
  isDirty,
935
874
  markDirty,
936
875
  isTouched,
@@ -0,0 +1,11 @@
1
+ export declare function createMetaContext(metaRef: React.RefObject<Map<string, unknown>>, triggerRerender: () => void): {
2
+ get<T = unknown>(key: string): T | undefined;
3
+ set(key: string, value: unknown, opts?: {
4
+ silent?: boolean;
5
+ }): void;
6
+ delete(key: string): void;
7
+ has(key: string): boolean;
8
+ keys(): MapIterator<string>;
9
+ values(): MapIterator<unknown>;
10
+ clear(): void;
11
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMetaContext = createMetaContext;
4
+ function createMetaContext(metaRef, triggerRerender) {
5
+ return {
6
+ get(key) {
7
+ return metaRef.current.get(key);
8
+ },
9
+ set(key, value, opts) {
10
+ metaRef.current.set(key, value);
11
+ if (!(opts === null || opts === void 0 ? void 0 : opts.silent))
12
+ triggerRerender();
13
+ },
14
+ delete(key) {
15
+ metaRef.current.delete(key);
16
+ },
17
+ has(key) {
18
+ return metaRef.current.has(key);
19
+ },
20
+ keys() {
21
+ return metaRef.current.keys();
22
+ },
23
+ values() {
24
+ return metaRef.current.values();
25
+ },
26
+ clear() {
27
+ metaRef.current.clear();
28
+ },
29
+ };
30
+ }
@@ -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,31 @@ 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
+ type ReadyContext<T> = {
68
+ meta: FormMeta;
69
+ };
70
+ export type FormMeta = {
71
+ get<T = unknown>(key: string): T | undefined;
72
+ set: (key: string, value: unknown, options?: {
73
+ silent?: boolean;
74
+ }) => void;
75
+ delete: (key: string) => void;
76
+ has: (key: string) => boolean;
77
+ keys: () => MapIterator<string>;
78
+ values: () => MapIterator<unknown>;
79
+ clear: () => void;
65
80
  };
66
81
  export type FormInstance<T> = {
67
82
  /**
68
83
  * Indicates whether the form is currently submitting.
69
84
  */
70
- isSubmitting: boolean;
85
+ submitting: boolean;
71
86
  /**
72
87
  * Indicates whether the form is validated.
73
88
  */
74
- isValidated: boolean;
89
+ validated: boolean;
75
90
  /**
76
91
  * Retrieves the current values of the form.
77
92
  */
@@ -240,29 +255,50 @@ export type FormInstance<T> = {
240
255
  };
241
256
  Field: typeof Field<T>;
242
257
  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
- };
258
+ meta: FormMeta;
252
259
  };
253
260
  export type SetValues<T> = (values: Partial<T>, options?: {
254
261
  overwrite?: boolean;
255
262
  }) => void;
256
263
  export type SetErrors<T> = (errors?: Partial<Record<Path<T>, string>> | z.ZodError["issues"]) => void;
257
264
  export type FormOptions<TSchema extends ZodObject<any> | undefined, TValues> = {
265
+ /**
266
+ * The schema to use for validation.
267
+ */
258
268
  schema?: TSchema;
269
+ /**
270
+ * The default values of the form.
271
+ */
259
272
  defaultValues?: TValues;
273
+ /**
274
+ * The errors of the form.
275
+ */
260
276
  errors?: Partial<Record<Path<SchemaType<TSchema, TValues>>, string>>;
277
+ /**
278
+ * The error parser to use for parsing errors.
279
+ */
261
280
  errorParser?: (message: string) => string;
281
+ /**
282
+ * The check function to use for checking the form.
283
+ */
262
284
  check?: CheckFn<SchemaType<TSchema, TValues>>;
285
+ /**
286
+ * The computed fields of the form.
287
+ */
263
288
  computed?: Record<string, Computed<SchemaType<TSchema, TValues>>>;
289
+ /**
290
+ * The submit handler of the form.
291
+ */
264
292
  onSubmit?: (values: SchemaType<TSchema, TValues>, ctx: HandlerContext<SchemaType<TSchema, TValues>>) => void;
265
- onReady?: (values: SchemaType<TSchema, TValues>) => void;
293
+ /**
294
+ * Called when the form is ready/mounted.
295
+ */
296
+ onReady?: (values: SchemaType<TSchema, TValues>, ctx: ReadyContext<SchemaType<TSchema, TValues>>) => void;
297
+ /**
298
+ * The mode of the form.
299
+ *
300
+ * @default "uncontrolled"
301
+ */
266
302
  mode?: "controlled" | "uncontrolled";
267
303
  /**
268
304
  * The validation trigger.
@@ -274,8 +310,19 @@ export type FormOptions<TSchema extends ZodObject<any> | undefined, TValues> = {
274
310
  * Used to register the form so it can be accessed outside the hook.
275
311
  */
276
312
  id?: string;
313
+ /**
314
+ * The key used to persist the form state.
315
+ * If provided, the form state will be persisted to localStorage and restored on mount.
316
+ * If id is provided, it will be used as the key.
317
+ */
277
318
  persistKey?: string;
319
+ /**
320
+ * Whether to focus the first field with an error on submit.
321
+ */
278
322
  autoFocusOnError?: boolean;
323
+ /**
324
+ * Whether to use the saved form state first.
325
+ */
279
326
  savedFormFirst?: boolean;
280
327
  };
281
328
  type CheckFn<T> = (data: T, ctx: {
@@ -307,4 +354,23 @@ export type ConditionalConfig<T> = {
307
354
  then?: Partial<ConditionalEffects>;
308
355
  else?: Partial<ConditionalEffects>;
309
356
  };
357
+ export type UseFormInitializationProps = {
358
+ defaultValues: Record<string, any>;
359
+ persistKey?: string;
360
+ savedFormFirst?: boolean;
361
+ generatePlaceholders: Record<string, any>;
362
+ computed?: Record<string, {
363
+ deps?: string[];
364
+ fn: (values: any, index?: number) => any;
365
+ }>;
366
+ compute: (key: string, deps: string[], fn: (values: any) => any) => void;
367
+ draftListeners: React.RefObject<{
368
+ restore?: (values: any) => void;
369
+ }>;
370
+ onReady?: (values: any, ctx: any) => void;
371
+ setValues: (values: Record<string, any>, options?: {
372
+ overwrite?: boolean;
373
+ }, silent?: boolean) => void;
374
+ createHandlerContext: (values: Record<string, any>) => any;
375
+ };
310
376
  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.2",
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",
@@ -48,6 +48,7 @@
48
48
  "files": [
49
49
  "dist",
50
50
  "README.md",
51
+ "CHANGELOG.md",
51
52
  "LICENSE"
52
53
  ]
53
54
  }
package/README.old.md DELETED
@@ -1,141 +0,0 @@
1
- ⚠️ Using register() is convenient for quick input wiring, but for large forms where performance matters, prefer useField or Field to avoid unnecessary re-renders.
2
-
3
- For performance and clarity, here’s the usual pattern I’d recommend:
4
-
5
- <form.Field /> (from form instance) or standalone <Field />
6
-
7
- Best for most use cases.
8
-
9
- Handles binding, errors, and local reactivity automatically.
10
-
11
- Encapsulates the field logic, so the parent form doesn’t rerender on every change.
12
-
13
- Easy to drop into JSX and add labels, wrappers, or custom styling.
14
-
15
- useField() inside a separate component
16
-
17
- Great when you want full programmatic control over the field.
18
-
19
- Can wrap a custom input or a complex component.
20
-
21
- Keeps reactivity local to the component.
22
-
23
- Allows you to do more advanced things like computed values, conditional logic, or side effects specific to that field.
24
-
25
- ✅ Rule of thumb:
26
-
27
- If you just need a simple form input, use <Field /> — minimal boilerplate, good defaults.
28
-
29
- If you need custom behavior or a completely custom component, wrap it with a component using useField().
30
-
31
- This way, the form itself stays light and doesn’t rerender unnecessarily, while each field manages its own state efficiently.
32
-
33
- # @explita/formly
34
-
35
- A powerful and extensible React form hook for building scalable forms with Zod validation, persistence, and full control.
36
-
37
- ## ✨ Features
38
-
39
- - ✅ Built-in Zod schema validation
40
- - ✅ Controlled and uncontrolled modes
41
- - ✅ Persistent form state via `localStorage`
42
- - ✅ Field-level error handling and parsing
43
- - ✅ Debounced input validation
44
- - ✅ Works seamlessly with any UI library (e.g. shadcn/ui)
45
-
46
- ## 📦 Installation
47
-
48
- ```bash
49
- npm install @explita/formly
50
- # or
51
- yarn add @explita/formly
52
- # or
53
- pnpm add @explita/formly
54
- ```
55
-
56
- ## 🧪 Usage
57
-
58
- ```tsx
59
- import { z } from "zod";
60
- import { useForm, Form, Field } from "@explita/formly";
61
- import { Input } from "@/components/ui/input";
62
-
63
- const schema = z.object({
64
- email: z.email({ error: "Invalid email" }),
65
- password: z
66
- .string()
67
- .min(6, { error: "Password must be at least 6 characters" }),
68
- });
69
-
70
- export default function LoginForm() {
71
- const form = useForm({
72
- schema,
73
- defaultValues: { email: "", password: "" },
74
- onSubmit: async (values) => {
75
- console.log("Submitted", values);
76
- // call server action here or perform an HTTP request
77
- // const response = await login(values)
78
- // return response
79
- return values;
80
- },
81
- onSuccess: (result, ctx) => {
82
- console.log("Success", result);
83
- // result is the result of onSubmit
84
- // ctx.reset(); - reset the form, you don't need this if resetOnSuccess is true
85
- },
86
- onError: (error, ctx) => {
87
- console.log("Error", error, ctx);
88
- // error - the error object (usually from schema or server)
89
- // ctx.setErrors({ email: "Email is required" }); - useful for server errors
90
- },
91
- persistKey: "login-form", // Optional – saves input across reloads
92
- errorParser: (msg) => msg, // Optional – customize error messages
93
- mode: "controlled", // Optional – "controlled" is the default
94
- resetOnSuccess: true, // Optional – clears the form on success
95
- });
96
-
97
- //Field meta is an object that contains the value, error, and hasError properties
98
-
99
- return (
100
- <Form use={form}>
101
- <Field name="email" label="Email" isRequired>
102
- {(props, meta) => <Input {...props} />}
103
- </Field>
104
-
105
- <Field name="password" label="Password" isRequired>
106
- {(props, meta) => <Input type="password" {...props} />}
107
- </Field>
108
-
109
- <button type="submit" disabled={form.isSubmitting}>
110
- Submit
111
- </button>
112
- </Form>
113
- );
114
- }
115
- ```
116
-
117
- ## 🧩 API Overview
118
-
119
- ### `useForm(options)`
120
-
121
- | Option | Type | Description |
122
- | ---------------- | ------------------------------------- | ------------------------------------------- |
123
- | `schema` | `ZodObject` | Optional Zod schema for validation |
124
- | `defaultValues` | `Partial<T>` | Initial form values |
125
- | `onSubmit` | `(values, formData) => Promise<void>` | Async submission handler |
126
- | `onSuccess` | `(result) => void` | Called on successful submission |
127
- | `onError` | `(error, ctx) => void` | Called on error, with access to `setErrors` |
128
- | `persistKey` | `string` | Key to store form values under |
129
- | `errorParser` | `(msg: string) => string` | Optional formatter for error messages |
130
- | `mode` | `controlled`\|`uncontrolled` | Default to controlled |
131
- | `resetOnSuccess` | `boolean` | Clear the form on successful submission |
132
-
133
- ### `useFormContext()`
134
-
135
- Can be used in any component nested inside the `Form` component to access the form context.
136
-
137
- ##
138
-
139
- ### 📄 License
140
-
141
- MIT — Made with ❤️ by [Explita](https://explita.ng)