@dynamic-field-kit/core 1.0.0 → 1.0.3
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/README.md +299 -0
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Dynamic Field Kit
|
|
2
|
+
|
|
3
|
+
A lightweight, extensible **dynamic form engine** for React, built for scalable applications and design systems.
|
|
4
|
+
|
|
5
|
+
`dynamic-field-kit` lets you define forms using **configuration objects** instead of hard-coded UI, and allows applications to **freely extend field types** (`text`, `number`, `checkbox`, `select`, `date`, `custom`, …) without modifying the library.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- Schema-driven dynamic forms
|
|
12
|
+
- Extensible field types (no enums, no hard-coded unions)
|
|
13
|
+
- Pluggable field renderers via registry
|
|
14
|
+
- Runtime conditional fields (`appearCondition`)
|
|
15
|
+
- Clean TypeScript declarations (DTS-safe)
|
|
16
|
+
- Core logic separated from React rendering
|
|
17
|
+
- Ideal for form builders & design systems
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Packages
|
|
22
|
+
|
|
23
|
+
| Package | Description |
|
|
24
|
+
|------|------------|
|
|
25
|
+
| `@dynamic-field-kit/core` | Core types and field registry |
|
|
26
|
+
| `@dynamic-field-kit/react` | React components (FieldInput, MultiFieldInput, DynamicInput) |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 📥 Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @dynamic-field-kit/core @dynamic-field-kit/react
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Peer dependency**
|
|
37
|
+
|
|
38
|
+
```txt
|
|
39
|
+
react >= 17
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🧱 Core Concepts
|
|
45
|
+
|
|
46
|
+
The library **does NOT define field types** like:
|
|
47
|
+
```ts
|
|
48
|
+
"text" | "number"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Instead, it exposes an **extendable interface** that applications can augment:
|
|
52
|
+
```ts
|
|
53
|
+
export interface FieldTypeMap {}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This allows:
|
|
57
|
+
- Unlimited custom field types
|
|
58
|
+
- Strong typing without locking consumers
|
|
59
|
+
- No need to rebuild the library
|
|
60
|
+
|
|
61
|
+
This pattern is used by mature libraries like **MUI, React Hook Form,** and **Redux Toolkit**.
|
|
62
|
+
|
|
63
|
+
## 🧩 Defining Field Types (App Side)
|
|
64
|
+
|
|
65
|
+
Create a `.d.ts` file in your app (e.g. src/types/dynamic-field.d.ts):
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import "@dynamic-field-kit/core"
|
|
69
|
+
|
|
70
|
+
declare module "@dynamic-field-kit/core" {
|
|
71
|
+
interface FieldTypeMap {
|
|
72
|
+
text: string
|
|
73
|
+
number: number
|
|
74
|
+
checkbox: boolean
|
|
75
|
+
select: string
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
⚠️ Make sure this file is included in tsconfig.json.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## FieldRendererProps
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export interface FieldRendererProps<T = any> {
|
|
87
|
+
value?: T
|
|
88
|
+
onValueChange?: (value: T) => void
|
|
89
|
+
label?: string
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
👉 A common contract for all field renderers
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### FieldDescription
|
|
97
|
+
|
|
98
|
+
A `FieldDescription` defines **what a field is**, not **how it looks**.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { FieldDescription } from "@dynamic-field-kit/core"
|
|
102
|
+
|
|
103
|
+
const fields: FieldDescription[] = [
|
|
104
|
+
{
|
|
105
|
+
name: "username",
|
|
106
|
+
type: "text",
|
|
107
|
+
label: "Username"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "age",
|
|
111
|
+
type: "number",
|
|
112
|
+
label: "Age",
|
|
113
|
+
appearCondition: (data) => data.username !== ""
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Common Properties**
|
|
119
|
+
| Property | Description |
|
|
120
|
+
|------|------------|
|
|
121
|
+
| name | Field key in form data |
|
|
122
|
+
| type | Field renderer key |
|
|
123
|
+
| label | UI label |
|
|
124
|
+
| value | Default value |
|
|
125
|
+
| appearCondition | Runtime visibility condition |
|
|
126
|
+
|
|
127
|
+
**Field Registry (Render Layer)**
|
|
128
|
+
The library does **not** ship UI components.
|
|
129
|
+
|
|
130
|
+
Instead, applications register their own renderers.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { fieldRegistry } from "@dynamic-field-kit/core"
|
|
134
|
+
|
|
135
|
+
fieldRegistry.register("text", ({ value, onValueChange, label }) => (
|
|
136
|
+
<div>
|
|
137
|
+
<label>{label}</label>
|
|
138
|
+
<input
|
|
139
|
+
value={value ?? ""}
|
|
140
|
+
onChange={(e) => onValueChange?.(e.target.value)}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
fieldRegistry.register("checkbox", ({ value, onValueChange, label }) => (
|
|
146
|
+
<label>
|
|
147
|
+
<input
|
|
148
|
+
type="checkbox"
|
|
149
|
+
checked={!!value}
|
|
150
|
+
onChange={(e) => onValueChange?.(e.target.checked)}
|
|
151
|
+
/>
|
|
152
|
+
{label}
|
|
153
|
+
</label>
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## ⚛️ React Usage
|
|
161
|
+
|
|
162
|
+
**MultiFieldInput (Main Form Engine)**
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { MultiFieldInput } from "@dynamic-field-kit/react"
|
|
166
|
+
import { FieldDescription } from "@dynamic-field-kit/core"
|
|
167
|
+
|
|
168
|
+
const fields: FieldDescription[] = [
|
|
169
|
+
{ name: "email", type: "text", label: "Email" },
|
|
170
|
+
{ name: "age", type: "number", label: "Age" }
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
const Example = () => {
|
|
174
|
+
return (
|
|
175
|
+
<MultiFieldInput
|
|
176
|
+
fieldDescriptions={fields}
|
|
177
|
+
onChange={(data) => {
|
|
178
|
+
console.log("Form data:", data)
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Controlled Form**
|
|
186
|
+
```tsx
|
|
187
|
+
const [formData, setFormData] = useState({})
|
|
188
|
+
|
|
189
|
+
<MultiFieldInput
|
|
190
|
+
fieldDescriptions={fields}
|
|
191
|
+
properties={formData}
|
|
192
|
+
onChange={setFormData}
|
|
193
|
+
/>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## ➕ Adding a New Field Type
|
|
199
|
+
|
|
200
|
+
You **do not** need to modify the library.
|
|
201
|
+
|
|
202
|
+
Just extend `FieldTypeMap`:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
declare module "@dynamic-field-kit/react" {
|
|
206
|
+
interface FieldTypeMap {
|
|
207
|
+
date: Date
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Then register a renderer:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
fieldRegistry.register("date", ({ value, onValueChange }) => (
|
|
216
|
+
<input
|
|
217
|
+
type="date"
|
|
218
|
+
value={value ? value.toISOString().slice(0, 10) : ""}
|
|
219
|
+
onChange={(e) =>
|
|
220
|
+
onValueChange?.(new Date(e.target.value))
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Now `"date"` is fully type-safe everywhere.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
## 🧠 Domain Typing (Optional)
|
|
230
|
+
The library intentionally avoids enforcing domain types.
|
|
231
|
+
If you want strict typing, cast inside your app:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
interface UserForm {
|
|
235
|
+
age: number
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const fields: FieldDescription[] = [
|
|
239
|
+
{
|
|
240
|
+
name: "age",
|
|
241
|
+
type: "number",
|
|
242
|
+
appearCondition: (data) =>
|
|
243
|
+
(data as UserForm).age > 18
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
```
|
|
247
|
+
This keeps the library generic while allowing strict typing in the app.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 🧩 Components API
|
|
252
|
+
|
|
253
|
+
**<DynamicInput />**
|
|
254
|
+
|
|
255
|
+
Resolves and renders a field based on its type.
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
<DynamicInput type="text" value="hello" />
|
|
259
|
+
```
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
**<FieldInput />**
|
|
263
|
+
|
|
264
|
+
Renders a single field with value binding.
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
<FieldInput
|
|
268
|
+
fieldDescription={field}
|
|
269
|
+
renderInfos={formData}
|
|
270
|
+
onChange={(value, key) => {}}
|
|
271
|
+
/>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Demo
|
|
275
|
+
- [Examples](https://github.com/vannt-dev/dynamic-field-kit-demo)
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 🏗 Architecture
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
dynamic-field-kit
|
|
283
|
+
├─ packages/
|
|
284
|
+
│ ├─ core # framework-agnostic types
|
|
285
|
+
│ └─ react # React renderer + registry
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 🚫 Non-Goals
|
|
291
|
+
This library intentionally does not include:
|
|
292
|
+
-Built-in UI components
|
|
293
|
+
-Built-in UI components
|
|
294
|
+
-Form state management library
|
|
295
|
+
|
|
296
|
+
It is a **form engine**, not a full form framework.
|
|
297
|
+
|
|
298
|
+
## 📄 License
|
|
299
|
+
MIT © vannt-dev
|
package/dist/index.d.mts
CHANGED
|
@@ -16,12 +16,12 @@ interface FieldDescription<T extends FieldTypeKey = FieldTypeKey> {
|
|
|
16
16
|
appearCondition?: (data: Properties) => boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type
|
|
19
|
+
type FieldRenderer<T = any> = (props: FieldRendererProps<T>) => JSX.Element;
|
|
20
20
|
declare class FieldRegistry {
|
|
21
|
-
private
|
|
22
|
-
register<
|
|
23
|
-
get(type:
|
|
21
|
+
private registry;
|
|
22
|
+
register<K extends keyof FieldTypeMap>(type: K, renderer: FieldRenderer<FieldTypeMap[K]>): void;
|
|
23
|
+
get<K extends keyof FieldTypeMap>(type: K): FieldRenderer<FieldTypeMap[K]> | undefined;
|
|
24
24
|
}
|
|
25
25
|
declare const fieldRegistry: FieldRegistry;
|
|
26
26
|
|
|
27
|
-
export { type FieldDescription, type FieldRendererProps, type FieldTypeKey, type FieldTypeMap, type Properties, fieldRegistry };
|
|
27
|
+
export { type FieldDescription, FieldRegistry, type FieldRenderer, type FieldRendererProps, type FieldTypeKey, type FieldTypeMap, type Properties, fieldRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -16,12 +16,12 @@ interface FieldDescription<T extends FieldTypeKey = FieldTypeKey> {
|
|
|
16
16
|
appearCondition?: (data: Properties) => boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type
|
|
19
|
+
type FieldRenderer<T = any> = (props: FieldRendererProps<T>) => JSX.Element;
|
|
20
20
|
declare class FieldRegistry {
|
|
21
|
-
private
|
|
22
|
-
register<
|
|
23
|
-
get(type:
|
|
21
|
+
private registry;
|
|
22
|
+
register<K extends keyof FieldTypeMap>(type: K, renderer: FieldRenderer<FieldTypeMap[K]>): void;
|
|
23
|
+
get<K extends keyof FieldTypeMap>(type: K): FieldRenderer<FieldTypeMap[K]> | undefined;
|
|
24
24
|
}
|
|
25
25
|
declare const fieldRegistry: FieldRegistry;
|
|
26
26
|
|
|
27
|
-
export { type FieldDescription, type FieldRendererProps, type FieldTypeKey, type FieldTypeMap, type Properties, fieldRegistry };
|
|
27
|
+
export { type FieldDescription, FieldRegistry, type FieldRenderer, type FieldRendererProps, type FieldTypeKey, type FieldTypeMap, type Properties, fieldRegistry };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
FieldRegistry: () => FieldRegistry,
|
|
23
24
|
fieldRegistry: () => fieldRegistry
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -27,18 +28,21 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
28
|
// src/fieldRegistry.ts
|
|
28
29
|
var FieldRegistry = class {
|
|
29
30
|
constructor() {
|
|
30
|
-
|
|
31
|
+
// ❗ INTERNAL = string, KHÔNG constrain
|
|
32
|
+
this.registry = {};
|
|
31
33
|
}
|
|
34
|
+
// ✅ Type safety ở API
|
|
32
35
|
register(type, renderer) {
|
|
33
|
-
this.
|
|
36
|
+
this.registry[type] = renderer;
|
|
34
37
|
}
|
|
35
38
|
get(type) {
|
|
36
|
-
return this.
|
|
39
|
+
return this.registry[type];
|
|
37
40
|
}
|
|
38
41
|
};
|
|
39
42
|
var fieldRegistry = new FieldRegistry();
|
|
40
43
|
// Annotate the CommonJS export names for ESM import in node:
|
|
41
44
|
0 && (module.exports = {
|
|
45
|
+
FieldRegistry,
|
|
42
46
|
fieldRegistry
|
|
43
47
|
});
|
|
44
48
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/fieldRegistry.ts"],"sourcesContent":["export * from \"./types\"\r\nexport * from \"./fieldRegistry\"\r\n","import { JSX } from \"react\"\r\nimport { FieldRendererProps,
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/fieldRegistry.ts"],"sourcesContent":["export * from \"./types\"\r\nexport * from \"./fieldRegistry\"\r\n","import { JSX } from \"react\"\r\nimport type { FieldRendererProps, FieldTypeMap } from \"./types\"\r\n\r\nexport type FieldRenderer<T = any> = (props: FieldRendererProps<T>) => JSX.Element\r\n\r\nexport class FieldRegistry {\r\n // ❗ INTERNAL = string, KHÔNG constrain\r\n private registry: Record<string, FieldRenderer<any>> = {}\r\n\r\n // ✅ Type safety ở API\r\n register<K extends keyof FieldTypeMap>(\r\n type: K,\r\n renderer: FieldRenderer<FieldTypeMap[K]>\r\n ) {\r\n this.registry[type as string] = renderer\r\n }\r\n\r\n get<K extends keyof FieldTypeMap>(\r\n type: K\r\n ): FieldRenderer<FieldTypeMap[K]> | undefined {\r\n return this.registry[type as string]\r\n }\r\n}\r\n\r\nexport const fieldRegistry = new FieldRegistry()\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,gBAAN,MAAoB;AAAA,EAApB;AAEH;AAAA,SAAQ,WAA+C,CAAC;AAAA;AAAA;AAAA,EAGxD,SACI,MACA,UACF;AACE,SAAK,SAAS,IAAc,IAAI;AAAA,EACpC;AAAA,EAEA,IACI,MAC0C;AAC1C,WAAO,KAAK,SAAS,IAAc;AAAA,EACvC;AACJ;AAEO,IAAM,gBAAgB,IAAI,cAAc;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// src/fieldRegistry.ts
|
|
2
2
|
var FieldRegistry = class {
|
|
3
3
|
constructor() {
|
|
4
|
-
|
|
4
|
+
// ❗ INTERNAL = string, KHÔNG constrain
|
|
5
|
+
this.registry = {};
|
|
5
6
|
}
|
|
7
|
+
// ✅ Type safety ở API
|
|
6
8
|
register(type, renderer) {
|
|
7
|
-
this.
|
|
9
|
+
this.registry[type] = renderer;
|
|
8
10
|
}
|
|
9
11
|
get(type) {
|
|
10
|
-
return this.
|
|
12
|
+
return this.registry[type];
|
|
11
13
|
}
|
|
12
14
|
};
|
|
13
15
|
var fieldRegistry = new FieldRegistry();
|
|
14
16
|
export {
|
|
17
|
+
FieldRegistry,
|
|
15
18
|
fieldRegistry
|
|
16
19
|
};
|
|
17
20
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fieldRegistry.ts"],"sourcesContent":["import { JSX } from \"react\"\r\nimport { FieldRendererProps,
|
|
1
|
+
{"version":3,"sources":["../src/fieldRegistry.ts"],"sourcesContent":["import { JSX } from \"react\"\r\nimport type { FieldRendererProps, FieldTypeMap } from \"./types\"\r\n\r\nexport type FieldRenderer<T = any> = (props: FieldRendererProps<T>) => JSX.Element\r\n\r\nexport class FieldRegistry {\r\n // ❗ INTERNAL = string, KHÔNG constrain\r\n private registry: Record<string, FieldRenderer<any>> = {}\r\n\r\n // ✅ Type safety ở API\r\n register<K extends keyof FieldTypeMap>(\r\n type: K,\r\n renderer: FieldRenderer<FieldTypeMap[K]>\r\n ) {\r\n this.registry[type as string] = renderer\r\n }\r\n\r\n get<K extends keyof FieldTypeMap>(\r\n type: K\r\n ): FieldRenderer<FieldTypeMap[K]> | undefined {\r\n return this.registry[type as string]\r\n }\r\n}\r\n\r\nexport const fieldRegistry = new FieldRegistry()\r\n"],"mappings":";AAKO,IAAM,gBAAN,MAAoB;AAAA,EAApB;AAEH;AAAA,SAAQ,WAA+C,CAAC;AAAA;AAAA;AAAA,EAGxD,SACI,MACA,UACF;AACE,SAAK,SAAS,IAAc,IAAI;AAAA,EACpC;AAAA,EAEA,IACI,MAC0C;AAC1C,WAAO,KAAK,SAAS,IAAc;AAAA,EACvC;AACJ;AAEO,IAAM,gBAAgB,IAAI,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-field-kit/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Core types and field registry for dynamic-field-kit",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -21,4 +21,4 @@
|
|
|
21
21
|
"build": "tsup",
|
|
22
22
|
"clean": "rm -rf dist"
|
|
23
23
|
}
|
|
24
|
-
}
|
|
24
|
+
}
|