@axenstudio/axen-form 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.
- package/README.md +665 -0
- package/dist/adapters/yupAdapter.cjs +61 -0
- package/dist/adapters/yupAdapter.d.ts +21 -0
- package/dist/adapters/yupAdapter.d.ts.map +1 -0
- package/dist/adapters/yupAdapter.js +61 -0
- package/dist/adapters/zodAdapter.cjs +59 -0
- package/dist/adapters/zodAdapter.d.ts +22 -0
- package/dist/adapters/zodAdapter.d.ts.map +1 -0
- package/dist/adapters/zodAdapter.js +59 -0
- package/dist/core/dateUtils.cjs +69 -0
- package/dist/core/dateUtils.d.ts +28 -0
- package/dist/core/dateUtils.d.ts.map +1 -0
- package/dist/core/dateUtils.js +69 -0
- package/dist/core/formStore.cjs +184 -0
- package/dist/core/formStore.d.ts +39 -0
- package/dist/core/formStore.d.ts.map +1 -0
- package/dist/core/formStore.js +184 -0
- package/dist/core/numberUtils.cjs +59 -0
- package/dist/core/numberUtils.d.ts +26 -0
- package/dist/core/numberUtils.d.ts.map +1 -0
- package/dist/core/numberUtils.js +59 -0
- package/dist/core/pathUtils.cjs +44 -0
- package/dist/core/pathUtils.d.ts +15 -0
- package/dist/core/pathUtils.d.ts.map +1 -0
- package/dist/core/pathUtils.js +44 -0
- package/dist/core/payloadUtils.cjs +26 -0
- package/dist/core/payloadUtils.d.ts +22 -0
- package/dist/core/payloadUtils.d.ts.map +1 -0
- package/dist/core/payloadUtils.js +26 -0
- package/dist/core/validation.cjs +41 -0
- package/dist/core/validation.d.ts +7 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +41 -0
- package/dist/fields/AutocompleteField/AutocompleteField.cjs +242 -0
- package/dist/fields/AutocompleteField/AutocompleteField.d.ts +6 -0
- package/dist/fields/AutocompleteField/AutocompleteField.d.ts.map +1 -0
- package/dist/fields/AutocompleteField/AutocompleteField.js +242 -0
- package/dist/fields/AutocompleteField/AutocompleteField.module.css +168 -0
- package/dist/fields/AutocompleteField/AutocompleteField.module.css.cjs +44 -0
- package/dist/fields/AutocompleteField/AutocompleteField.module.css.js +44 -0
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.cjs +286 -0
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.d.ts +6 -0
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.d.ts.map +1 -0
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.js +286 -0
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.cjs +289 -0
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.d.ts +10 -0
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.d.ts.map +1 -0
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.js +289 -0
- package/dist/fields/CheckboxField/CheckboxField.cjs +60 -0
- package/dist/fields/CheckboxField/CheckboxField.d.ts +3 -0
- package/dist/fields/CheckboxField/CheckboxField.d.ts.map +1 -0
- package/dist/fields/CheckboxField/CheckboxField.js +60 -0
- package/dist/fields/CheckboxField/CheckboxField.module.css +29 -0
- package/dist/fields/CheckboxField/CheckboxField.module.css.cjs +11 -0
- package/dist/fields/CheckboxField/CheckboxField.module.css.js +11 -0
- package/dist/fields/ColorField/ColorField.cjs +69 -0
- package/dist/fields/ColorField/ColorField.d.ts +3 -0
- package/dist/fields/ColorField/ColorField.d.ts.map +1 -0
- package/dist/fields/ColorField/ColorField.js +69 -0
- package/dist/fields/ColorField/ColorField.module.css +33 -0
- package/dist/fields/ColorField/ColorField.module.css.cjs +14 -0
- package/dist/fields/ColorField/ColorField.module.css.js +14 -0
- package/dist/fields/CurrencyField/CurrencyField.cjs +120 -0
- package/dist/fields/CurrencyField/CurrencyField.d.ts +10 -0
- package/dist/fields/CurrencyField/CurrencyField.d.ts.map +1 -0
- package/dist/fields/CurrencyField/CurrencyField.js +120 -0
- package/dist/fields/DateField/DateField.cjs +70 -0
- package/dist/fields/DateField/DateField.d.ts +7 -0
- package/dist/fields/DateField/DateField.d.ts.map +1 -0
- package/dist/fields/DateField/DateField.js +70 -0
- package/dist/fields/DateTimeField/DateTimeField.cjs +71 -0
- package/dist/fields/DateTimeField/DateTimeField.d.ts +8 -0
- package/dist/fields/DateTimeField/DateTimeField.d.ts.map +1 -0
- package/dist/fields/DateTimeField/DateTimeField.js +71 -0
- package/dist/fields/EmailField/EmailField.cjs +57 -0
- package/dist/fields/EmailField/EmailField.d.ts +3 -0
- package/dist/fields/EmailField/EmailField.d.ts.map +1 -0
- package/dist/fields/EmailField/EmailField.js +57 -0
- package/dist/fields/NumberField/NumberField.cjs +110 -0
- package/dist/fields/NumberField/NumberField.d.ts +10 -0
- package/dist/fields/NumberField/NumberField.d.ts.map +1 -0
- package/dist/fields/NumberField/NumberField.js +110 -0
- package/dist/fields/PasswordField/PasswordField.cjs +77 -0
- package/dist/fields/PasswordField/PasswordField.d.ts +3 -0
- package/dist/fields/PasswordField/PasswordField.d.ts.map +1 -0
- package/dist/fields/PasswordField/PasswordField.js +77 -0
- package/dist/fields/PasswordField/PasswordField.module.css +41 -0
- package/dist/fields/PasswordField/PasswordField.module.css.cjs +14 -0
- package/dist/fields/PasswordField/PasswordField.module.css.js +14 -0
- package/dist/fields/PhoneField/PhoneField.cjs +58 -0
- package/dist/fields/PhoneField/PhoneField.d.ts +3 -0
- package/dist/fields/PhoneField/PhoneField.d.ts.map +1 -0
- package/dist/fields/PhoneField/PhoneField.js +58 -0
- package/dist/fields/RadioField/RadioField.cjs +66 -0
- package/dist/fields/RadioField/RadioField.d.ts +3 -0
- package/dist/fields/RadioField/RadioField.d.ts.map +1 -0
- package/dist/fields/RadioField/RadioField.js +66 -0
- package/dist/fields/RadioField/RadioField.module.css +34 -0
- package/dist/fields/RadioField/RadioField.module.css.cjs +14 -0
- package/dist/fields/RadioField/RadioField.module.css.js +14 -0
- package/dist/fields/SelectField/SelectField.cjs +58 -0
- package/dist/fields/SelectField/SelectField.d.ts +3 -0
- package/dist/fields/SelectField/SelectField.d.ts.map +1 -0
- package/dist/fields/SelectField/SelectField.js +58 -0
- package/dist/fields/SliderField/SliderField.cjs +79 -0
- package/dist/fields/SliderField/SliderField.d.ts +7 -0
- package/dist/fields/SliderField/SliderField.d.ts.map +1 -0
- package/dist/fields/SliderField/SliderField.js +79 -0
- package/dist/fields/SliderField/SliderField.module.css +76 -0
- package/dist/fields/SliderField/SliderField.module.css.cjs +14 -0
- package/dist/fields/SliderField/SliderField.module.css.js +14 -0
- package/dist/fields/SwitchField/SwitchField.cjs +65 -0
- package/dist/fields/SwitchField/SwitchField.d.ts +3 -0
- package/dist/fields/SwitchField/SwitchField.d.ts.map +1 -0
- package/dist/fields/SwitchField/SwitchField.js +65 -0
- package/dist/fields/SwitchField/SwitchField.module.css +79 -0
- package/dist/fields/SwitchField/SwitchField.module.css.cjs +17 -0
- package/dist/fields/SwitchField/SwitchField.module.css.js +17 -0
- package/dist/fields/TextField/TextField.cjs +56 -0
- package/dist/fields/TextField/TextField.d.ts +3 -0
- package/dist/fields/TextField/TextField.d.ts.map +1 -0
- package/dist/fields/TextField/TextField.js +56 -0
- package/dist/fields/TextareaField/TextareaField.cjs +57 -0
- package/dist/fields/TextareaField/TextareaField.d.ts +5 -0
- package/dist/fields/TextareaField/TextareaField.d.ts.map +1 -0
- package/dist/fields/TextareaField/TextareaField.js +57 -0
- package/dist/fields/TimeField/TimeField.cjs +67 -0
- package/dist/fields/TimeField/TimeField.d.ts +6 -0
- package/dist/fields/TimeField/TimeField.d.ts.map +1 -0
- package/dist/fields/TimeField/TimeField.js +67 -0
- package/dist/fields/defaultComponentMap.cjs +43 -0
- package/dist/fields/defaultComponentMap.d.ts +7 -0
- package/dist/fields/defaultComponentMap.d.ts.map +1 -0
- package/dist/fields/defaultComponentMap.js +43 -0
- package/dist/fields/fieldBase.module.css +201 -0
- package/dist/fields/fieldBase.module.css.cjs +50 -0
- package/dist/fields/fieldBase.module.css.js +50 -0
- package/dist/fields/fieldUtils.cjs +26 -0
- package/dist/fields/fieldUtils.d.ts +24 -0
- package/dist/fields/fieldUtils.d.ts.map +1 -0
- package/dist/fields/fieldUtils.js +26 -0
- package/dist/index.cjs +99 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +99 -0
- package/dist/layout/Box/Box.cjs +70 -0
- package/dist/layout/Box/Box.d.ts +6 -0
- package/dist/layout/Box/Box.d.ts.map +1 -0
- package/dist/layout/Box/Box.js +70 -0
- package/dist/layout/Box/Box.module.css +3 -0
- package/dist/layout/Box/Box.module.css.cjs +8 -0
- package/dist/layout/Box/Box.module.css.js +8 -0
- package/dist/layout/Divider/Divider.cjs +20 -0
- package/dist/layout/Divider/Divider.d.ts +6 -0
- package/dist/layout/Divider/Divider.d.ts.map +1 -0
- package/dist/layout/Divider/Divider.js +20 -0
- package/dist/layout/Divider/Divider.module.css +25 -0
- package/dist/layout/Divider/Divider.module.css.cjs +14 -0
- package/dist/layout/Divider/Divider.module.css.js +14 -0
- package/dist/layout/Grid/Grid.cjs +106 -0
- package/dist/layout/Grid/Grid.d.ts +7 -0
- package/dist/layout/Grid/Grid.d.ts.map +1 -0
- package/dist/layout/Grid/Grid.js +106 -0
- package/dist/layout/Grid/Grid.module.css +43 -0
- package/dist/layout/Grid/Grid.module.css.cjs +23 -0
- package/dist/layout/Grid/Grid.module.css.js +23 -0
- package/dist/layout/Spacer/Spacer.cjs +30 -0
- package/dist/layout/Spacer/Spacer.d.ts +21 -0
- package/dist/layout/Spacer/Spacer.d.ts.map +1 -0
- package/dist/layout/Spacer/Spacer.js +30 -0
- package/dist/layout/Stack/Stack.cjs +52 -0
- package/dist/layout/Stack/Stack.d.ts +7 -0
- package/dist/layout/Stack/Stack.d.ts.map +1 -0
- package/dist/layout/Stack/Stack.js +52 -0
- package/dist/layout/Stack/Stack.module.css +3 -0
- package/dist/layout/Stack/Stack.module.css.cjs +8 -0
- package/dist/layout/Stack/Stack.module.css.js +8 -0
- package/dist/layout/index.d.ts +6 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/react/AxenArrayField.cjs +80 -0
- package/dist/react/AxenArrayField.d.ts +15 -0
- package/dist/react/AxenArrayField.d.ts.map +1 -0
- package/dist/react/AxenArrayField.js +80 -0
- package/dist/react/AxenField.cjs +62 -0
- package/dist/react/AxenField.d.ts +13 -0
- package/dist/react/AxenField.d.ts.map +1 -0
- package/dist/react/AxenField.js +62 -0
- package/dist/react/AxenForm.cjs +134 -0
- package/dist/react/AxenForm.d.ts +7 -0
- package/dist/react/AxenForm.d.ts.map +1 -0
- package/dist/react/AxenForm.js +134 -0
- package/dist/react/FormContext.cjs +13 -0
- package/dist/react/FormContext.d.ts +11 -0
- package/dist/react/FormContext.d.ts.map +1 -0
- package/dist/react/FormContext.js +13 -0
- package/dist/react/useAxenForm.cjs +88 -0
- package/dist/react/useAxenForm.d.ts +8 -0
- package/dist/react/useAxenForm.d.ts.map +1 -0
- package/dist/react/useAxenForm.js +88 -0
- package/dist/react/useField.cjs +41 -0
- package/dist/react/useField.d.ts +13 -0
- package/dist/react/useField.d.ts.map +1 -0
- package/dist/react/useField.js +41 -0
- package/dist/react/useFieldArray.cjs +74 -0
- package/dist/react/useFieldArray.d.ts +5 -0
- package/dist/react/useFieldArray.d.ts.map +1 -0
- package/dist/react/useFieldArray.js +74 -0
- package/dist/types.d.ts +226 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
# @axenstudio/axen-form
|
|
2
|
+
|
|
3
|
+
Lightweight, config-driven, enterprise-grade React form library.
|
|
4
|
+
Zero UI dependency. CSS Modules. A11y-aware. TypeScript strict.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@axenstudio/axen-form)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://react.dev)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why axen-form?
|
|
13
|
+
|
|
14
|
+
| Feature | axen-form | Formik + MUI | React Hook Form |
|
|
15
|
+
| ------------- | ------------------------------------- | --------------------- | ---------------- |
|
|
16
|
+
| UI Dependency | **None** — native HTML | MUI (~350KB) | None |
|
|
17
|
+
| Form Engine | **Built-in** reactive store | Formik rerender-heavy | Ref-based |
|
|
18
|
+
| Validation | **Plugin-based** (Yup / Zod / custom) | Yup only | Resolver pattern |
|
|
19
|
+
| Styling | **CSS Modules** + design tokens | Emotion runtime | BYO |
|
|
20
|
+
| Config-Driven | **Yes** — one config = one form | No | No |
|
|
21
|
+
| Bundle Size | **< 15KB** gzipped | ~350KB+ with MUI | ~10KB |
|
|
22
|
+
| React Version | **≥ 19** | ≥ 16 | ≥ 16 |
|
|
23
|
+
| TypeScript | **Strict** end-to-end | Partial | Good |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @axenstudio/axen-form
|
|
31
|
+
# or
|
|
32
|
+
pnpm add @axenstudio/axen-form
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Peer dependencies:** `react ≥ 19`, `react-dom ≥ 19`
|
|
36
|
+
|
|
37
|
+
**Optional validation libraries:**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Pick one (or both)
|
|
41
|
+
pnpm add yup # Yup adapter
|
|
42
|
+
pnpm add zod # Zod adapter
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
|
|
51
|
+
import '@axenstudio/axen-form/tokens';
|
|
52
|
+
import '@axenstudio/axen-form/reset';
|
|
53
|
+
|
|
54
|
+
const config = {
|
|
55
|
+
initialValues: { name: '', email: '', role: 'user' },
|
|
56
|
+
fields: [
|
|
57
|
+
{ name: 'name', type: 'text', label: 'Full Name', required: true, colSpan: 6 },
|
|
58
|
+
{ name: 'email', type: 'email', label: 'Email', required: true, colSpan: 6 },
|
|
59
|
+
{
|
|
60
|
+
name: 'role',
|
|
61
|
+
type: 'select',
|
|
62
|
+
label: 'Role',
|
|
63
|
+
options: [
|
|
64
|
+
{ value: 'user', label: 'User' },
|
|
65
|
+
{ value: 'admin', label: 'Admin' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function App() {
|
|
72
|
+
return (
|
|
73
|
+
<AxenForm
|
|
74
|
+
config={config}
|
|
75
|
+
components={defaultComponentMap}
|
|
76
|
+
onSubmit={(values) => console.log(values)}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That's it. One config, one component — a full form with validation, layout, and accessibility.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 20 Built-in Field Types
|
|
87
|
+
|
|
88
|
+
### Text Inputs
|
|
89
|
+
|
|
90
|
+
| Type | Component | Description |
|
|
91
|
+
| ---------- | --------------- | ---------------------------------- |
|
|
92
|
+
| `text` | `TextField` | Standard text input |
|
|
93
|
+
| `email` | `EmailField` | Email input with `type="email"` |
|
|
94
|
+
| `password` | `PasswordField` | Password with show/hide toggle |
|
|
95
|
+
| `phone` | `PhoneField` | Phone input with `type="tel"` |
|
|
96
|
+
| `textarea` | `TextareaField` | Multi-line text, configurable rows |
|
|
97
|
+
|
|
98
|
+
### Numeric Inputs
|
|
99
|
+
|
|
100
|
+
| Type | Component | Description |
|
|
101
|
+
| ---------- | --------------- | ----------------------------------------------------- |
|
|
102
|
+
| `number` | `NumberField` | Locale-aware thousand separator + cursor preservation |
|
|
103
|
+
| `currency` | `CurrencyField` | Intl currency formatting with prefix/suffix |
|
|
104
|
+
| `slider` | `SliderField` | Range slider with value display |
|
|
105
|
+
|
|
106
|
+
### Date & Time
|
|
107
|
+
|
|
108
|
+
| Type | Component | Description |
|
|
109
|
+
| ---------- | --------------- | ------------------------------------------------------ |
|
|
110
|
+
| `date` | `DateField` | Date picker — displays locale, stores ISO `YYYY-MM-DD` |
|
|
111
|
+
| `time` | `TimeField` | Time picker — 12h/24h, stores ISO `HH:mm` |
|
|
112
|
+
| `datetime` | `DateTimeField` | Combined date+time, stores ISO `YYYY-MM-DDTHH:mm` |
|
|
113
|
+
|
|
114
|
+
### Selection
|
|
115
|
+
|
|
116
|
+
| Type | Component | Description |
|
|
117
|
+
| ---------- | --------------- | -------------------------- |
|
|
118
|
+
| `select` | `SelectField` | Native `<select>` dropdown |
|
|
119
|
+
| `checkbox` | `CheckboxField` | Styled checkbox with label |
|
|
120
|
+
| `radio` | `RadioField` | Radio group from `options` |
|
|
121
|
+
| `switch` | `SwitchField` | Toggle switch (on/off) |
|
|
122
|
+
|
|
123
|
+
### Autocomplete
|
|
124
|
+
|
|
125
|
+
| Type | Component | Description |
|
|
126
|
+
| ---------------------- | -------------------------- | ---------------------------------- |
|
|
127
|
+
| `autocomplete` | `AutocompleteField` | Server-side search with debounce |
|
|
128
|
+
| `autocomplete-multi` | `AutocompleteMultiField` | Multi-select with chip/tag display |
|
|
129
|
+
| `autocomplete-predict` | `AutocompletePredictField` | Ghost text prediction + Tab accept |
|
|
130
|
+
|
|
131
|
+
### Miscellaneous
|
|
132
|
+
|
|
133
|
+
| Type | Component | Description |
|
|
134
|
+
| ------- | ------------ | ------------------------ |
|
|
135
|
+
| `color` | `ColorField` | Color swatch + hex input |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Core Concepts
|
|
140
|
+
|
|
141
|
+
### Config-Driven Forms
|
|
142
|
+
|
|
143
|
+
Define your entire form with a configuration object:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import type { FormConfig } from '@axenstudio/axen-form';
|
|
147
|
+
|
|
148
|
+
const config: FormConfig = {
|
|
149
|
+
initialValues: {
|
|
150
|
+
name: '',
|
|
151
|
+
email: '',
|
|
152
|
+
age: 0,
|
|
153
|
+
birthday: '',
|
|
154
|
+
bio: '',
|
|
155
|
+
},
|
|
156
|
+
fields: [
|
|
157
|
+
{ name: 'name', type: 'text', label: 'Name', required: true },
|
|
158
|
+
{
|
|
159
|
+
name: 'email',
|
|
160
|
+
type: 'email',
|
|
161
|
+
label: 'Email',
|
|
162
|
+
required: true,
|
|
163
|
+
requiredMessage: 'Email wajib diisi',
|
|
164
|
+
},
|
|
165
|
+
{ name: 'age', type: 'number', label: 'Age', min: 0, max: 150, locale: 'en-US' },
|
|
166
|
+
{ name: 'birthday', type: 'date', label: 'Birthday' },
|
|
167
|
+
{ name: 'bio', type: 'textarea', label: 'Bio', rows: 4 },
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Responsive Grid Layout
|
|
173
|
+
|
|
174
|
+
Fields are placed in a 12-column CSS Grid. Control column span per breakpoint:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
{
|
|
178
|
+
name: 'firstName',
|
|
179
|
+
type: 'text',
|
|
180
|
+
label: 'First Name',
|
|
181
|
+
colSpan: { xs: 12, sm: 6, md: 4 }, // Full → half → third
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Or use a simple number for all breakpoints:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
{ name: 'email', type: 'email', label: 'Email', colSpan: 6 }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Validation Adapters
|
|
192
|
+
|
|
193
|
+
Bring your own validation library via adapters:
|
|
194
|
+
|
|
195
|
+
**Yup:**
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
|
|
199
|
+
import { yupAdapter } from '@axenstudio/axen-form/adapters/yup';
|
|
200
|
+
|
|
201
|
+
<AxenForm
|
|
202
|
+
config={config}
|
|
203
|
+
components={defaultComponentMap}
|
|
204
|
+
validationAdapter={yupAdapter}
|
|
205
|
+
onSubmit={handleSubmit}
|
|
206
|
+
/>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Zod:**
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { zodAdapter } from '@axenstudio/axen-form/adapters/zod';
|
|
213
|
+
|
|
214
|
+
<AxenForm
|
|
215
|
+
config={config}
|
|
216
|
+
components={defaultComponentMap}
|
|
217
|
+
validationAdapter={zodAdapter}
|
|
218
|
+
onSubmit={handleSubmit}
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Built-in (zero-dep):**
|
|
223
|
+
|
|
224
|
+
Without any adapter, axen-form uses `simpleValidator` — validates `required` fields automatically.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Advanced Features
|
|
229
|
+
|
|
230
|
+
### Conditional Fields
|
|
231
|
+
|
|
232
|
+
Hide fields dynamically based on form values:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
{
|
|
236
|
+
name: 'companyName',
|
|
237
|
+
type: 'text',
|
|
238
|
+
label: 'Company Name',
|
|
239
|
+
hidden: (values) => values.role !== 'business',
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Hidden fields are excluded from the rendered form and validation.
|
|
244
|
+
|
|
245
|
+
### Array / Repeating Fields
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import type { ArrayFieldConfig } from '@axenstudio/axen-form';
|
|
249
|
+
|
|
250
|
+
const contactFields: ArrayFieldConfig = {
|
|
251
|
+
name: 'contacts',
|
|
252
|
+
type: 'text', // base type (unused in array context)
|
|
253
|
+
isArray: true,
|
|
254
|
+
label: 'Contacts',
|
|
255
|
+
addLabel: '+ Add Contact',
|
|
256
|
+
removeLabel: 'Remove',
|
|
257
|
+
minItems: 1,
|
|
258
|
+
maxItems: 5,
|
|
259
|
+
fields: [
|
|
260
|
+
{ name: 'name', type: 'text', label: 'Name', colSpan: 6 },
|
|
261
|
+
{ name: 'email', type: 'email', label: 'Email', colSpan: 6 },
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Imperative Control via Ref
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
import { useRef } from 'react';
|
|
270
|
+
import type { AxenFormRef } from '@axenstudio/axen-form';
|
|
271
|
+
|
|
272
|
+
function MyForm() {
|
|
273
|
+
const formRef = useRef<AxenFormRef>(null);
|
|
274
|
+
|
|
275
|
+
const handleExternalSubmit = () => formRef.current?.submit();
|
|
276
|
+
const handleReset = () => formRef.current?.resetForm();
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<>
|
|
280
|
+
<AxenForm
|
|
281
|
+
ref={formRef}
|
|
282
|
+
config={config}
|
|
283
|
+
components={defaultComponentMap}
|
|
284
|
+
onSubmit={handleSubmit}
|
|
285
|
+
/>
|
|
286
|
+
<button onClick={handleExternalSubmit}>Submit from Outside</button>
|
|
287
|
+
<button onClick={handleReset}>Reset</button>
|
|
288
|
+
</>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Available ref methods:**
|
|
294
|
+
|
|
295
|
+
| Method | Return | Description |
|
|
296
|
+
| -------------------- | ------------------------- | --------------------------------------- |
|
|
297
|
+
| `submit()` | `void` | Trigger form submission |
|
|
298
|
+
| `resetForm(values?)` | `void` | Reset form (optionally with new values) |
|
|
299
|
+
| `getValues()` | `Record<string, unknown>` | Get current form values |
|
|
300
|
+
| `getErrors()` | `Record<string, string>` | Get current validation errors |
|
|
301
|
+
| `isValid()` | `boolean` | Check if form has no errors |
|
|
302
|
+
| `isDirty()` | `boolean` | Check if values differ from initial |
|
|
303
|
+
|
|
304
|
+
### Payload Whitelist
|
|
305
|
+
|
|
306
|
+
Control which fields are included in the submit payload:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
// Static whitelist
|
|
310
|
+
<AxenForm
|
|
311
|
+
config={config}
|
|
312
|
+
components={defaultComponentMap}
|
|
313
|
+
payloadFields={['name', 'email']}
|
|
314
|
+
onSubmit={(values) => {
|
|
315
|
+
// values only contains { name, email }
|
|
316
|
+
}}
|
|
317
|
+
/>
|
|
318
|
+
|
|
319
|
+
// Dynamic whitelist based on discriminator field
|
|
320
|
+
<AxenForm
|
|
321
|
+
config={config}
|
|
322
|
+
components={defaultComponentMap}
|
|
323
|
+
payloadFields={{
|
|
324
|
+
individual: ['firstName', 'lastName', 'ssn'],
|
|
325
|
+
company: ['companyName', 'taxId'],
|
|
326
|
+
}}
|
|
327
|
+
payloadDiscriminator="accountType"
|
|
328
|
+
onSubmit={handleSubmit}
|
|
329
|
+
/>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Headless Usage with `useAxenForm`
|
|
333
|
+
|
|
334
|
+
For full control over rendering, use the headless hook:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { useAxenForm, useField } from '@axenstudio/axen-form';
|
|
338
|
+
|
|
339
|
+
function CustomForm() {
|
|
340
|
+
const { store, formApi } = useAxenForm({
|
|
341
|
+
initialValues: { name: '' },
|
|
342
|
+
fields: [{ name: 'name', type: 'text', label: 'Name', required: true }],
|
|
343
|
+
onSubmit: async (values) => {
|
|
344
|
+
/* ... */
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<form onSubmit={formApi.handleSubmit}>
|
|
350
|
+
<input
|
|
351
|
+
name="name"
|
|
352
|
+
value={formApi.values.name as string}
|
|
353
|
+
onChange={formApi.handleChange}
|
|
354
|
+
onBlur={formApi.handleBlur}
|
|
355
|
+
/>
|
|
356
|
+
{formApi.errors.name && <span>{formApi.errors.name}</span>}
|
|
357
|
+
<button type="submit">Submit</button>
|
|
358
|
+
</form>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Custom Field Components
|
|
364
|
+
|
|
365
|
+
Override any built-in field by providing a custom component:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
import type { FieldComponentProps } from '@axenstudio/axen-form';
|
|
369
|
+
|
|
370
|
+
function MyCustomSelect({ name, value, onChange, options, label, error, helperText }: FieldComponentProps) {
|
|
371
|
+
return (
|
|
372
|
+
<div>
|
|
373
|
+
<label>{label}</label>
|
|
374
|
+
{/* Your custom select UI */}
|
|
375
|
+
{error && <span className="error">{helperText}</span>}
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Per-field override
|
|
381
|
+
const config = {
|
|
382
|
+
initialValues: { country: '' },
|
|
383
|
+
fields: [
|
|
384
|
+
{ name: 'country', type: 'select', label: 'Country', component: MyCustomSelect, options: [...] },
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Or type-level override via components prop
|
|
389
|
+
<AxenForm
|
|
390
|
+
config={config}
|
|
391
|
+
components={{ ...defaultComponentMap, select: MyCustomSelect }}
|
|
392
|
+
onSubmit={handleSubmit}
|
|
393
|
+
/>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Layout Components
|
|
399
|
+
|
|
400
|
+
Built-in layout components with MUI-like API, powered by CSS Grid:
|
|
401
|
+
|
|
402
|
+
### Grid
|
|
403
|
+
|
|
404
|
+
12-column responsive grid container:
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { Grid } from '@axenstudio/axen-form';
|
|
408
|
+
|
|
409
|
+
<Grid container spacing={2}>
|
|
410
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
411
|
+
<TextField {...props} />
|
|
412
|
+
</Grid>
|
|
413
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
414
|
+
<TextField {...props} />
|
|
415
|
+
</Grid>
|
|
416
|
+
</Grid>;
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Box
|
|
420
|
+
|
|
421
|
+
Generic flex/block container:
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
import { Box } from '@axenstudio/axen-form';
|
|
425
|
+
|
|
426
|
+
<Box display="flex" gap="16px" p="8px">
|
|
427
|
+
<span>Left</span>
|
|
428
|
+
<span>Right</span>
|
|
429
|
+
</Box>;
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Stack
|
|
433
|
+
|
|
434
|
+
Vertical/horizontal stack with gap:
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
import { Stack } from '@axenstudio/axen-form';
|
|
438
|
+
|
|
439
|
+
<Stack direction="column" spacing={2}>
|
|
440
|
+
<div>Item 1</div>
|
|
441
|
+
<div>Item 2</div>
|
|
442
|
+
</Stack>;
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Divider
|
|
446
|
+
|
|
447
|
+
Horizontal/vertical divider:
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
import { Divider } from '@axenstudio/axen-form';
|
|
451
|
+
|
|
452
|
+
<Stack direction="column" spacing={2}>
|
|
453
|
+
<div>Above</div>
|
|
454
|
+
<Divider />
|
|
455
|
+
<div>Below</div>
|
|
456
|
+
</Stack>;
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Spacer
|
|
460
|
+
|
|
461
|
+
Invisible spacing element — creates horizontal, vertical, or flex-basis gaps. Uses the same 8px spacing unit as Box/Grid/Stack.
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { Spacer } from '@axenstudio/axen-form';
|
|
465
|
+
|
|
466
|
+
// Horizontal gap (16px)
|
|
467
|
+
<Stack direction="row">
|
|
468
|
+
<Button>Save</Button>
|
|
469
|
+
<Spacer x={2} />
|
|
470
|
+
<Button>Cancel</Button>
|
|
471
|
+
</Stack>
|
|
472
|
+
|
|
473
|
+
// Vertical gap (24px)
|
|
474
|
+
<Stack direction="column">
|
|
475
|
+
<Card>Section A</Card>
|
|
476
|
+
<Spacer y={3} />
|
|
477
|
+
<Card>Section B</Card>
|
|
478
|
+
</Stack>
|
|
479
|
+
|
|
480
|
+
// Push items apart (flex grow)
|
|
481
|
+
<Stack direction="row">
|
|
482
|
+
<Logo />
|
|
483
|
+
<Spacer grow />
|
|
484
|
+
<NavLinks />
|
|
485
|
+
</Stack>
|
|
486
|
+
|
|
487
|
+
// Fixed flex basis (32px)
|
|
488
|
+
<Spacer basis={4} />
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
| Prop | Type | Default | Description |
|
|
492
|
+
| --- | --- | --- | --- |
|
|
493
|
+
| `x` | `number` | `0` | Horizontal spacing multiplier (width = x × 8px) |
|
|
494
|
+
| `y` | `number` | `0` | Vertical spacing multiplier (height = y × 8px) |
|
|
495
|
+
| `basis` | `number` | `0` | Flex basis multiplier (basis × 8px) |
|
|
496
|
+
| `grow` | `boolean` | `false` | Grow to fill remaining space |
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Theming with CSS Custom Properties
|
|
501
|
+
|
|
502
|
+
axen-form uses CSS Custom Properties (design tokens) for theming. Import the default tokens and override as needed:
|
|
503
|
+
|
|
504
|
+
```css
|
|
505
|
+
/* Import defaults */
|
|
506
|
+
@import '@axenstudio/axen-form/tokens';
|
|
507
|
+
@import '@axenstudio/axen-form/reset';
|
|
508
|
+
|
|
509
|
+
/* Override tokens */
|
|
510
|
+
:root {
|
|
511
|
+
--axen-primary-color: #1a73e8;
|
|
512
|
+
--axen-error-color: #d32f2f;
|
|
513
|
+
--axen-focus-color: #1a73e8;
|
|
514
|
+
--axen-border-radius: 8px;
|
|
515
|
+
--axen-font-family: 'Inter', sans-serif;
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Key token categories:**
|
|
520
|
+
|
|
521
|
+
| Category | Example Tokens |
|
|
522
|
+
| ---------- | ------------------------------------------------------------------ |
|
|
523
|
+
| Colors | `--axen-primary-color`, `--axen-error-color`, `--axen-label-color` |
|
|
524
|
+
| Typography | `--axen-font-family`, `--axen-label-font-size` |
|
|
525
|
+
| Spacing | `--axen-field-gap`, `--axen-input-padding` |
|
|
526
|
+
| Borders | `--axen-border-color`, `--axen-border-radius` |
|
|
527
|
+
| Focus | `--axen-focus-color`, `--axen-focus-ring` |
|
|
528
|
+
| States | `--axen-input-disabled-bg`, `--axen-placeholder-color` |
|
|
529
|
+
|
|
530
|
+
**Built-in A11y support:**
|
|
531
|
+
|
|
532
|
+
- Dark mode via `@media (prefers-color-scheme: dark)`
|
|
533
|
+
- High contrast via `@media (prefers-contrast: more)`
|
|
534
|
+
- Reduced motion via `@media (prefers-reduced-motion: reduce)`
|
|
535
|
+
- Focus-visible rings for keyboard navigation
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## API Reference
|
|
540
|
+
|
|
541
|
+
### `<AxenForm>`
|
|
542
|
+
|
|
543
|
+
Main config-driven form component.
|
|
544
|
+
|
|
545
|
+
| Prop | Type | Default | Description |
|
|
546
|
+
| ---------------------- | -------------------------------------- | ----------------- | ------------------------------------------- |
|
|
547
|
+
| `config` | `FormConfig` | **required** | Form configuration (fields + initialValues) |
|
|
548
|
+
| `onSubmit` | `(values, meta) => void` | **required** | Submit handler |
|
|
549
|
+
| `components` | `FieldTypeComponentMap` | `{}` | Component map (use `defaultComponentMap`) |
|
|
550
|
+
| `validationAdapter` | `ValidationAdapter` | `simpleValidator` | Validation library adapter |
|
|
551
|
+
| `ref` | `Ref<AxenFormRef>` | — | Imperative handle |
|
|
552
|
+
| `columns` | `number` | `12` | Grid columns |
|
|
553
|
+
| `gap` | `string \| number` | — | Grid gap |
|
|
554
|
+
| `disabled` | `boolean` | `false` | Disable all fields |
|
|
555
|
+
| `className` | `string` | — | Additional CSS class |
|
|
556
|
+
| `payloadFields` | `string[] \| Record<string, string[]>` | — | Payload whitelist |
|
|
557
|
+
| `payloadDiscriminator` | `string` | — | Discriminator field for dynamic payload |
|
|
558
|
+
| `validateOnChange` | `boolean` | `true` | Validate on field change |
|
|
559
|
+
| `validateOnBlur` | `boolean` | `true` | Validate on field blur |
|
|
560
|
+
|
|
561
|
+
### `FieldConfig`
|
|
562
|
+
|
|
563
|
+
Configuration for a single field.
|
|
564
|
+
|
|
565
|
+
| Property | Type | Description |
|
|
566
|
+
| -------------------- | --------------------------------------- | -------------------------------------------------- |
|
|
567
|
+
| `name` | `string` | Field name (supports nested paths: `address.city`) |
|
|
568
|
+
| `type` | `BuiltInFieldType \| string` | Field type identifier |
|
|
569
|
+
| `label` | `string` | Field label |
|
|
570
|
+
| `placeholder` | `string` | Input placeholder |
|
|
571
|
+
| `required` | `boolean` | Mark as required |
|
|
572
|
+
| `requiredMessage` | `string` | Custom required error message |
|
|
573
|
+
| `disabled` | `boolean` | Disable this field |
|
|
574
|
+
| `readOnly` | `boolean` | Read-only mode |
|
|
575
|
+
| `hidden` | `boolean \| (values) => boolean` | Conditional hiding |
|
|
576
|
+
| `component` | `ComponentType` | Override component for this field |
|
|
577
|
+
| `options` | `FieldOption[]` | Options for select/radio/autocomplete |
|
|
578
|
+
| `colSpan` | `number \| { xs?, sm?, md?, lg?, xl? }` | Grid column span |
|
|
579
|
+
| `helperText` | `string` | Helper text below field |
|
|
580
|
+
| `min`, `max`, `step` | `number` | Numeric constraints |
|
|
581
|
+
| `prefix`, `suffix` | `string` | Display prefix/suffix (currency) |
|
|
582
|
+
| `locale` | `string` | Locale for number/date formatting |
|
|
583
|
+
| `rows` | `number` | Textarea rows |
|
|
584
|
+
| `fetchOptions` | `(query, signal?) => Promise<Option[]>` | Async option fetcher (autocomplete) |
|
|
585
|
+
| `fieldProps` | `Record<string, unknown>` | Extra props forwarded to component |
|
|
586
|
+
|
|
587
|
+
### Hooks
|
|
588
|
+
|
|
589
|
+
| Hook | Signature | Description |
|
|
590
|
+
| ---------------- | ------------------------------------------------------------------------ | ------------------------------- |
|
|
591
|
+
| `useAxenForm` | `(config) => { store, formApi }` | Create a headless form instance |
|
|
592
|
+
| `useField` | `(name) => { value, error, touched, onChange, onBlur }` | Subscribe to a single field |
|
|
593
|
+
| `useFieldArray` | `(name) => { items, push, remove, swap, insert, replace, move, length }` | Array field operations |
|
|
594
|
+
| `useFormContext` | `() => FormApi` | Access form API from context |
|
|
595
|
+
|
|
596
|
+
### Utilities
|
|
597
|
+
|
|
598
|
+
| Utility | Description |
|
|
599
|
+
| --------------------------------------- | --------------------------------------- |
|
|
600
|
+
| `mapPayload(values, config, fields)` | Apply payload whitelist to form values |
|
|
601
|
+
| `pickFields(values, fieldNames)` | Pick specific fields from values object |
|
|
602
|
+
| `getStrippedFields(values, fieldNames)` | Get fields that would be stripped |
|
|
603
|
+
| `get(obj, path, default?)` | Get nested value by dot path |
|
|
604
|
+
| `set(obj, path, value)` | Immutable set nested value |
|
|
605
|
+
| `formatNumber(value, locale)` | Format number with locale |
|
|
606
|
+
| `formatDateLocale(isoDate, locale)` | Format ISO date for display |
|
|
607
|
+
| `cx(...classes)` | Conditional class name joiner |
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## Architecture
|
|
612
|
+
|
|
613
|
+
```
|
|
614
|
+
src/
|
|
615
|
+
├── core/ # Framework-agnostic form engine
|
|
616
|
+
│ ├── formStore.ts # Reactive state (useSyncExternalStore compatible)
|
|
617
|
+
│ ├── validation.ts # Adapter interface + simpleValidator
|
|
618
|
+
│ ├── pathUtils.ts # Nested path get/set/toArray
|
|
619
|
+
│ ├── numberUtils.ts # Locale-aware number formatting
|
|
620
|
+
│ ├── dateUtils.ts # ISO ↔ locale date conversion
|
|
621
|
+
│ └── payloadUtils.ts # Payload whitelist filter
|
|
622
|
+
│
|
|
623
|
+
├── react/ # React 19+ bindings
|
|
624
|
+
│ ├── AxenForm.tsx # Main config-driven renderer
|
|
625
|
+
│ ├── AxenField.tsx # Field wrapper component
|
|
626
|
+
│ ├── AxenArrayField.tsx # Array field renderer
|
|
627
|
+
│ ├── useAxenForm.ts # Headless form hook
|
|
628
|
+
│ ├── useField.ts # Single field subscription
|
|
629
|
+
│ ├── useFieldArray.ts # Array field operations
|
|
630
|
+
│ └── FormContext.ts # React context provider
|
|
631
|
+
│
|
|
632
|
+
├── fields/ # 20 built-in field components
|
|
633
|
+
│ ├── defaultComponentMap.ts
|
|
634
|
+
│ ├── fieldUtils.ts # Shared utilities (cx, ariaProps)
|
|
635
|
+
│ ├── fieldBase.module.css # Shared field styles
|
|
636
|
+
│ ├── TextField/
|
|
637
|
+
│ ├── EmailField/
|
|
638
|
+
│ ├── ... # 18 more field directories
|
|
639
|
+
│ └── ColorField/
|
|
640
|
+
│
|
|
641
|
+
├── layout/ # Grid, Box, Stack, Divider
|
|
642
|
+
├── tokens/ # CSS Custom Properties + reset
|
|
643
|
+
├── adapters/ # yupAdapter, zodAdapter
|
|
644
|
+
├── types.ts # All TypeScript definitions
|
|
645
|
+
└── index.ts # Public API barrel
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Browser Support
|
|
651
|
+
|
|
652
|
+
| Browser | Version |
|
|
653
|
+
| ------- | --------------- |
|
|
654
|
+
| Chrome | Last 2 versions |
|
|
655
|
+
| Firefox | Last 2 versions |
|
|
656
|
+
| Safari | 15.4+ |
|
|
657
|
+
| Edge | Last 2 versions |
|
|
658
|
+
|
|
659
|
+
Requires React 19+ and ES2022+ environment.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## License
|
|
664
|
+
|
|
665
|
+
MIT © [Axen Studio](https://github.com/axenstudio)
|