@vielzeug/formit 1.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 +679 -0
- package/dist/formit.cjs +2 -0
- package/dist/formit.cjs.map +1 -0
- package/dist/formit.js +311 -0
- package/dist/formit.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# @vielzeug/formit
|
|
2
|
+
|
|
3
|
+
Type-safe form state management with effortless validation. Build robust forms with minimal code and maximum flexibility.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Type-Safe** - Full TypeScript support with path-based type inference
|
|
8
|
+
- ✅ **Flexible Validation** - Sync/async validators with granular control
|
|
9
|
+
- ✅ **Smart State Tracking** - Automatic dirty and touched state management
|
|
10
|
+
- ✅ **Field Binding** - One-line input integration with customizable extractors
|
|
11
|
+
- ✅ **Nested Paths** - Deep object and array support with dot/bracket notation
|
|
12
|
+
- ✅ **Performance** - Targeted validation (only touched fields, specific subsets)
|
|
13
|
+
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, or vanilla JS
|
|
14
|
+
- ✅ **Lightweight** - Zero dependencies, minimal footprint
|
|
15
|
+
- ✅ **Developer Experience** - Intuitive API with comprehensive helpers
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# pnpm
|
|
21
|
+
pnpm add @vielzeug/formit
|
|
22
|
+
|
|
23
|
+
# npm
|
|
24
|
+
npm install @vielzeug/formit
|
|
25
|
+
|
|
26
|
+
# yarn
|
|
27
|
+
yarn add @vielzeug/formit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { createForm } from '@vielzeug/formit';
|
|
34
|
+
|
|
35
|
+
// Define your form structure
|
|
36
|
+
interface LoginForm {
|
|
37
|
+
email: string;
|
|
38
|
+
password: string;
|
|
39
|
+
rememberMe: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create form with validation
|
|
43
|
+
const form = createForm<LoginForm>({
|
|
44
|
+
initialValues: {
|
|
45
|
+
email: '',
|
|
46
|
+
password: '',
|
|
47
|
+
rememberMe: false,
|
|
48
|
+
},
|
|
49
|
+
fields: {
|
|
50
|
+
email: {
|
|
51
|
+
validators: [
|
|
52
|
+
(value) => (!value ? 'Email is required' : undefined),
|
|
53
|
+
(value) => (!value.includes('@') ? 'Invalid email' : undefined),
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
password: {
|
|
57
|
+
validators: (value) =>
|
|
58
|
+
value.length < 8 ? 'Password must be at least 8 characters' : undefined,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Use in your component
|
|
64
|
+
function LoginForm() {
|
|
65
|
+
return (
|
|
66
|
+
<form onSubmit={async (e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
await form.submit(async (values) => {
|
|
69
|
+
await api.login(values);
|
|
70
|
+
});
|
|
71
|
+
}}>
|
|
72
|
+
<input {...form.bind('email')} type="email" />
|
|
73
|
+
{form.isTouched('email') && form.getError('email') && (
|
|
74
|
+
<span>{form.getError('email')}</span>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<input {...form.bind('password')} type="password" />
|
|
78
|
+
{form.isTouched('password') && form.getError('password') && (
|
|
79
|
+
<span>{form.getError('password')}</span>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<label>
|
|
83
|
+
<input {...form.bind('rememberMe')} type="checkbox" />
|
|
84
|
+
Remember me
|
|
85
|
+
</label>
|
|
86
|
+
|
|
87
|
+
<button type="submit" disabled={form.getStateSnapshot().isSubmitting}>
|
|
88
|
+
Login
|
|
89
|
+
</button>
|
|
90
|
+
</form>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Core Concepts
|
|
96
|
+
|
|
97
|
+
### Form Creation
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const form = createForm({
|
|
101
|
+
// Initial values
|
|
102
|
+
initialValues: { name: 'Alice', age: 30 },
|
|
103
|
+
|
|
104
|
+
// Field-level validation
|
|
105
|
+
fields: {
|
|
106
|
+
name: {
|
|
107
|
+
validators: (value) => (!value ? 'Required' : undefined),
|
|
108
|
+
initialValue: 'Default Name', // Used if not in initialValues
|
|
109
|
+
},
|
|
110
|
+
age: {
|
|
111
|
+
validators: [
|
|
112
|
+
(value) => (value < 18 ? 'Must be 18+' : undefined),
|
|
113
|
+
(value) => (value > 120 ? 'Invalid age' : undefined),
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Form-level validation
|
|
119
|
+
validate: (values) => {
|
|
120
|
+
const errors: any = {};
|
|
121
|
+
if (values.password !== values.confirmPassword) {
|
|
122
|
+
errors.confirmPassword = 'Passwords must match';
|
|
123
|
+
}
|
|
124
|
+
return errors;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Path Handling
|
|
130
|
+
|
|
131
|
+
Formit supports deep nested paths with dot notation and array indices:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Nested objects
|
|
135
|
+
form.setValue('user.profile.name', 'Alice');
|
|
136
|
+
form.getValue('user.profile.name'); // 'Alice'
|
|
137
|
+
|
|
138
|
+
// Arrays with bracket notation
|
|
139
|
+
form.setValue('items[0].title', 'First Item');
|
|
140
|
+
form.getValue('items[0].title'); // 'First Item'
|
|
141
|
+
|
|
142
|
+
// Array path format
|
|
143
|
+
form.setValue(['tags', 1], 'tag2');
|
|
144
|
+
form.getValue(['tags', 1]); // 'tag2'
|
|
145
|
+
|
|
146
|
+
// Auto-creates nested structures
|
|
147
|
+
form.setValue('deep.nested.path', 'value');
|
|
148
|
+
// Creates: { deep: { nested: { path: 'value' } } }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Validation
|
|
152
|
+
|
|
153
|
+
#### Field-Level Validators
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const form = createForm({
|
|
157
|
+
fields: {
|
|
158
|
+
email: {
|
|
159
|
+
validators: [
|
|
160
|
+
// Sync validation
|
|
161
|
+
(value) => (!value ? 'Required' : undefined),
|
|
162
|
+
|
|
163
|
+
// Async validation
|
|
164
|
+
async (value) => {
|
|
165
|
+
const exists = await checkEmailExists(value);
|
|
166
|
+
return exists ? 'Email already taken' : undefined;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Object return (multiple errors)
|
|
170
|
+
(value) => ({
|
|
171
|
+
format: !value.includes('@') ? 'Invalid format' : '',
|
|
172
|
+
length: value.length < 5 ? 'Too short' : '',
|
|
173
|
+
}),
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Validate single field
|
|
180
|
+
const error = await form.validateField('email');
|
|
181
|
+
|
|
182
|
+
// Validate all fields
|
|
183
|
+
const errors = await form.validateAll();
|
|
184
|
+
|
|
185
|
+
// Validate only touched fields (better UX)
|
|
186
|
+
await form.validateAll({ onlyTouched: true });
|
|
187
|
+
|
|
188
|
+
// Validate specific fields
|
|
189
|
+
await form.validateAll({ fields: ['email', 'password'] });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### Form-Level Validation
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const form = createForm({
|
|
196
|
+
validate: (values) => {
|
|
197
|
+
const errors: any = {};
|
|
198
|
+
|
|
199
|
+
if (values.password !== values.confirmPassword) {
|
|
200
|
+
errors.confirmPassword = 'Passwords must match';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (values.startDate > values.endDate) {
|
|
204
|
+
errors.endDate = 'End date must be after start date';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return errors;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Field Binding
|
|
213
|
+
|
|
214
|
+
Bind fields to inputs with automatic state management:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Basic binding
|
|
218
|
+
<input {...form.bind('email')} />
|
|
219
|
+
|
|
220
|
+
// Custom value extractor for select/checkbox
|
|
221
|
+
const selectBinding = form.bind('category', {
|
|
222
|
+
valueExtractor: (e) => e.target.selectedOptions[0].value,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const checkboxBinding = form.bind('agreed', {
|
|
226
|
+
valueExtractor: (e) => e.target.checked,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Disable auto-touch on blur
|
|
230
|
+
const binding = form.bind('field', {
|
|
231
|
+
markTouchedOnBlur: false,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Binding includes: value, onChange, onBlur, name
|
|
235
|
+
const binding = form.bind('name');
|
|
236
|
+
binding.value; // Current value
|
|
237
|
+
binding.onChange; // Change handler
|
|
238
|
+
binding.onBlur; // Blur handler
|
|
239
|
+
binding.name; // Field key
|
|
240
|
+
binding.set; // Setter function
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### State Management
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Get/Set values
|
|
247
|
+
form.getValue('email');
|
|
248
|
+
form.setValue('email', 'test@example.com');
|
|
249
|
+
|
|
250
|
+
form.getValues(); // All values
|
|
251
|
+
form.setValues({ email: 'new@email.com', name: 'Bob' });
|
|
252
|
+
|
|
253
|
+
// Replace all values
|
|
254
|
+
form.setValues({ newField: 'value' }, { replace: true });
|
|
255
|
+
|
|
256
|
+
// Error management
|
|
257
|
+
form.getError('email');
|
|
258
|
+
form.getErrors(); // All errors
|
|
259
|
+
form.setError('email', 'Custom error');
|
|
260
|
+
form.setErrors({ email: 'Error 1', password: 'Error 2' });
|
|
261
|
+
form.resetErrors();
|
|
262
|
+
|
|
263
|
+
// State helpers
|
|
264
|
+
form.isDirty('email'); // Check if modified
|
|
265
|
+
form.isTouched('email'); // Check if touched
|
|
266
|
+
form.markTouched('email'); // Mark as touched
|
|
267
|
+
|
|
268
|
+
// Reset form
|
|
269
|
+
form.reset(); // Reset to initial values
|
|
270
|
+
form.reset({ email: 'new@email.com' }); // Reset to new values
|
|
271
|
+
|
|
272
|
+
// Get state snapshot
|
|
273
|
+
const state = form.getStateSnapshot();
|
|
274
|
+
state.values; // Current values
|
|
275
|
+
state.errors; // Current errors
|
|
276
|
+
state.dirty; // Dirty state map
|
|
277
|
+
state.touched; // Touched state map
|
|
278
|
+
state.isSubmitting; // Submitting status
|
|
279
|
+
state.isValidating; // Validating status
|
|
280
|
+
state.submitCount; // Number of submissions
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Form Submission
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Submit with validation
|
|
287
|
+
await form.submit(async (values) => {
|
|
288
|
+
await api.post('/users', values);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Skip validation
|
|
292
|
+
await form.submit(
|
|
293
|
+
async (values) => {
|
|
294
|
+
await api.post('/draft', values);
|
|
295
|
+
},
|
|
296
|
+
{ validate: false },
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Handle validation errors
|
|
300
|
+
try {
|
|
301
|
+
await form.submit(async (values) => {
|
|
302
|
+
await api.post('/users', values);
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error instanceof ValidationError) {
|
|
306
|
+
console.log('Validation failed:', error.errors);
|
|
307
|
+
} else {
|
|
308
|
+
console.error('Submit failed:', error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Abort submission
|
|
313
|
+
const controller = new AbortController();
|
|
314
|
+
form.submit(
|
|
315
|
+
async (values) => {
|
|
316
|
+
await api.post('/users', values);
|
|
317
|
+
},
|
|
318
|
+
{ signal: controller.signal },
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Later...
|
|
322
|
+
controller.abort();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Subscriptions
|
|
326
|
+
|
|
327
|
+
React to form state changes:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// Subscribe to entire form
|
|
331
|
+
const unsubscribe = form.subscribe((state) => {
|
|
332
|
+
console.log('Form state:', state);
|
|
333
|
+
console.log('Values:', state.values);
|
|
334
|
+
console.log('Errors:', state.errors);
|
|
335
|
+
console.log('Is submitting:', state.isSubmitting);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Subscribe to specific field
|
|
339
|
+
const unsubscribe = form.subscribeField('email', (field) => {
|
|
340
|
+
console.log('Value:', field.value);
|
|
341
|
+
console.log('Error:', field.error);
|
|
342
|
+
console.log('Dirty:', field.dirty);
|
|
343
|
+
console.log('Touched:', field.touched);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Clean up
|
|
347
|
+
unsubscribe();
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Advanced Usage
|
|
351
|
+
|
|
352
|
+
### Multi-Step Forms
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
const form = createForm<WizardForm>({
|
|
356
|
+
/* ... */
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Step 1: Validate only current step fields
|
|
360
|
+
await form.validateAll({ fields: ['firstName', 'lastName'] });
|
|
361
|
+
|
|
362
|
+
// Step 2: Validate only touched fields
|
|
363
|
+
await form.validateAll({ onlyTouched: true });
|
|
364
|
+
|
|
365
|
+
// Final step: Validate all
|
|
366
|
+
await form.validateAll();
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Dynamic Forms
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Add item to array
|
|
373
|
+
const items = form.getValue('items') || [];
|
|
374
|
+
form.setValue('items', [...items, { name: '', quantity: 0 }]);
|
|
375
|
+
|
|
376
|
+
// Update nested item
|
|
377
|
+
form.setValue('items[0].name', 'New Name');
|
|
378
|
+
|
|
379
|
+
// Remove item
|
|
380
|
+
const updatedItems = items.filter((_, i) => i !== indexToRemove);
|
|
381
|
+
form.setValue('items', updatedItems);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Custom Value Extraction
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// Multi-select
|
|
388
|
+
const multiSelect = form.bind('selectedOptions', {
|
|
389
|
+
valueExtractor: (e) =>
|
|
390
|
+
Array.from(e.target.selectedOptions).map(opt => opt.value),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// File input
|
|
394
|
+
const fileInput = form.bind('avatar', {
|
|
395
|
+
valueExtractor: (e) => e.target.files[0],
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Custom component
|
|
399
|
+
const customBinding = form.bind('customField', {
|
|
400
|
+
valueExtractor: (customValue) => customValue,
|
|
401
|
+
});
|
|
402
|
+
<CustomInput onChange={customBinding.onChange} />
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Conditional Validation
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
const form = createForm({
|
|
409
|
+
fields: {
|
|
410
|
+
country: {
|
|
411
|
+
validators: (value) => (!value ? 'Required' : undefined),
|
|
412
|
+
},
|
|
413
|
+
zipCode: {
|
|
414
|
+
validators: (value, values) => {
|
|
415
|
+
// Only validate if country is USA
|
|
416
|
+
if (values.country === 'USA' && !value) {
|
|
417
|
+
return 'ZIP code required for USA';
|
|
418
|
+
}
|
|
419
|
+
return undefined;
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Framework Integration
|
|
427
|
+
|
|
428
|
+
### React
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { createForm } from '@vielzeug/formit';
|
|
432
|
+
import { useEffect, useState } from 'react';
|
|
433
|
+
|
|
434
|
+
function useForm<T>(formFactory: () => ReturnType<typeof createForm<T>>) {
|
|
435
|
+
const [form] = useState(formFactory);
|
|
436
|
+
const [state, setState] = useState(form.getStateSnapshot());
|
|
437
|
+
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
const unsubscribe = form.subscribe(setState);
|
|
440
|
+
return unsubscribe;
|
|
441
|
+
}, [form]);
|
|
442
|
+
|
|
443
|
+
return { form, state };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function MyForm() {
|
|
447
|
+
const { form, state } = useForm(() => createForm({ /* ... */ }));
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<form onSubmit={async (e) => {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
await form.submit(async (values) => {
|
|
453
|
+
await api.post('/data', values);
|
|
454
|
+
});
|
|
455
|
+
}}>
|
|
456
|
+
<input {...form.bind('name')} />
|
|
457
|
+
{state.isSubmitting && <Spinner />}
|
|
458
|
+
</form>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Vue
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
import { createForm } from '@vielzeug/formit';
|
|
467
|
+
import { reactive, onMounted, onUnmounted } from 'vue';
|
|
468
|
+
|
|
469
|
+
export function useForm<T>(config) {
|
|
470
|
+
const form = createForm<T>(config);
|
|
471
|
+
const state = reactive(form.getStateSnapshot());
|
|
472
|
+
|
|
473
|
+
let unsubscribe: (() => void) | null = null;
|
|
474
|
+
|
|
475
|
+
onMounted(() => {
|
|
476
|
+
unsubscribe = form.subscribe((newState) => {
|
|
477
|
+
Object.assign(state, newState);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
onUnmounted(() => {
|
|
482
|
+
unsubscribe?.();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return { form, state };
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Svelte
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
import { createForm } from '@vielzeug/formit';
|
|
493
|
+
import { writable } from 'svelte/store';
|
|
494
|
+
import { onMount, onDestroy } from 'svelte';
|
|
495
|
+
|
|
496
|
+
export function useForm<T>(config) {
|
|
497
|
+
const form = createForm<T>(config);
|
|
498
|
+
const state = writable(form.getStateSnapshot());
|
|
499
|
+
|
|
500
|
+
let unsubscribe: (() => void) | null = null;
|
|
501
|
+
|
|
502
|
+
onMount(() => {
|
|
503
|
+
unsubscribe = form.subscribe((newState) => {
|
|
504
|
+
state.set(newState);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
onDestroy(() => {
|
|
509
|
+
unsubscribe?.();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return { form, state };
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## API Reference
|
|
517
|
+
|
|
518
|
+
### Form Methods
|
|
519
|
+
|
|
520
|
+
| Method | Description |
|
|
521
|
+
| --------------------------------- | ------------------------------------------------------------ |
|
|
522
|
+
| `bind(path, config?)` | Create binding object for input with value, onChange, onBlur |
|
|
523
|
+
| `getValue(path)` | Get value at path |
|
|
524
|
+
| `getValues()` | Get all form values |
|
|
525
|
+
| `setValue(path, value, options?)` | Set value at path |
|
|
526
|
+
| `setValues(values, options?)` | Set multiple values |
|
|
527
|
+
| `getError(path)` | Get error for field |
|
|
528
|
+
| `getErrors()` | Get all errors |
|
|
529
|
+
| `setError(path, message?)` | Set error for field |
|
|
530
|
+
| `setErrors(errors)` | Set multiple errors |
|
|
531
|
+
| `resetErrors()` | Clear all errors |
|
|
532
|
+
| `isDirty(path)` | Check if field is modified |
|
|
533
|
+
| `isTouched(path)` | Check if field is touched |
|
|
534
|
+
| `markTouched(path)` | Mark field as touched |
|
|
535
|
+
| `validateField(path, signal?)` | Validate single field |
|
|
536
|
+
| `validateAll(options?)` | Validate all/specific fields |
|
|
537
|
+
| `submit(onSubmit, options?)` | Submit form with validation |
|
|
538
|
+
| `reset(initialValues?)` | Reset form state |
|
|
539
|
+
| `getStateSnapshot()` | Get immutable state snapshot |
|
|
540
|
+
| `subscribe(listener)` | Subscribe to form changes |
|
|
541
|
+
| `subscribeField(path, listener)` | Subscribe to field changes |
|
|
542
|
+
|
|
543
|
+
### Types
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// Form configuration
|
|
547
|
+
interface FormInit<TForm> {
|
|
548
|
+
initialValues?: TForm;
|
|
549
|
+
fields?: Record<string, FieldConfig>;
|
|
550
|
+
validate?: (values: TForm) => Errors | Promise<Errors>;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Field configuration
|
|
554
|
+
interface FieldConfig<TValue, TForm> {
|
|
555
|
+
initialValue?: TValue;
|
|
556
|
+
validators?: Validator<TValue, TForm> | Array<Validator<TValue, TForm>>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
type Validator<TValue, TForm> = (
|
|
560
|
+
value: TValue,
|
|
561
|
+
values: TForm,
|
|
562
|
+
) => MaybePromise<string | Record<string, string> | undefined | null>;
|
|
563
|
+
|
|
564
|
+
// Bind configuration
|
|
565
|
+
interface BindConfig {
|
|
566
|
+
valueExtractor?: (event: any) => any;
|
|
567
|
+
markTouchedOnBlur?: boolean;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Form state
|
|
571
|
+
interface FormState<TForm> {
|
|
572
|
+
values: TForm;
|
|
573
|
+
errors: Errors;
|
|
574
|
+
dirty: Record<string, boolean>;
|
|
575
|
+
touched: Record<string, boolean>;
|
|
576
|
+
isSubmitting: boolean;
|
|
577
|
+
isValidating: boolean;
|
|
578
|
+
submitCount: number;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Validation error
|
|
582
|
+
class ValidationError extends Error {
|
|
583
|
+
errors: Errors;
|
|
584
|
+
type: 'validation';
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Best Practices
|
|
589
|
+
|
|
590
|
+
### 1. Type Safety
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
// Define interface for type safety
|
|
594
|
+
interface UserForm {
|
|
595
|
+
name: string;
|
|
596
|
+
email: string;
|
|
597
|
+
age: number;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const form = createForm<UserForm>({
|
|
601
|
+
initialValues: { name: '', email: '', age: 0 },
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// TypeScript will catch errors
|
|
605
|
+
form.setValue('nam', 'test'); // ❌ Error: 'nam' doesn't exist
|
|
606
|
+
form.setValue('name', 123); // ❌ Error: type mismatch
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### 2. Validation Strategy
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
// ✅ Show errors only after touch
|
|
613
|
+
{form.isTouched('email') && form.getError('email') && (
|
|
614
|
+
<ErrorMessage>{form.getError('email')}</ErrorMessage>
|
|
615
|
+
)}
|
|
616
|
+
|
|
617
|
+
// ✅ Validate on submit, not on every change
|
|
618
|
+
await form.submit(async (values) => {
|
|
619
|
+
// Validation happens automatically before this
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 3. Performance Optimization
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
// ✅ Validate only touched fields for better UX
|
|
627
|
+
await form.validateAll({ onlyTouched: true });
|
|
628
|
+
|
|
629
|
+
// ✅ Validate specific fields in multi-step forms
|
|
630
|
+
await form.validateAll({ fields: ['step1Field1', 'step1Field2'] });
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### 4. Error Handling
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// ✅ Handle validation vs submission errors
|
|
637
|
+
try {
|
|
638
|
+
await form.submit(async (values) => {
|
|
639
|
+
await api.post('/users', values);
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (error instanceof ValidationError) {
|
|
643
|
+
// Handle validation errors
|
|
644
|
+
console.log(error.errors);
|
|
645
|
+
} else {
|
|
646
|
+
// Handle API/network errors
|
|
647
|
+
showNotification('Failed to submit');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## Comparison
|
|
653
|
+
|
|
654
|
+
| Feature | Formit | Formik | React Hook Form |
|
|
655
|
+
| ------------------------- | -------------- | ------------------- | --------------- |
|
|
656
|
+
| Framework Agnostic | ✅ | ❌ React only | ❌ React only |
|
|
657
|
+
| TypeScript Support | ✅ First-class | ✅ Good | ✅ Good |
|
|
658
|
+
| Nested Paths | ✅ Native | ✅ Via dot notation | ✅ Via register |
|
|
659
|
+
| Async Validation | ✅ Built-in | ✅ Built-in | ✅ Via resolver |
|
|
660
|
+
| Bundle Size | **~2KB** | ~13KB | ~9KB |
|
|
661
|
+
| Dependencies | 0 | React | React |
|
|
662
|
+
| Field-Level Subscriptions | ✅ | ⚠️ Limited | ✅ |
|
|
663
|
+
| Granular Validation | ✅ | ❌ | ⚠️ Limited |
|
|
664
|
+
| Custom Bind Config | ✅ | ❌ | ❌ |
|
|
665
|
+
|
|
666
|
+
## License
|
|
667
|
+
|
|
668
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
669
|
+
|
|
670
|
+
## Links
|
|
671
|
+
|
|
672
|
+
- [GitHub Repository](https://github.com/helmuthdu/vielzeug)
|
|
673
|
+
- [Documentation](https://vielzeug.dev)
|
|
674
|
+
- [NPM Package](https://www.npmjs.com/package/@vielzeug/formit)
|
|
675
|
+
- [Issue Tracker](https://github.com/helmuthdu/vielzeug/issues)
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) ecosystem - A collection of type-safe utilities for modern web development.
|
package/dist/formit.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class j extends Error{errors;type="validation";constructor(l){super("Form validation failed"),this.name="ValidationError",this.errors=l,typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,j)}}function O(c){if(Array.isArray(c))return c;const l=String(c).trim();if(!l)return[];const y=[],i=/([^.[\]]+)|\[(\d+)]/g;for(let n;n=i.exec(l);)n[1]!==void 0?y.push(n[1]):n[2]!==void 0&&y.push(Number(n[2]));return y}function v(c){return O(c).map(String).join(".")}function E(c,l,y){const i=O(l);let n=c;for(const f of i){if(n==null)return y;n=n[f]}return n===void 0?y:n}function L(c,l,y){const i=O(l);if(i.length===0)return y;const f=typeof i[0]=="number",m=Array.isArray(c)?[...c]:f?[]:{...c??{}};let g=m;for(let u=0;u<i.length;u++){const b=i[u];if(u===i.length-1)g[b]=y;else{const k=i[u+1],S=g[b],w=typeof k=="number";let a;Array.isArray(S)?a=[...S]:S&&typeof S=="object"?a={...S}:a=w?[]:{},g[b]=a,g=a}}return m}function tt(c={}){const l=c.fields??{},y=c.validate;let i=S(c.initialValues??{},l),n={};const f={},m={};let g=!1,u=!1,b=0;const A=new Set,k=new Map;function S(t,r){let e={...t};for(const o of Object.keys(r)){const s=r[o];s?.initialValue!==void 0&&E(e,o)===void 0&&(e=L(e,o,s.initialValue))}return e}let w=!1;function a(){w||(w=!0,Promise.resolve().then(()=>{w=!1,M()}))}function M(){const t={dirty:{...m},errors:n,isSubmitting:u,isValidating:g,submitCount:b,touched:{...f},values:i};for(const r of A)try{r(t)}catch{}for(const[r,e]of k.entries()){const o=E(i,r),s=n[r],h=f[r]||!1,p=m[r]||!1;for(const V of e)try{V({dirty:p,error:s,touched:h,value:o})}catch{}}}function P(t){if(t){if(typeof t=="string")return t;if(typeof t=="object"){const r=Object.values(t).filter(Boolean);return r.length>0?r.join("; "):void 0}}}async function F(t,r){const e=l[t];if(!e?.validators)return;const o=Array.isArray(e.validators)?e.validators:[e.validators],s=E(i,t);for(const h of o){if(r?.aborted)throw new Error("Validation aborted");const p=await h(s,i),V=P(p);if(V)return V}}async function C(t){if(!y)return{};if(t?.aborted)throw new Error("Validation aborted");return await y(i)??{}}function z(){return i}function N(t){return E(i,t)}function x(t,r,e={}){const o=v(t),s=E(i,t);return i=L(i,t,r),(e.markDirty??!0)&&s!==r&&(m[o]=!0),e.markTouched&&(f[o]=!0),a(),r}function q(t,r={}){if(r.replace?i={...t}:i={...i,...t},r.markAllDirty)for(const e of Object.keys(t))m[e]=!0;a()}function G(){return n}function H(t){return n[v(t)]}function I(t,r){const e=v(t);if(r){n={...n,[e]:r},a();return}if(!(e in n))return;const o={...n};delete o[e],n=o,a()}function J(){n={},a()}function B(t){f[v(t)]=!0,a()}async function K(t,r){const e=v(t);g=!0,a();try{const o=await F(e,r);if(o)n={...n,[e]:o};else{const{[e]:s,...h}=n;n=h}return o}finally{g=!1,a()}}async function D(t){g=!0,a();const r=t?.signal;try{const e={};let o=new Set([...Object.keys(l),...Object.keys(i)]);t?.onlyTouched&&(o=new Set(Array.from(o).filter(s=>f[s]))),t?.fields&&t.fields.length>0&&(o=new Set(t.fields));for(const s of o){if(r?.aborted)throw new Error("Validation aborted");const h=await F(s,r);h&&(e[s]=h)}try{const s=await C(r);Object.assign(e,s)}catch(s){e[""]=s instanceof Error?s.message:String(s)}return n=e,n}finally{g=!1,a()}}async function Q(t,r){if(u)return Promise.reject(new Error("Form is already being submitted"));b+=1,u=!0,a();const e=r?.signal;try{if((r?.validate??!0)&&await D({signal:e}),Object.keys(n).length>0)return u=!1,a(),Promise.reject(new j(n));const s=await t(i);return u=!1,a(),s}catch(o){throw u=!1,a(),o}}function R(t){A.add(t);try{t({dirty:{...m},errors:n,isSubmitting:u,isValidating:g,submitCount:b,touched:{...f},values:i})}catch{}return()=>A.delete(t)}function U(t,r){const e=v(t);let o=k.get(e);o||(o=new Set,k.set(e,o)),o.add(r);try{r({dirty:m[e]||!1,error:n[e],touched:f[e]||!1,value:E(i,e)})}catch{}return()=>{o.delete(r),o.size===0&&k.delete(e)}}function W(t,r){const e=v(t),o=r?.valueExtractor??(d=>d&&typeof d=="object"&&"target"in d?d.target.value:d),s=r?.markTouchedOnBlur??!0,h=d=>{const T=E(i,t),$=typeof d=="function"?d(T):d;x(t,$,{markDirty:!0,markTouched:!0})};return{name:e,onBlur:()=>{s&&B(t)},onChange:d=>{const T=o(d);h(T)},set:h,get value(){return E(i,e)},set value(d){h(d)}}}function X(t){return m[v(t)]||!1}function Y(t){return f[v(t)]||!1}function Z(t){i=S(t??c.initialValues??{},l),n={}}function _(t){n={...t},a()}return{bind:W,getError:H,getErrors:G,getStateSnapshot:()=>({dirty:{...m},errors:n,isSubmitting:u,isValidating:g,submitCount:b,touched:{...f},values:i}),getValue:N,getValues:z,isDirty:X,isTouched:Y,markTouched:B,reset:Z,resetErrors:J,setError:I,setErrors:_,setValue:x,setValues:q,submit:Q,subscribe:R,subscribeField:U,validateAll:D,validateField:K}}exports.ValidationError=j;exports.createForm=tt;
|
|
2
|
+
//# sourceMappingURL=formit.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formit.cjs","sources":["../src/formit.ts"],"sourcesContent":["/** biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: - */\n/** biome-ignore-all lint/suspicious/noExplicitAny: - */\n\n// formit - minimal, typed, no array helpers\n\n/** -------------------- Types -------------------- **/\n\ntype MaybePromise<T> = T | Promise<T>;\nexport type Path = string | Array<string | number>;\n\n/**\n * Error thrown when form validation fails during submission\n */\nexport class ValidationError extends Error {\n public readonly errors: Errors;\n public readonly type = 'validation' as const;\n\n constructor(errors: Errors) {\n super('Form validation failed');\n this.name = 'ValidationError';\n this.errors = errors;\n\n // Maintain a proper stack trace for where the error was thrown (V8 only)\n if (typeof (Error as any).captureStackTrace === 'function') {\n (Error as any).captureStackTrace(this, ValidationError);\n }\n }\n}\n\n/** -------------------- Path Utilities -------------------- **/\n\n/**\n * Converts a path to an array of keys and indices.\n *\n * @param path - The path to convert (string or array)\n * @returns Array of path segments (strings and numbers)\n *\n * @example\n * ```ts\n * toPathArray('user.name') // ['user', 'name']\n * toPathArray('items[0]') // ['items', 0]\n * toPathArray(['users', 0, 'name']) // ['users', 0, 'name']\n * ```\n */\nfunction toPathArray(path: Path): Array<string | number> {\n if (Array.isArray(path)) return path;\n\n const pathString = String(path).trim();\n if (!pathString) return [];\n\n const segments: Array<string | number> = [];\n const regex = /([^.[\\]]+)|\\[(\\d+)]/g;\n\n // biome-ignore lint/suspicious/noAssignInExpressions: Standard regex iteration pattern\n for (let match: RegExpExecArray | null; (match = regex.exec(pathString)); ) {\n if (match[1] !== undefined) segments.push(match[1]);\n else if (match[2] !== undefined) segments.push(Number(match[2]));\n }\n\n return segments;\n}\n\n/**\n * Converts a path to a dot-notation string key.\n *\n * @param path - The path to convert\n * @returns Dot-notation string representation\n *\n * @example\n * ```ts\n * toKey('user.name') // 'user.name'\n * toKey(['user', 'name']) // 'user.name'\n * toKey(['items', 0, 'title']) // 'items.0.title'\n * ```\n */\nfunction toKey(path: Path): string {\n return toPathArray(path).map(String).join('.');\n}\n\n/**\n * Gets a value from an object using a path.\n *\n * @param obj - The object to read from\n * @param path - The path to the value\n * @param fallback - Optional fallback value if path is not found\n * @returns The value at the path, or fallback if not found\n *\n * @example\n * ```ts\n * getAt({ user: { name: 'Alice' } }, 'user.name') // 'Alice'\n * getAt({ items: [{ id: 1 }] }, 'items[0].id') // 1\n * getAt({}, 'missing', 'default') // 'default'\n * ```\n */\nfunction getAt(obj: any, path: Path, fallback?: any): any {\n const pathSegments = toPathArray(path);\n let current = obj;\n\n for (const segment of pathSegments) {\n if (current == null) return fallback;\n current = current[segment as any];\n }\n\n return current === undefined ? fallback : current;\n}\n\n/**\n * Sets a value in an object using a path (immutably).\n *\n * @param obj - The object to update\n * @param path - The path where to set the value\n * @param value - The value to set\n * @returns A new object with the value set at the path\n *\n * @example\n * ```ts\n * setAt({}, 'user.name', 'Alice')\n * // { user: { name: 'Alice' } }\n *\n * setAt({}, 'items[0].title', 'First')\n * // { items: [{ title: 'First' }] }\n *\n * setAt({ count: 1 }, 'count', 2)\n * // { count: 2 }\n * ```\n */\nfunction setAt(obj: any, path: Path, value: any): any {\n const pathSegments = toPathArray(path);\n\n if (pathSegments.length === 0) return value;\n\n // Create a shallow copy of root - detect if you should be arrayed\n const firstSegment = pathSegments[0];\n const rootShouldBeArray = typeof firstSegment === 'number';\n const root = Array.isArray(obj) ? [...obj] : rootShouldBeArray ? [] : { ...(obj ?? {}) };\n\n let current: any = root;\n\n for (let i = 0; i < pathSegments.length; i++) {\n const segment = pathSegments[i];\n const isLastSegment = i === pathSegments.length - 1;\n\n if (isLastSegment) {\n current[segment as any] = value;\n } else {\n const nextSegment = pathSegments[i + 1];\n const nextValue = current[segment as any];\n\n // Determine if the next level should be an array (numeric key) or object\n const shouldBeArray = typeof nextSegment === 'number';\n\n let copy: any;\n if (Array.isArray(nextValue)) {\n copy = [...nextValue];\n } else if (nextValue && typeof nextValue === 'object') {\n copy = { ...nextValue };\n } else {\n // Create a new container-array if the next segment is numeric, object otherwise\n copy = shouldBeArray ? [] : {};\n }\n\n current[segment as any] = copy;\n current = copy;\n }\n }\n\n return root;\n}\n\n/** -------------------- Form Types -------------------- **/\n\nexport type Errors = Partial<Record<string, string>>;\n\nexport type FieldValidator<TValue, TForm> =\n | ((value: TValue, values: TForm) => MaybePromise<string | undefined | null>)\n | ((value: TValue, values: TForm) => MaybePromise<Record<string, string> | undefined | null>);\n\nexport type FormValidator<TForm> = (values: TForm) => MaybePromise<Errors | undefined | null>;\n\nexport type FieldConfig<TValue, TForm> = {\n initialValue?: TValue;\n validators?: FieldValidator<TValue, TForm> | Array<FieldValidator<TValue, TForm>>;\n};\n\nexport type FormInit<TForm extends Record<string, any> = Record<string, any>> = {\n initialValues?: TForm;\n fields?: Partial<{ [K in keyof TForm & string]: FieldConfig<TForm[K], TForm> }>;\n validate?: FormValidator<TForm>;\n};\n\nexport type FormState<TForm> = {\n values: TForm;\n errors: Errors;\n touched: Record<string, boolean>;\n dirty: Record<string, boolean>;\n isValidating: boolean;\n isSubmitting: boolean;\n submitCount: number;\n};\n\ntype Listener<TForm> = (state: FormState<TForm>) => void;\ntype FieldListener<TValue> = (payload: { value: TValue; error?: string; touched: boolean; dirty: boolean }) => void;\n\nexport type BindConfig = {\n /**\n * Custom value extractor from event\n * @default (event) => event?.target?.value ?? event\n */\n valueExtractor?: (event: any) => any;\n /**\n * Whether to mark field as touched on blur\n * @default true\n */\n markTouchedOnBlur?: boolean;\n};\n\n/** -------------------- Form Creation -------------------- **/\n\nexport function createForm<TForm extends Record<string, any> = Record<string, any>>(init: FormInit<TForm> = {}) {\n const fieldConfigs = (init.fields ?? {}) as Partial<Record<string, FieldConfig<any, TForm>>>;\n const formValidator = init.validate;\n\n // Initialize values with initial values and field configs\n let values = initializeValues(init.initialValues ?? ({} as TForm), fieldConfigs);\n let errors: Errors = {};\n const touched: Record<string, boolean> = {};\n const dirty: Record<string, boolean> = {};\n let isValidating = false;\n let isSubmitting = false;\n let submitCount = 0;\n\n const listeners = new Set<Listener<TForm>>();\n const fieldListeners = new Map<string, Set<FieldListener<any>>>();\n\n /** -------------------- Internal Helpers -------------------- **/\n\n /**\n * Initialize form values from initial values and field configs.\n *\n * @param initialValues - The initial form values\n * @param configs - Field configurations with initialValue properties\n * @returns Merged form values with field config initial values applied\n */\n function initializeValues(initialValues: TForm, configs: Partial<Record<string, FieldConfig<any, TForm>>>): TForm {\n let result = { ...initialValues };\n\n for (const key of Object.keys(configs)) {\n const config = configs[key];\n if (config?.initialValue !== undefined && getAt(result, key) === undefined) {\n result = setAt(result, key, config.initialValue) as TForm;\n }\n }\n\n return result;\n }\n\n /**\n * Schedule notification to all listeners (debounced to next tick).\n */\n let scheduled = false;\n function scheduleNotify() {\n if (scheduled) return;\n scheduled = true;\n\n Promise.resolve().then(() => {\n scheduled = false;\n notifyListeners();\n });\n }\n\n /**\n * Notify all form and field listeners of state changes.\n */\n function notifyListeners() {\n const snapshot: FormState<TForm> = {\n dirty: { ...dirty },\n errors,\n isSubmitting,\n isValidating,\n submitCount,\n touched: { ...touched },\n values,\n };\n\n // Notify form listeners\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch {\n // Swallow listener errors to prevent breaking other listeners\n }\n }\n\n // Notify field listeners\n for (const [path, listenerSet] of fieldListeners.entries()) {\n const value = getAt(values, path);\n const error = errors[path];\n const isTouched = touched[path] || false;\n const isDirty = dirty[path] || false;\n\n for (const fieldListener of listenerSet) {\n try {\n fieldListener({ dirty: isDirty, error, touched: isTouched, value });\n } catch {\n // Swallow listener errors\n }\n }\n }\n }\n\n /**\n * Convert validator result to error message string.\n */\n function resultToErrorMessage(result: any): string | undefined {\n if (!result) return undefined;\n if (typeof result === 'string') return result;\n\n // Object with error messages - join all non-empty values\n if (typeof result === 'object') {\n const errorMessages = Object.values(result).filter(Boolean) as string[];\n return errorMessages.length > 0 ? errorMessages.join('; ') : undefined;\n }\n\n return undefined;\n }\n\n /**\n * Run all validators for a specific field.\n *\n * @param pathKey - The field path key to validate\n * @param signal - Optional AbortSignal for cancellation\n * @returns Error message if validation failed, undefined otherwise\n *\n * @throws {DOMException} When validation is aborted via signal\n */\n async function runFieldValidators(pathKey: string, signal?: AbortSignal): Promise<string | undefined> {\n const config = fieldConfigs[pathKey];\n if (!config?.validators) return undefined;\n\n const validators = Array.isArray(config.validators) ? config.validators : [config.validators];\n const fieldValue = getAt(values, pathKey);\n\n for (const validator of validators) {\n if (signal?.aborted) {\n throw new Error('Validation aborted');\n }\n\n const result = await validator(fieldValue, values);\n const errorMessage = resultToErrorMessage(result);\n if (errorMessage) return errorMessage;\n }\n\n return undefined;\n }\n\n /**\n * Run the form-level validator.\n *\n * @param signal - Optional AbortSignal for cancellation\n * @returns Object with field errors, or empty object if no errors\n *\n * @throws {DOMException} When validation is aborted via signal\n */\n async function runFormValidator(signal?: AbortSignal): Promise<Errors> {\n if (!formValidator) return {};\n\n if (signal?.aborted) {\n throw new Error('Validation aborted');\n }\n\n const result = await formValidator(values);\n return (result ?? {}) as Errors;\n }\n\n /** -------------------- Public API - Value Management -------------------- **/\n\n /**\n * Get all form values.\n */\n function getValues(): TForm {\n return values;\n }\n\n /**\n * Get a specific field value by path.\n *\n * @param path - The field path (e.g., 'user.name' or ['items', 0, 'title'])\n * @returns The value at the specified path\n */\n function getValue(path: Path) {\n return getAt(values, path);\n }\n\n /**\n * Set a specific field value by a path.\n *\n * @param path - The field path (e.g., 'user.name' or ['items', 0, 'title'])\n * @param value - The value to set\n * @param options - Optional configuration\n * @param options.markDirty - Whether to mark the field as dirty (default: true)\n * @param options.markTouched - Whether to mark the field as touched (default: false)\n * @returns The value that was set\n */\n function setValue(path: Path, value: any, options: { markDirty?: boolean; markTouched?: boolean } = {}) {\n const key = toKey(path);\n const previousValue = getAt(values, path);\n\n values = setAt(values, path, value) as TForm;\n\n if (options.markDirty ?? true) {\n // Reference equality check - objects/arrays with same content but different refs will be marked dirty\n if (previousValue !== value) {\n dirty[key] = true;\n }\n }\n\n if (options.markTouched) {\n touched[key] = true;\n }\n\n scheduleNotify();\n return value;\n }\n\n /**\n * Set multiple form values at once.\n *\n * @param nextValues - Partial form values to merge or replace\n * @param options - Optional configuration\n * @param options.replace - If true, replaces all values; if false, merges with existing (default: false)\n * @param options.markAllDirty - If true, marks all changed fields as dirty (default: false)\n */\n function setValues(nextValues: Partial<TForm>, options: { replace?: boolean; markAllDirty?: boolean } = {}) {\n if (options.replace) {\n values = { ...nextValues } as TForm;\n } else {\n values = { ...values, ...nextValues } as TForm;\n }\n\n if (options.markAllDirty) {\n for (const key of Object.keys(nextValues)) {\n dirty[key] = true;\n }\n }\n\n scheduleNotify();\n }\n\n /** -------------------- Public API - Error Management -------------------- **/\n\n /**\n * Get all form errors.\n *\n * @returns Object containing all field errors keyed by field path\n */\n function getErrors() {\n return errors;\n }\n\n /**\n * Get a specific field error by path.\n *\n * @param path - The field path\n * @returns Error message for the field, or undefined if no error\n */\n function getError(path: Path) {\n return errors[toKey(path)];\n }\n\n /**\n * Set a specific field error by path.\n *\n * @param path - The field path\n * @param message - Error message to set, or undefined to clear the error\n */\n function setError(path: Path, message?: string) {\n const key = toKey(path);\n\n if (message) {\n errors = { ...errors, [key]: message };\n scheduleNotify();\n return;\n }\n\n // Clear error\n if (!(key in errors)) return;\n\n const copy = { ...errors };\n delete copy[key];\n errors = copy;\n scheduleNotify();\n }\n\n /**\n * Reset all form errors.\n *\n * @remarks\n * Clears all error messages and triggers a state notification.\n */\n function resetErrors() {\n errors = {};\n scheduleNotify();\n }\n\n /** -------------------- Public API - Touch Management -------------------- **/\n\n /**\n * Mark a field as touched.\n *\n * @param path - The field path to mark as touched\n *\n * @remarks\n * Touched fields are typically used to show validation errors only after user interaction.\n */\n function markTouched(path: Path) {\n touched[toKey(path)] = true;\n scheduleNotify();\n }\n\n /** -------------------- Public API - Validation -------------------- **/\n\n /**\n * Validate a single field.\n *\n * @param path - The field path to validate\n * @param signal - Optional AbortSignal for cancellation\n * @returns Error message if validation failed, undefined otherwise\n *\n * @throws {DOMException} When validation is aborted via signal\n */\n async function validateField(path: Path, signal?: AbortSignal) {\n const key = toKey(path);\n isValidating = true;\n scheduleNotify();\n\n try {\n const error = await runFieldValidators(key, signal);\n\n // Update errors object immutably\n if (error) {\n errors = { ...errors, [key]: error };\n } else {\n const { [key]: _, ...rest } = errors;\n errors = rest;\n }\n\n return error;\n } finally {\n isValidating = false;\n scheduleNotify();\n }\n }\n\n /**\n * Validate all fields and form-level validators.\n *\n * @param options - Optional validation configuration\n * @param options.signal - Optional AbortSignal for cancellation\n * @param options.onlyTouched - If true, only validate touched fields\n * @param options.fields - If provided, only validate these specific fields\n * @returns Object containing all field errors\n *\n * @throws {Error} When validation is aborted via signal\n */\n async function validateAll(options?: { signal?: AbortSignal; onlyTouched?: boolean; fields?: string[] }) {\n isValidating = true;\n scheduleNotify();\n\n const signal = options?.signal;\n\n try {\n const nextErrors: Errors = {};\n\n // Collect all field paths to validate\n let fieldsToValidate = new Set<string>([...Object.keys(fieldConfigs), ...Object.keys(values)]);\n\n // Filter by touched fields if requested\n if (options?.onlyTouched) {\n fieldsToValidate = new Set(Array.from(fieldsToValidate).filter((key) => touched[key]));\n }\n\n // Filter by specific fields if requested\n if (options?.fields && options.fields.length > 0) {\n fieldsToValidate = new Set(options.fields);\n }\n\n // Run field validators\n for (const path of fieldsToValidate) {\n if (signal?.aborted) {\n throw new Error('Validation aborted');\n }\n\n const error = await runFieldValidators(path, signal);\n if (error) nextErrors[path] = error;\n }\n\n // Run form-level validator\n try {\n const formErrors = await runFormValidator(signal);\n Object.assign(nextErrors, formErrors);\n } catch (error) {\n nextErrors[''] = error instanceof Error ? error.message : String(error);\n }\n\n errors = nextErrors;\n return errors;\n } finally {\n isValidating = false;\n scheduleNotify();\n }\n }\n\n /** -------------------- Public API - Form Submission -------------------- **/\n\n /**\n * Submit the form with optional validation.\n *\n * @param onSubmit - Callback function to handle form submission with validated values\n * @param options - Optional configuration\n * @param options.signal - Optional AbortSignal for cancellation\n * @param options.validate - Whether to run validation before submission (default: true)\n * @returns Promise resolving to the result of onSubmit callback\n *\n * @throws {ValidationError} When validation fails and form has errors\n * @throws {Error} When form is already submitting\n * @throws {Error} When submission is aborted via signal\n */\n async function submit(\n onSubmit: (values: TForm) => MaybePromise<any>,\n options?: { signal?: AbortSignal; validate?: boolean },\n ) {\n if (isSubmitting) {\n return Promise.reject(new Error('Form is already being submitted'));\n }\n\n submitCount += 1;\n isSubmitting = true;\n scheduleNotify();\n\n const signal = options?.signal;\n\n try {\n // Run validation if requested\n if (options?.validate ?? true) {\n await validateAll({ signal });\n }\n\n // Check for validation errors\n const hasErrors = Object.keys(errors).length > 0;\n if (hasErrors) {\n isSubmitting = false;\n scheduleNotify();\n return Promise.reject(new ValidationError(errors));\n }\n\n // Execute submit handler\n const result = await onSubmit(values);\n\n isSubmitting = false;\n scheduleNotify();\n\n return result;\n } catch (error) {\n isSubmitting = false;\n scheduleNotify();\n throw error;\n }\n }\n\n /** -------------------- Public API - Subscriptions -------------------- **/\n\n /**\n * Subscribe to form state changes.\n *\n * @param listener - Callback function that receives form state snapshots\n * @returns Unsubscribe function to stop listening to state changes\n *\n * @remarks\n * The listener is immediately called with the current state upon subscription.\n * Listener errors are swallowed to prevent breaking other listeners.\n */\n function subscribe(listener: Listener<TForm>) {\n listeners.add(listener);\n\n // Immediately notify the new listener with current state\n try {\n listener({\n dirty: { ...dirty },\n errors,\n isSubmitting,\n isValidating,\n submitCount,\n touched: { ...touched },\n values,\n });\n } catch {\n // Swallow listener errors\n }\n\n return () => listeners.delete(listener);\n }\n\n /**\n * Subscribe to a specific field's changes.\n *\n * @param path - The field path to subscribe to\n * @param listener - Callback function that receives field state updates\n * @returns Unsubscribe function to stop listening to field changes\n */\n function subscribeField(path: Path, listener: FieldListener<any>) {\n const key = toKey(path);\n let listenerSet = fieldListeners.get(key);\n\n if (!listenerSet) {\n listenerSet = new Set();\n fieldListeners.set(key, listenerSet);\n }\n\n listenerSet.add(listener);\n\n // Immediately notify the new listener with the current state\n try {\n listener({\n dirty: dirty[key] || false,\n error: errors[key],\n touched: touched[key] || false,\n value: getAt(values, key),\n });\n } catch {\n // Swallow listener errors\n }\n\n return () => {\n listenerSet!.delete(listener);\n // Clean up empty listener sets to prevent memory leaks\n if (listenerSet!.size === 0) {\n fieldListeners.delete(key);\n }\n };\n }\n\n /** -------------------- Public API - Field Binding -------------------- **/\n\n /**\n * Create a binding object for a field that can be used with inputs.\n *\n * @param path - The field path to bind\n * @param config - Optional configuration for value extraction and blur behavior\n * @returns Object with value, onChange, onBlur, and setter methods for input binding\n *\n * @example\n * ```tsx\n * const nameBinding = bind('user.name');\n * <input {...nameBinding} />\n *\n * // With custom value extractor\n * const selectBinding = bind('category', {\n * valueExtractor: (e) => e.target.selectedOptions[0].value\n * });\n *\n * // Disable mark touched on blur\n * const fieldBinding = bind('field', { markTouchedOnBlur: false });\n * ```\n */\n function bind(path: Path, config?: BindConfig) {\n const key = toKey(path);\n const valueExtractor =\n config?.valueExtractor ??\n ((event: any) => (event && typeof event === 'object' && 'target' in event ? (event.target as any).value : event));\n const markTouchedOnBlur = config?.markTouchedOnBlur ?? true;\n\n const setter = (newValue: any | ((prev: any) => any)) => {\n const previousValue = getAt(values, path);\n const nextValue = typeof newValue === 'function' ? (newValue as (prev: any) => any)(previousValue) : newValue;\n\n setValue(path, nextValue, { markDirty: true, markTouched: true });\n };\n\n const onChange = (event: any) => {\n const value = valueExtractor(event);\n setter(value);\n };\n\n const onBlur = () => {\n if (markTouchedOnBlur) {\n markTouched(path);\n }\n };\n\n return {\n name: key,\n onBlur,\n onChange,\n set: setter,\n get value() {\n return getAt(values, key);\n },\n set value(newValue: any) {\n setter(newValue);\n },\n };\n }\n\n function isDirty(path: Path): boolean {\n return dirty[toKey(path)] || false;\n }\n\n function isTouched(path: Path): boolean {\n return touched[toKey(path)] || false;\n }\n\n function reset(initialValues?: TForm) {\n values = initializeValues(initialValues ?? init.initialValues ?? ({} as TForm), fieldConfigs);\n errors = {};\n }\n\n function setErrors(next: Errors) {\n errors = { ...next };\n scheduleNotify();\n }\n\n /** -------------------- Return Public API -------------------- **/\n\n return {\n bind,\n getError,\n getErrors,\n getStateSnapshot: (): FormState<TForm> => ({\n dirty: { ...dirty },\n errors,\n isSubmitting,\n isValidating,\n submitCount,\n touched: { ...touched },\n values,\n }),\n getValue,\n getValues,\n isDirty,\n isTouched,\n markTouched,\n reset,\n resetErrors,\n setError,\n setErrors,\n setValue,\n setValues,\n submit,\n subscribe,\n subscribeField,\n validateAll,\n validateField,\n };\n}\n"],"names":["ValidationError","errors","toPathArray","path","pathString","segments","regex","match","toKey","getAt","obj","fallback","pathSegments","current","segment","setAt","value","rootShouldBeArray","root","i","nextSegment","nextValue","shouldBeArray","copy","createForm","init","fieldConfigs","formValidator","values","initializeValues","touched","dirty","isValidating","isSubmitting","submitCount","listeners","fieldListeners","initialValues","configs","result","key","config","scheduled","scheduleNotify","notifyListeners","snapshot","listener","listenerSet","error","isTouched","isDirty","fieldListener","resultToErrorMessage","errorMessages","runFieldValidators","pathKey","signal","validators","fieldValue","validator","errorMessage","runFormValidator","getValues","getValue","setValue","options","previousValue","setValues","nextValues","getErrors","getError","setError","message","resetErrors","markTouched","validateField","_","rest","validateAll","nextErrors","fieldsToValidate","formErrors","submit","onSubmit","subscribe","subscribeField","bind","valueExtractor","event","markTouchedOnBlur","setter","newValue","reset","setErrors","next"],"mappings":"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"}
|
package/dist/formit.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
class T extends Error {
|
|
2
|
+
errors;
|
|
3
|
+
type = "validation";
|
|
4
|
+
constructor(f) {
|
|
5
|
+
super("Form validation failed"), this.name = "ValidationError", this.errors = f, typeof Error.captureStackTrace == "function" && Error.captureStackTrace(this, T);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function O(c) {
|
|
9
|
+
if (Array.isArray(c)) return c;
|
|
10
|
+
const f = String(c).trim();
|
|
11
|
+
if (!f) return [];
|
|
12
|
+
const y = [], s = /([^.[\]]+)|\[(\d+)]/g;
|
|
13
|
+
for (let n; n = s.exec(f); )
|
|
14
|
+
n[1] !== void 0 ? y.push(n[1]) : n[2] !== void 0 && y.push(Number(n[2]));
|
|
15
|
+
return y;
|
|
16
|
+
}
|
|
17
|
+
function v(c) {
|
|
18
|
+
return O(c).map(String).join(".");
|
|
19
|
+
}
|
|
20
|
+
function k(c, f, y) {
|
|
21
|
+
const s = O(f);
|
|
22
|
+
let n = c;
|
|
23
|
+
for (const l of s) {
|
|
24
|
+
if (n == null) return y;
|
|
25
|
+
n = n[l];
|
|
26
|
+
}
|
|
27
|
+
return n === void 0 ? y : n;
|
|
28
|
+
}
|
|
29
|
+
function L(c, f, y) {
|
|
30
|
+
const s = O(f);
|
|
31
|
+
if (s.length === 0) return y;
|
|
32
|
+
const l = typeof s[0] == "number", m = Array.isArray(c) ? [...c] : l ? [] : { ...c ?? {} };
|
|
33
|
+
let g = m;
|
|
34
|
+
for (let u = 0; u < s.length; u++) {
|
|
35
|
+
const b = s[u];
|
|
36
|
+
if (u === s.length - 1)
|
|
37
|
+
g[b] = y;
|
|
38
|
+
else {
|
|
39
|
+
const E = s[u + 1], S = g[b], w = typeof E == "number";
|
|
40
|
+
let a;
|
|
41
|
+
Array.isArray(S) ? a = [...S] : S && typeof S == "object" ? a = { ...S } : a = w ? [] : {}, g[b] = a, g = a;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
function tt(c = {}) {
|
|
47
|
+
const f = c.fields ?? {}, y = c.validate;
|
|
48
|
+
let s = S(c.initialValues ?? {}, f), n = {};
|
|
49
|
+
const l = {}, m = {};
|
|
50
|
+
let g = !1, u = !1, b = 0;
|
|
51
|
+
const A = /* @__PURE__ */ new Set(), E = /* @__PURE__ */ new Map();
|
|
52
|
+
function S(t, r) {
|
|
53
|
+
let e = { ...t };
|
|
54
|
+
for (const o of Object.keys(r)) {
|
|
55
|
+
const i = r[o];
|
|
56
|
+
i?.initialValue !== void 0 && k(e, o) === void 0 && (e = L(e, o, i.initialValue));
|
|
57
|
+
}
|
|
58
|
+
return e;
|
|
59
|
+
}
|
|
60
|
+
let w = !1;
|
|
61
|
+
function a() {
|
|
62
|
+
w || (w = !0, Promise.resolve().then(() => {
|
|
63
|
+
w = !1, C();
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
function C() {
|
|
67
|
+
const t = {
|
|
68
|
+
dirty: { ...m },
|
|
69
|
+
errors: n,
|
|
70
|
+
isSubmitting: u,
|
|
71
|
+
isValidating: g,
|
|
72
|
+
submitCount: b,
|
|
73
|
+
touched: { ...l },
|
|
74
|
+
values: s
|
|
75
|
+
};
|
|
76
|
+
for (const r of A)
|
|
77
|
+
try {
|
|
78
|
+
r(t);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
for (const [r, e] of E.entries()) {
|
|
82
|
+
const o = k(s, r), i = n[r], h = l[r] || !1, p = m[r] || !1;
|
|
83
|
+
for (const V of e)
|
|
84
|
+
try {
|
|
85
|
+
V({ dirty: p, error: i, touched: h, value: o });
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function M(t) {
|
|
91
|
+
if (t) {
|
|
92
|
+
if (typeof t == "string") return t;
|
|
93
|
+
if (typeof t == "object") {
|
|
94
|
+
const r = Object.values(t).filter(Boolean);
|
|
95
|
+
return r.length > 0 ? r.join("; ") : void 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function x(t, r) {
|
|
100
|
+
const e = f[t];
|
|
101
|
+
if (!e?.validators) return;
|
|
102
|
+
const o = Array.isArray(e.validators) ? e.validators : [e.validators], i = k(s, t);
|
|
103
|
+
for (const h of o) {
|
|
104
|
+
if (r?.aborted)
|
|
105
|
+
throw new Error("Validation aborted");
|
|
106
|
+
const p = await h(i, s), V = M(p);
|
|
107
|
+
if (V) return V;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function P(t) {
|
|
111
|
+
if (!y) return {};
|
|
112
|
+
if (t?.aborted)
|
|
113
|
+
throw new Error("Validation aborted");
|
|
114
|
+
return await y(s) ?? {};
|
|
115
|
+
}
|
|
116
|
+
function z() {
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
function N(t) {
|
|
120
|
+
return k(s, t);
|
|
121
|
+
}
|
|
122
|
+
function B(t, r, e = {}) {
|
|
123
|
+
const o = v(t), i = k(s, t);
|
|
124
|
+
return s = L(s, t, r), (e.markDirty ?? !0) && i !== r && (m[o] = !0), e.markTouched && (l[o] = !0), a(), r;
|
|
125
|
+
}
|
|
126
|
+
function q(t, r = {}) {
|
|
127
|
+
if (r.replace ? s = { ...t } : s = { ...s, ...t }, r.markAllDirty)
|
|
128
|
+
for (const e of Object.keys(t))
|
|
129
|
+
m[e] = !0;
|
|
130
|
+
a();
|
|
131
|
+
}
|
|
132
|
+
function G() {
|
|
133
|
+
return n;
|
|
134
|
+
}
|
|
135
|
+
function H(t) {
|
|
136
|
+
return n[v(t)];
|
|
137
|
+
}
|
|
138
|
+
function I(t, r) {
|
|
139
|
+
const e = v(t);
|
|
140
|
+
if (r) {
|
|
141
|
+
n = { ...n, [e]: r }, a();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!(e in n)) return;
|
|
145
|
+
const o = { ...n };
|
|
146
|
+
delete o[e], n = o, a();
|
|
147
|
+
}
|
|
148
|
+
function J() {
|
|
149
|
+
n = {}, a();
|
|
150
|
+
}
|
|
151
|
+
function F(t) {
|
|
152
|
+
l[v(t)] = !0, a();
|
|
153
|
+
}
|
|
154
|
+
async function K(t, r) {
|
|
155
|
+
const e = v(t);
|
|
156
|
+
g = !0, a();
|
|
157
|
+
try {
|
|
158
|
+
const o = await x(e, r);
|
|
159
|
+
if (o)
|
|
160
|
+
n = { ...n, [e]: o };
|
|
161
|
+
else {
|
|
162
|
+
const { [e]: i, ...h } = n;
|
|
163
|
+
n = h;
|
|
164
|
+
}
|
|
165
|
+
return o;
|
|
166
|
+
} finally {
|
|
167
|
+
g = !1, a();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function D(t) {
|
|
171
|
+
g = !0, a();
|
|
172
|
+
const r = t?.signal;
|
|
173
|
+
try {
|
|
174
|
+
const e = {};
|
|
175
|
+
let o = /* @__PURE__ */ new Set([...Object.keys(f), ...Object.keys(s)]);
|
|
176
|
+
t?.onlyTouched && (o = new Set(Array.from(o).filter((i) => l[i]))), t?.fields && t.fields.length > 0 && (o = new Set(t.fields));
|
|
177
|
+
for (const i of o) {
|
|
178
|
+
if (r?.aborted)
|
|
179
|
+
throw new Error("Validation aborted");
|
|
180
|
+
const h = await x(i, r);
|
|
181
|
+
h && (e[i] = h);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const i = await P(r);
|
|
185
|
+
Object.assign(e, i);
|
|
186
|
+
} catch (i) {
|
|
187
|
+
e[""] = i instanceof Error ? i.message : String(i);
|
|
188
|
+
}
|
|
189
|
+
return n = e, n;
|
|
190
|
+
} finally {
|
|
191
|
+
g = !1, a();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function Q(t, r) {
|
|
195
|
+
if (u)
|
|
196
|
+
return Promise.reject(new Error("Form is already being submitted"));
|
|
197
|
+
b += 1, u = !0, a();
|
|
198
|
+
const e = r?.signal;
|
|
199
|
+
try {
|
|
200
|
+
if ((r?.validate ?? !0) && await D({ signal: e }), Object.keys(n).length > 0)
|
|
201
|
+
return u = !1, a(), Promise.reject(new T(n));
|
|
202
|
+
const i = await t(s);
|
|
203
|
+
return u = !1, a(), i;
|
|
204
|
+
} catch (o) {
|
|
205
|
+
throw u = !1, a(), o;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function R(t) {
|
|
209
|
+
A.add(t);
|
|
210
|
+
try {
|
|
211
|
+
t({
|
|
212
|
+
dirty: { ...m },
|
|
213
|
+
errors: n,
|
|
214
|
+
isSubmitting: u,
|
|
215
|
+
isValidating: g,
|
|
216
|
+
submitCount: b,
|
|
217
|
+
touched: { ...l },
|
|
218
|
+
values: s
|
|
219
|
+
});
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
return () => A.delete(t);
|
|
223
|
+
}
|
|
224
|
+
function U(t, r) {
|
|
225
|
+
const e = v(t);
|
|
226
|
+
let o = E.get(e);
|
|
227
|
+
o || (o = /* @__PURE__ */ new Set(), E.set(e, o)), o.add(r);
|
|
228
|
+
try {
|
|
229
|
+
r({
|
|
230
|
+
dirty: m[e] || !1,
|
|
231
|
+
error: n[e],
|
|
232
|
+
touched: l[e] || !1,
|
|
233
|
+
value: k(s, e)
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
return () => {
|
|
238
|
+
o.delete(r), o.size === 0 && E.delete(e);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function W(t, r) {
|
|
242
|
+
const e = v(t), o = r?.valueExtractor ?? ((d) => d && typeof d == "object" && "target" in d ? d.target.value : d), i = r?.markTouchedOnBlur ?? !0, h = (d) => {
|
|
243
|
+
const j = k(s, t), $ = typeof d == "function" ? d(j) : d;
|
|
244
|
+
B(t, $, { markDirty: !0, markTouched: !0 });
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
name: e,
|
|
248
|
+
onBlur: () => {
|
|
249
|
+
i && F(t);
|
|
250
|
+
},
|
|
251
|
+
onChange: (d) => {
|
|
252
|
+
const j = o(d);
|
|
253
|
+
h(j);
|
|
254
|
+
},
|
|
255
|
+
set: h,
|
|
256
|
+
get value() {
|
|
257
|
+
return k(s, e);
|
|
258
|
+
},
|
|
259
|
+
set value(d) {
|
|
260
|
+
h(d);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function X(t) {
|
|
265
|
+
return m[v(t)] || !1;
|
|
266
|
+
}
|
|
267
|
+
function Y(t) {
|
|
268
|
+
return l[v(t)] || !1;
|
|
269
|
+
}
|
|
270
|
+
function Z(t) {
|
|
271
|
+
s = S(t ?? c.initialValues ?? {}, f), n = {};
|
|
272
|
+
}
|
|
273
|
+
function _(t) {
|
|
274
|
+
n = { ...t }, a();
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
bind: W,
|
|
278
|
+
getError: H,
|
|
279
|
+
getErrors: G,
|
|
280
|
+
getStateSnapshot: () => ({
|
|
281
|
+
dirty: { ...m },
|
|
282
|
+
errors: n,
|
|
283
|
+
isSubmitting: u,
|
|
284
|
+
isValidating: g,
|
|
285
|
+
submitCount: b,
|
|
286
|
+
touched: { ...l },
|
|
287
|
+
values: s
|
|
288
|
+
}),
|
|
289
|
+
getValue: N,
|
|
290
|
+
getValues: z,
|
|
291
|
+
isDirty: X,
|
|
292
|
+
isTouched: Y,
|
|
293
|
+
markTouched: F,
|
|
294
|
+
reset: Z,
|
|
295
|
+
resetErrors: J,
|
|
296
|
+
setError: I,
|
|
297
|
+
setErrors: _,
|
|
298
|
+
setValue: B,
|
|
299
|
+
setValues: q,
|
|
300
|
+
submit: Q,
|
|
301
|
+
subscribe: R,
|
|
302
|
+
subscribeField: U,
|
|
303
|
+
validateAll: D,
|
|
304
|
+
validateField: K
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
export {
|
|
308
|
+
T as ValidationError,
|
|
309
|
+
tt as createForm
|
|
310
|
+
};
|
|
311
|
+
//# sourceMappingURL=formit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formit.js","sources":["../src/formit.ts"],"sourcesContent":["/** biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: - */\n/** biome-ignore-all lint/suspicious/noExplicitAny: - */\n\n// formit - minimal, typed, no array helpers\n\n/** -------------------- Types -------------------- **/\n\ntype MaybePromise<T> = T | Promise<T>;\nexport type Path = string | Array<string | number>;\n\n/**\n * Error thrown when form validation fails during submission\n */\nexport class ValidationError extends Error {\n public readonly errors: Errors;\n public readonly type = 'validation' as const;\n\n constructor(errors: Errors) {\n super('Form validation failed');\n this.name = 'ValidationError';\n this.errors = errors;\n\n // Maintain a proper stack trace for where the error was thrown (V8 only)\n if (typeof (Error as any).captureStackTrace === 'function') {\n (Error as any).captureStackTrace(this, ValidationError);\n }\n }\n}\n\n/** -------------------- Path Utilities -------------------- **/\n\n/**\n * Converts a path to an array of keys and indices.\n *\n * @param path - The path to convert (string or array)\n * @returns Array of path segments (strings and numbers)\n *\n * @example\n * ```ts\n * toPathArray('user.name') // ['user', 'name']\n * toPathArray('items[0]') // ['items', 0]\n * toPathArray(['users', 0, 'name']) // ['users', 0, 'name']\n * ```\n */\nfunction toPathArray(path: Path): Array<string | number> {\n if (Array.isArray(path)) return path;\n\n const pathString = String(path).trim();\n if (!pathString) return [];\n\n const segments: Array<string | number> = [];\n const regex = /([^.[\\]]+)|\\[(\\d+)]/g;\n\n // biome-ignore lint/suspicious/noAssignInExpressions: Standard regex iteration pattern\n for (let match: RegExpExecArray | null; (match = regex.exec(pathString)); ) {\n if (match[1] !== undefined) segments.push(match[1]);\n else if (match[2] !== undefined) segments.push(Number(match[2]));\n }\n\n return segments;\n}\n\n/**\n * Converts a path to a dot-notation string key.\n *\n * @param path - The path to convert\n * @returns Dot-notation string representation\n *\n * @example\n * ```ts\n * toKey('user.name') // 'user.name'\n * toKey(['user', 'name']) // 'user.name'\n * toKey(['items', 0, 'title']) // 'items.0.title'\n * ```\n */\nfunction toKey(path: Path): string {\n return toPathArray(path).map(String).join('.');\n}\n\n/**\n * Gets a value from an object using a path.\n *\n * @param obj - The object to read from\n * @param path - The path to the value\n * @param fallback - Optional fallback value if path is not found\n * @returns The value at the path, or fallback if not found\n *\n * @example\n * ```ts\n * getAt({ user: { name: 'Alice' } }, 'user.name') // 'Alice'\n * getAt({ items: [{ id: 1 }] }, 'items[0].id') // 1\n * getAt({}, 'missing', 'default') // 'default'\n * ```\n */\nfunction getAt(obj: any, path: Path, fallback?: any): any {\n const pathSegments = toPathArray(path);\n let current = obj;\n\n for (const segment of pathSegments) {\n if (current == null) return fallback;\n current = current[segment as any];\n }\n\n return current === undefined ? fallback : current;\n}\n\n/**\n * Sets a value in an object using a path (immutably).\n *\n * @param obj - The object to update\n * @param path - The path where to set the value\n * @param value - The value to set\n * @returns A new object with the value set at the path\n *\n * @example\n * ```ts\n * setAt({}, 'user.name', 'Alice')\n * // { user: { name: 'Alice' } }\n *\n * setAt({}, 'items[0].title', 'First')\n * // { items: [{ title: 'First' }] }\n *\n * setAt({ count: 1 }, 'count', 2)\n * // { count: 2 }\n * ```\n */\nfunction setAt(obj: any, path: Path, value: any): any {\n const pathSegments = toPathArray(path);\n\n if (pathSegments.length === 0) return value;\n\n // Create a shallow copy of root - detect if you should be arrayed\n const firstSegment = pathSegments[0];\n const rootShouldBeArray = typeof firstSegment === 'number';\n const root = Array.isArray(obj) ? [...obj] : rootShouldBeArray ? [] : { ...(obj ?? {}) };\n\n let current: any = root;\n\n for (let i = 0; i < pathSegments.length; i++) {\n const segment = pathSegments[i];\n const isLastSegment = i === pathSegments.length - 1;\n\n if (isLastSegment) {\n current[segment as any] = value;\n } else {\n const nextSegment = pathSegments[i + 1];\n const nextValue = current[segment as any];\n\n // Determine if the next level should be an array (numeric key) or object\n const shouldBeArray = typeof nextSegment === 'number';\n\n let copy: any;\n if (Array.isArray(nextValue)) {\n copy = [...nextValue];\n } else if (nextValue && typeof nextValue === 'object') {\n copy = { ...nextValue };\n } else {\n // Create a new container-array if the next segment is numeric, object otherwise\n copy = shouldBeArray ? [] : {};\n }\n\n current[segment as any] = copy;\n current = copy;\n }\n }\n\n return root;\n}\n\n/** -------------------- Form Types -------------------- **/\n\nexport type Errors = Partial<Record<string, string>>;\n\nexport type FieldValidator<TValue, TForm> =\n | ((value: TValue, values: TForm) => MaybePromise<string | undefined | null>)\n | ((value: TValue, values: TForm) => MaybePromise<Record<string, string> | undefined | null>);\n\nexport type FormValidator<TForm> = (values: TForm) => MaybePromise<Errors | undefined | null>;\n\nexport type FieldConfig<TValue, TForm> = {\n initialValue?: TValue;\n validators?: FieldValidator<TValue, TForm> | Array<FieldValidator<TValue, TForm>>;\n};\n\nexport type FormInit<TForm extends Record<string, any> = Record<string, any>> = {\n initialValues?: TForm;\n fields?: Partial<{ [K in keyof TForm & string]: FieldConfig<TForm[K], TForm> }>;\n validate?: FormValidator<TForm>;\n};\n\nexport type FormState<TForm> = {\n values: TForm;\n errors: Errors;\n touched: Record<string, boolean>;\n dirty: Record<string, boolean>;\n isValidating: boolean;\n isSubmitting: boolean;\n submitCount: number;\n};\n\ntype Listener<TForm> = (state: FormState<TForm>) => void;\ntype FieldListener<TValue> = (payload: { value: TValue; error?: string; touched: boolean; dirty: boolean }) => void;\n\nexport type BindConfig = {\n /**\n * Custom value extractor from event\n * @default (event) => event?.target?.value ?? event\n */\n valueExtractor?: (event: any) => any;\n /**\n * Whether to mark field as touched on blur\n * @default true\n */\n markTouchedOnBlur?: boolean;\n};\n\n/** -------------------- Form Creation -------------------- **/\n\nexport function createForm<TForm extends Record<string, any> = Record<string, any>>(init: FormInit<TForm> = {}) {\n const fieldConfigs = (init.fields ?? {}) as Partial<Record<string, FieldConfig<any, TForm>>>;\n const formValidator = init.validate;\n\n // Initialize values with initial values and field configs\n let values = initializeValues(init.initialValues ?? ({} as TForm), fieldConfigs);\n let errors: Errors = {};\n const touched: Record<string, boolean> = {};\n const dirty: Record<string, boolean> = {};\n let isValidating = false;\n let isSubmitting = false;\n let submitCount = 0;\n\n const listeners = new Set<Listener<TForm>>();\n const fieldListeners = new Map<string, Set<FieldListener<any>>>();\n\n /** -------------------- Internal Helpers -------------------- **/\n\n /**\n * Initialize form values from initial values and field configs.\n *\n * @param initialValues - The initial form values\n * @param configs - Field configurations with initialValue properties\n * @returns Merged form values with field config initial values applied\n */\n function initializeValues(initialValues: TForm, configs: Partial<Record<string, FieldConfig<any, TForm>>>): TForm {\n let result = { ...initialValues };\n\n for (const key of Object.keys(configs)) {\n const config = configs[key];\n if (config?.initialValue !== undefined && getAt(result, key) === undefined) {\n result = setAt(result, key, config.initialValue) as TForm;\n }\n }\n\n return result;\n }\n\n /**\n * Schedule notification to all listeners (debounced to next tick).\n */\n let scheduled = false;\n function scheduleNotify() {\n if (scheduled) return;\n scheduled = true;\n\n Promise.resolve().then(() => {\n scheduled = false;\n notifyListeners();\n });\n }\n\n /**\n * Notify all form and field listeners of state changes.\n */\n function notifyListeners() {\n const snapshot: FormState<TForm> = {\n dirty: { ...dirty },\n errors,\n isSubmitting,\n isValidating,\n submitCount,\n touched: { ...touched },\n values,\n };\n\n // Notify form listeners\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch {\n // Swallow listener errors to prevent breaking other listeners\n }\n }\n\n // Notify field listeners\n for (const [path, listenerSet] of fieldListeners.entries()) {\n const value = getAt(values, path);\n const error = errors[path];\n const isTouched = touched[path] || false;\n const isDirty = dirty[path] || false;\n\n for (const fieldListener of listenerSet) {\n try {\n fieldListener({ dirty: isDirty, error, touched: isTouched, value });\n } catch {\n // Swallow listener errors\n }\n }\n }\n }\n\n /**\n * Convert validator result to error message string.\n */\n function resultToErrorMessage(result: any): string | undefined {\n if (!result) return undefined;\n if (typeof result === 'string') return result;\n\n // Object with error messages - join all non-empty values\n if (typeof result === 'object') {\n const errorMessages = Object.values(result).filter(Boolean) as string[];\n return errorMessages.length > 0 ? errorMessages.join('; ') : undefined;\n }\n\n return undefined;\n }\n\n /**\n * Run all validators for a specific field.\n *\n * @param pathKey - The field path key to validate\n * @param signal - Optional AbortSignal for cancellation\n * @returns Error message if validation failed, undefined otherwise\n *\n * @throws {DOMException} When validation is aborted via signal\n */\n async function runFieldValidators(pathKey: string, signal?: AbortSignal): Promise<string | undefined> {\n const config = fieldConfigs[pathKey];\n if (!config?.validators) return undefined;\n\n const validators = Array.isArray(config.validators) ? config.validators : [config.validators];\n const fieldValue = getAt(values, pathKey);\n\n for (const validator of validators) {\n if (signal?.aborted) {\n throw new Error('Validation aborted');\n }\n\n const result = await validator(fieldValue, values);\n const errorMessage = resultToErrorMessage(result);\n if (errorMessage) return errorMessage;\n }\n\n return undefined;\n }\n\n /**\n * Run the form-level validator.\n *\n * @param signal - Optional AbortSignal for cancellation\n * @returns Object with field errors, or empty object if no errors\n *\n * @throws {DOMException} When validation is aborted via signal\n */\n async function runFormValidator(signal?: AbortSignal): Promise<Errors> {\n if (!formValidator) return {};\n\n if (signal?.aborted) {\n throw new Error('Validation aborted');\n }\n\n const result = await formValidator(values);\n return (result ?? {}) as Errors;\n }\n\n /** -------------------- Public API - Value Management -------------------- **/\n\n /**\n * Get all form values.\n */\n function getValues(): TForm {\n return values;\n }\n\n /**\n * Get a specific field value by path.\n *\n * @param path - The field path (e.g., 'user.name' or ['items', 0, 'title'])\n * @returns The value at the specified path\n */\n function getValue(path: Path) {\n return getAt(values, path);\n }\n\n /**\n * Set a specific field value by a path.\n *\n * @param path - The field path (e.g., 'user.name' or ['items', 0, 'title'])\n * @param value - The value to set\n * @param options - Optional configuration\n * @param options.markDirty - Whether to mark the field as dirty (default: true)\n * @param options.markTouched - Whether to mark the field as touched (default: false)\n * @returns The value that was set\n */\n function setValue(path: Path, value: any, options: { markDirty?: boolean; markTouched?: boolean } = {}) {\n const key = toKey(path);\n const previousValue = getAt(values, path);\n\n values = setAt(values, path, value) as TForm;\n\n if (options.markDirty ?? true) {\n // Reference equality check - objects/arrays with same content but different refs will be marked dirty\n if (previousValue !== value) {\n dirty[key] = true;\n }\n }\n\n if (options.markTouched) {\n touched[key] = true;\n }\n\n scheduleNotify();\n return value;\n }\n\n /**\n * Set multiple form values at once.\n *\n * @param nextValues - Partial form values to merge or replace\n * @param options - Optional configuration\n * @param options.replace - If true, replaces all values; if false, merges with existing (default: false)\n * @param options.markAllDirty - If true, marks all changed fields as dirty (default: false)\n */\n function setValues(nextValues: Partial<TForm>, options: { replace?: boolean; markAllDirty?: boolean } = {}) {\n if (options.replace) {\n values = { ...nextValues } as TForm;\n } else {\n values = { ...values, ...nextValues } as TForm;\n }\n\n if (options.markAllDirty) {\n for (const key of Object.keys(nextValues)) {\n dirty[key] = true;\n }\n }\n\n scheduleNotify();\n }\n\n /** -------------------- Public API - Error Management -------------------- **/\n\n /**\n * Get all form errors.\n *\n * @returns Object containing all field errors keyed by field path\n */\n function getErrors() {\n return errors;\n }\n\n /**\n * Get a specific field error by path.\n *\n * @param path - The field path\n * @returns Error message for the field, or undefined if no error\n */\n function getError(path: Path) {\n return errors[toKey(path)];\n }\n\n /**\n * Set a specific field error by path.\n *\n * @param path - The field path\n * @param message - Error message to set, or undefined to clear the error\n */\n function setError(path: Path, message?: string) {\n const key = toKey(path);\n\n if (message) {\n errors = { ...errors, [key]: message };\n scheduleNotify();\n return;\n }\n\n // Clear error\n if (!(key in errors)) return;\n\n const copy = { ...errors };\n delete copy[key];\n errors = copy;\n scheduleNotify();\n }\n\n /**\n * Reset all form errors.\n *\n * @remarks\n * Clears all error messages and triggers a state notification.\n */\n function resetErrors() {\n errors = {};\n scheduleNotify();\n }\n\n /** -------------------- Public API - Touch Management -------------------- **/\n\n /**\n * Mark a field as touched.\n *\n * @param path - The field path to mark as touched\n *\n * @remarks\n * Touched fields are typically used to show validation errors only after user interaction.\n */\n function markTouched(path: Path) {\n touched[toKey(path)] = true;\n scheduleNotify();\n }\n\n /** -------------------- Public API - Validation -------------------- **/\n\n /**\n * Validate a single field.\n *\n * @param path - The field path to validate\n * @param signal - Optional AbortSignal for cancellation\n * @returns Error message if validation failed, undefined otherwise\n *\n * @throws {DOMException} When validation is aborted via signal\n */\n async function validateField(path: Path, signal?: AbortSignal) {\n const key = toKey(path);\n isValidating = true;\n scheduleNotify();\n\n try {\n const error = await runFieldValidators(key, signal);\n\n // Update errors object immutably\n if (error) {\n errors = { ...errors, [key]: error };\n } else {\n const { [key]: _, ...rest } = errors;\n errors = rest;\n }\n\n return error;\n } finally {\n isValidating = false;\n scheduleNotify();\n }\n }\n\n /**\n * Validate all fields and form-level validators.\n *\n * @param options - Optional validation configuration\n * @param options.signal - Optional AbortSignal for cancellation\n * @param options.onlyTouched - If true, only validate touched fields\n * @param options.fields - If provided, only validate these specific fields\n * @returns Object containing all field errors\n *\n * @throws {Error} When validation is aborted via signal\n */\n async function validateAll(options?: { signal?: AbortSignal; onlyTouched?: boolean; fields?: string[] }) {\n isValidating = true;\n scheduleNotify();\n\n const signal = options?.signal;\n\n try {\n const nextErrors: Errors = {};\n\n // Collect all field paths to validate\n let fieldsToValidate = new Set<string>([...Object.keys(fieldConfigs), ...Object.keys(values)]);\n\n // Filter by touched fields if requested\n if (options?.onlyTouched) {\n fieldsToValidate = new Set(Array.from(fieldsToValidate).filter((key) => touched[key]));\n }\n\n // Filter by specific fields if requested\n if (options?.fields && options.fields.length > 0) {\n fieldsToValidate = new Set(options.fields);\n }\n\n // Run field validators\n for (const path of fieldsToValidate) {\n if (signal?.aborted) {\n throw new Error('Validation aborted');\n }\n\n const error = await runFieldValidators(path, signal);\n if (error) nextErrors[path] = error;\n }\n\n // Run form-level validator\n try {\n const formErrors = await runFormValidator(signal);\n Object.assign(nextErrors, formErrors);\n } catch (error) {\n nextErrors[''] = error instanceof Error ? error.message : String(error);\n }\n\n errors = nextErrors;\n return errors;\n } finally {\n isValidating = false;\n scheduleNotify();\n }\n }\n\n /** -------------------- Public API - Form Submission -------------------- **/\n\n /**\n * Submit the form with optional validation.\n *\n * @param onSubmit - Callback function to handle form submission with validated values\n * @param options - Optional configuration\n * @param options.signal - Optional AbortSignal for cancellation\n * @param options.validate - Whether to run validation before submission (default: true)\n * @returns Promise resolving to the result of onSubmit callback\n *\n * @throws {ValidationError} When validation fails and form has errors\n * @throws {Error} When form is already submitting\n * @throws {Error} When submission is aborted via signal\n */\n async function submit(\n onSubmit: (values: TForm) => MaybePromise<any>,\n options?: { signal?: AbortSignal; validate?: boolean },\n ) {\n if (isSubmitting) {\n return Promise.reject(new Error('Form is already being submitted'));\n }\n\n submitCount += 1;\n isSubmitting = true;\n scheduleNotify();\n\n const signal = options?.signal;\n\n try {\n // Run validation if requested\n if (options?.validate ?? true) {\n await validateAll({ signal });\n }\n\n // Check for validation errors\n const hasErrors = Object.keys(errors).length > 0;\n if (hasErrors) {\n isSubmitting = false;\n scheduleNotify();\n return Promise.reject(new ValidationError(errors));\n }\n\n // Execute submit handler\n const result = await onSubmit(values);\n\n isSubmitting = false;\n scheduleNotify();\n\n return result;\n } catch (error) {\n isSubmitting = false;\n scheduleNotify();\n throw error;\n }\n }\n\n /** -------------------- Public API - Subscriptions -------------------- **/\n\n /**\n * Subscribe to form state changes.\n *\n * @param listener - Callback function that receives form state snapshots\n * @returns Unsubscribe function to stop listening to state changes\n *\n * @remarks\n * The listener is immediately called with the current state upon subscription.\n * Listener errors are swallowed to prevent breaking other listeners.\n */\n function subscribe(listener: Listener<TForm>) {\n listeners.add(listener);\n\n // Immediately notify the new listener with current state\n try {\n listener({\n dirty: { ...dirty },\n errors,\n isSubmitting,\n isValidating,\n submitCount,\n touched: { ...touched },\n values,\n });\n } catch {\n // Swallow listener errors\n }\n\n return () => listeners.delete(listener);\n }\n\n /**\n * Subscribe to a specific field's changes.\n *\n * @param path - The field path to subscribe to\n * @param listener - Callback function that receives field state updates\n * @returns Unsubscribe function to stop listening to field changes\n */\n function subscribeField(path: Path, listener: FieldListener<any>) {\n const key = toKey(path);\n let listenerSet = fieldListeners.get(key);\n\n if (!listenerSet) {\n listenerSet = new Set();\n fieldListeners.set(key, listenerSet);\n }\n\n listenerSet.add(listener);\n\n // Immediately notify the new listener with the current state\n try {\n listener({\n dirty: dirty[key] || false,\n error: errors[key],\n touched: touched[key] || false,\n value: getAt(values, key),\n });\n } catch {\n // Swallow listener errors\n }\n\n return () => {\n listenerSet!.delete(listener);\n // Clean up empty listener sets to prevent memory leaks\n if (listenerSet!.size === 0) {\n fieldListeners.delete(key);\n }\n };\n }\n\n /** -------------------- Public API - Field Binding -------------------- **/\n\n /**\n * Create a binding object for a field that can be used with inputs.\n *\n * @param path - The field path to bind\n * @param config - Optional configuration for value extraction and blur behavior\n * @returns Object with value, onChange, onBlur, and setter methods for input binding\n *\n * @example\n * ```tsx\n * const nameBinding = bind('user.name');\n * <input {...nameBinding} />\n *\n * // With custom value extractor\n * const selectBinding = bind('category', {\n * valueExtractor: (e) => e.target.selectedOptions[0].value\n * });\n *\n * // Disable mark touched on blur\n * const fieldBinding = bind('field', { markTouchedOnBlur: false });\n * ```\n */\n function bind(path: Path, config?: BindConfig) {\n const key = toKey(path);\n const valueExtractor =\n config?.valueExtractor ??\n ((event: any) => (event && typeof event === 'object' && 'target' in event ? (event.target as any).value : event));\n const markTouchedOnBlur = config?.markTouchedOnBlur ?? true;\n\n const setter = (newValue: any | ((prev: any) => any)) => {\n const previousValue = getAt(values, path);\n const nextValue = typeof newValue === 'function' ? (newValue as (prev: any) => any)(previousValue) : newValue;\n\n setValue(path, nextValue, { markDirty: true, markTouched: true });\n };\n\n const onChange = (event: any) => {\n const value = valueExtractor(event);\n setter(value);\n };\n\n const onBlur = () => {\n if (markTouchedOnBlur) {\n markTouched(path);\n }\n };\n\n return {\n name: key,\n onBlur,\n onChange,\n set: setter,\n get value() {\n return getAt(values, key);\n },\n set value(newValue: any) {\n setter(newValue);\n },\n };\n }\n\n function isDirty(path: Path): boolean {\n return dirty[toKey(path)] || false;\n }\n\n function isTouched(path: Path): boolean {\n return touched[toKey(path)] || false;\n }\n\n function reset(initialValues?: TForm) {\n values = initializeValues(initialValues ?? init.initialValues ?? ({} as TForm), fieldConfigs);\n errors = {};\n }\n\n function setErrors(next: Errors) {\n errors = { ...next };\n scheduleNotify();\n }\n\n /** -------------------- Return Public API -------------------- **/\n\n return {\n bind,\n getError,\n getErrors,\n getStateSnapshot: (): FormState<TForm> => ({\n dirty: { ...dirty },\n errors,\n isSubmitting,\n isValidating,\n submitCount,\n touched: { ...touched },\n values,\n }),\n getValue,\n getValues,\n isDirty,\n isTouched,\n markTouched,\n reset,\n resetErrors,\n setError,\n setErrors,\n setValue,\n setValues,\n submit,\n subscribe,\n subscribeField,\n validateAll,\n validateField,\n };\n}\n"],"names":["ValidationError","errors","toPathArray","path","pathString","segments","regex","match","toKey","getAt","obj","fallback","pathSegments","current","segment","setAt","value","rootShouldBeArray","root","i","nextSegment","nextValue","shouldBeArray","copy","createForm","init","fieldConfigs","formValidator","values","initializeValues","touched","dirty","isValidating","isSubmitting","submitCount","listeners","fieldListeners","initialValues","configs","result","key","config","scheduled","scheduleNotify","notifyListeners","snapshot","listener","listenerSet","error","isTouched","isDirty","fieldListener","resultToErrorMessage","errorMessages","runFieldValidators","pathKey","signal","validators","fieldValue","validator","errorMessage","runFormValidator","getValues","getValue","setValue","options","previousValue","setValues","nextValues","getErrors","getError","setError","message","resetErrors","markTouched","validateField","_","rest","validateAll","nextErrors","fieldsToValidate","formErrors","submit","onSubmit","subscribe","subscribeField","bind","valueExtractor","event","markTouchedOnBlur","setter","newValue","reset","setErrors","next"],"mappings":"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"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export declare type BindConfig = {
|
|
2
|
+
/**
|
|
3
|
+
* Custom value extractor from event
|
|
4
|
+
* @default (event) => event?.target?.value ?? event
|
|
5
|
+
*/
|
|
6
|
+
valueExtractor?: (event: any) => any;
|
|
7
|
+
/**
|
|
8
|
+
* Whether to mark field as touched on blur
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
markTouchedOnBlur?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** -------------------- Form Creation -------------------- **/
|
|
15
|
+
export declare function createForm<TForm extends Record<string, any> = Record<string, any>>(init?: FormInit<TForm>): {
|
|
16
|
+
bind: (path: Path, config?: BindConfig) => {
|
|
17
|
+
name: string;
|
|
18
|
+
onBlur: () => void;
|
|
19
|
+
onChange: (event: any) => void;
|
|
20
|
+
set: (newValue: any | ((prev: any) => any)) => void;
|
|
21
|
+
value: any;
|
|
22
|
+
};
|
|
23
|
+
getError: (path: Path) => string | undefined;
|
|
24
|
+
getErrors: () => Partial<Record<string, string>>;
|
|
25
|
+
getStateSnapshot: () => FormState<TForm>;
|
|
26
|
+
getValue: (path: Path) => any;
|
|
27
|
+
getValues: () => TForm;
|
|
28
|
+
isDirty: (path: Path) => boolean;
|
|
29
|
+
isTouched: (path: Path) => boolean;
|
|
30
|
+
markTouched: (path: Path) => void;
|
|
31
|
+
reset: (initialValues?: TForm) => void;
|
|
32
|
+
resetErrors: () => void;
|
|
33
|
+
setError: (path: Path, message?: string) => void;
|
|
34
|
+
setErrors: (next: Errors) => void;
|
|
35
|
+
setValue: (path: Path, value: any, options?: {
|
|
36
|
+
markDirty?: boolean;
|
|
37
|
+
markTouched?: boolean;
|
|
38
|
+
}) => any;
|
|
39
|
+
setValues: (nextValues: Partial<TForm>, options?: {
|
|
40
|
+
replace?: boolean;
|
|
41
|
+
markAllDirty?: boolean;
|
|
42
|
+
}) => void;
|
|
43
|
+
submit: (onSubmit: (values: TForm) => MaybePromise<any>, options?: {
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
validate?: boolean;
|
|
46
|
+
}) => Promise<any>;
|
|
47
|
+
subscribe: (listener: Listener<TForm>) => () => boolean;
|
|
48
|
+
subscribeField: (path: Path, listener: FieldListener<any>) => () => void;
|
|
49
|
+
validateAll: (options?: {
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
onlyTouched?: boolean;
|
|
52
|
+
fields?: string[];
|
|
53
|
+
}) => Promise<Partial<Record<string, string>>>;
|
|
54
|
+
validateField: (path: Path, signal?: AbortSignal) => Promise<string | undefined>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** -------------------- Form Types -------------------- **/
|
|
58
|
+
export declare type Errors = Partial<Record<string, string>>;
|
|
59
|
+
|
|
60
|
+
export declare type FieldConfig<TValue, TForm> = {
|
|
61
|
+
initialValue?: TValue;
|
|
62
|
+
validators?: FieldValidator<TValue, TForm> | Array<FieldValidator<TValue, TForm>>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
declare type FieldListener<TValue> = (payload: {
|
|
66
|
+
value: TValue;
|
|
67
|
+
error?: string;
|
|
68
|
+
touched: boolean;
|
|
69
|
+
dirty: boolean;
|
|
70
|
+
}) => void;
|
|
71
|
+
|
|
72
|
+
export declare type FieldValidator<TValue, TForm> = ((value: TValue, values: TForm) => MaybePromise<string | undefined | null>) | ((value: TValue, values: TForm) => MaybePromise<Record<string, string> | undefined | null>);
|
|
73
|
+
|
|
74
|
+
export declare type FormInit<TForm extends Record<string, any> = Record<string, any>> = {
|
|
75
|
+
initialValues?: TForm;
|
|
76
|
+
fields?: Partial<{
|
|
77
|
+
[K in keyof TForm & string]: FieldConfig<TForm[K], TForm>;
|
|
78
|
+
}>;
|
|
79
|
+
validate?: FormValidator<TForm>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export declare type FormState<TForm> = {
|
|
83
|
+
values: TForm;
|
|
84
|
+
errors: Errors;
|
|
85
|
+
touched: Record<string, boolean>;
|
|
86
|
+
dirty: Record<string, boolean>;
|
|
87
|
+
isValidating: boolean;
|
|
88
|
+
isSubmitting: boolean;
|
|
89
|
+
submitCount: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export declare type FormValidator<TForm> = (values: TForm) => MaybePromise<Errors | undefined | null>;
|
|
93
|
+
|
|
94
|
+
declare type Listener<TForm> = (state: FormState<TForm>) => void;
|
|
95
|
+
|
|
96
|
+
/** biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: - */
|
|
97
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: - */
|
|
98
|
+
/** -------------------- Types -------------------- **/
|
|
99
|
+
declare type MaybePromise<T> = T | Promise<T>;
|
|
100
|
+
|
|
101
|
+
export declare type Path = string | Array<string | number>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Error thrown when form validation fails during submission
|
|
105
|
+
*/
|
|
106
|
+
export declare class ValidationError extends Error {
|
|
107
|
+
readonly errors: Errors;
|
|
108
|
+
readonly type: "validation";
|
|
109
|
+
constructor(errors: Errors);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vielzeug/formit",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc && vite build",
|
|
19
|
+
"fix": "biome check --write --unsafe src",
|
|
20
|
+
"lint": "biome check src",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"test": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "~5.9.3",
|
|
31
|
+
"vite": "^7.3.1",
|
|
32
|
+
"vite-plugin-dts": "^4.5.4",
|
|
33
|
+
"vitest": "^4.0.18"
|
|
34
|
+
}
|
|
35
|
+
}
|