@explita/formly 0.1.1 → 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.
@@ -7,6 +7,7 @@ const react_1 = require("react");
7
7
  const utils_2 = require("../utils");
8
8
  function useFormInitialization({ defaultValues, persistKey, savedFormFirst, generatePlaceholders, computed, draftListeners, compute, onReady, setValues, createHandlerContext, }) {
9
9
  const previousDefaultValuesRef = (0, react_1.useRef)({});
10
+ const hasInitializedRef = (0, react_1.useRef)(false);
10
11
  (0, react_1.useEffect)(() => {
11
12
  var _a, _b;
12
13
  const saved = persistKey
@@ -18,15 +19,18 @@ function useFormInitialization({ defaultValues, persistKey, savedFormFirst, gene
18
19
  savedFormFirst,
19
20
  generatePlaceholders,
20
21
  });
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
22
  const flattenedDefaults = (0, utils_1.flattenFormValues)(defaultValues);
26
23
  const defaultsUnchanged = (0, utils_1.shallowEqual)(previousDefaultValuesRef.current, flattenedDefaults);
27
24
  if (defaultsUnchanged)
28
25
  return;
29
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
+ }
30
34
  // Run computed fields
31
35
  if (computed) {
32
36
  for (const key in computed) {
@@ -13,6 +13,7 @@ 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
15
  const use_form_initialization_1 = require("./use-form-initialization");
16
+ const meta_context_1 = require("../lib/meta-context");
16
17
  function useForm(options) {
17
18
  var _a;
18
19
  const { schema, validateOn = "change-submit", defaultValues = {}, errors = {}, mode = "controlled", errorParser, check, computed, onSubmit, onReady, autoFocusOnError = true, savedFormFirst = true, id, } = options || {};
@@ -814,31 +815,7 @@ function useForm(options) {
814
815
  },
815
816
  };
816
817
  }, []);
817
- const formMetadata = {
818
- get(key) {
819
- return metaRef.current.get(key);
820
- },
821
- set(key, value, opts) {
822
- metaRef.current.set(key, value);
823
- if (!(opts === null || opts === void 0 ? void 0 : opts.silent))
824
- triggerRerender();
825
- },
826
- delete(key) {
827
- metaRef.current.delete(key);
828
- },
829
- has(key) {
830
- return metaRef.current.has(key);
831
- },
832
- keys() {
833
- return metaRef.current.keys();
834
- },
835
- values() {
836
- return metaRef.current.values();
837
- },
838
- clear() {
839
- metaRef.current.clear();
840
- },
841
- };
818
+ const formMetadata = (0, meta_context_1.createMetaContext)(metaRef, triggerRerender);
842
819
  //initialize form
843
820
  // Intentionally depends only on defaultValues.
844
821
  // Draft restoration & computed logic are internally guarded.
@@ -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
+ }
@@ -64,6 +64,9 @@ export type HandlerContext<T> = {
64
64
  array: <P extends Path<T>>(path: P) => HandlerArrayHelpers<ArrayItem<T, P>>;
65
65
  meta: FormMeta;
66
66
  };
67
+ type ReadyContext<T> = {
68
+ meta: FormMeta;
69
+ };
67
70
  export type FormMeta = {
68
71
  get<T = unknown>(key: string): T | undefined;
69
72
  set: (key: string, value: unknown, options?: {
@@ -259,14 +262,43 @@ export type SetValues<T> = (values: Partial<T>, options?: {
259
262
  }) => void;
260
263
  export type SetErrors<T> = (errors?: Partial<Record<Path<T>, string>> | z.ZodError["issues"]) => void;
261
264
  export type FormOptions<TSchema extends ZodObject<any> | undefined, TValues> = {
265
+ /**
266
+ * The schema to use for validation.
267
+ */
262
268
  schema?: TSchema;
269
+ /**
270
+ * The default values of the form.
271
+ */
263
272
  defaultValues?: TValues;
273
+ /**
274
+ * The errors of the form.
275
+ */
264
276
  errors?: Partial<Record<Path<SchemaType<TSchema, TValues>>, string>>;
277
+ /**
278
+ * The error parser to use for parsing errors.
279
+ */
265
280
  errorParser?: (message: string) => string;
281
+ /**
282
+ * The check function to use for checking the form.
283
+ */
266
284
  check?: CheckFn<SchemaType<TSchema, TValues>>;
285
+ /**
286
+ * The computed fields of the form.
287
+ */
267
288
  computed?: Record<string, Computed<SchemaType<TSchema, TValues>>>;
289
+ /**
290
+ * The submit handler of the form.
291
+ */
268
292
  onSubmit?: (values: SchemaType<TSchema, TValues>, ctx: HandlerContext<SchemaType<TSchema, TValues>>) => void;
269
- onReady?: (values: SchemaType<TSchema, TValues>, ctx: HandlerContext<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
+ */
270
302
  mode?: "controlled" | "uncontrolled";
271
303
  /**
272
304
  * The validation trigger.
@@ -278,8 +310,19 @@ export type FormOptions<TSchema extends ZodObject<any> | undefined, TValues> = {
278
310
  * Used to register the form so it can be accessed outside the hook.
279
311
  */
280
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
+ */
281
318
  persistKey?: string;
319
+ /**
320
+ * Whether to focus the first field with an error on submit.
321
+ */
282
322
  autoFocusOnError?: boolean;
323
+ /**
324
+ * Whether to use the saved form state first.
325
+ */
283
326
  savedFormFirst?: boolean;
284
327
  };
285
328
  type CheckFn<T> = (data: T, ctx: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explita/formly",
3
- "version": "0.1.1",
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)