@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 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.
@@ -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,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("./formit.cjs");exports.ValidationError=r.ValidationError;exports.createForm=r.createForm;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -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,6 @@
1
+ import { ValidationError as a, createForm as e } from "./formit.js";
2
+ export {
3
+ a as ValidationError,
4
+ e as createForm
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -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
+ }