@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 +51 -0
- package/dist/hooks/use-form-initialization.js +8 -4
- package/dist/hooks/use-form.js +2 -25
- package/dist/lib/meta-context.d.ts +11 -0
- package/dist/lib/meta-context.js +30 -0
- package/dist/types/utils.d.ts +44 -1
- package/package.json +2 -1
- package/README.old.md +0 -141
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) {
|
package/dist/hooks/use-form.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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)
|