mobx-formly 0.0.2 → 0.1.0

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/dist/form.js ADDED
@@ -0,0 +1,612 @@
1
+ import { action, computed, makeObservable, observable, runInAction } from 'mobx';
2
+ import { createRef } from 'yummies/mobx';
3
+ import { clone, deleteAtPath, extractValue, getAtPath, isEqual, setAtPath } from './utils.js';
4
+ import { collectChangedPaths, collectDirtyPaths, collectErrorPaths, findErrorAtPath } from './utils.js';
5
+ import { PathStore } from './path-store.js';
6
+ import { FormValidator } from './validation.js';
7
+ const isPromiseLike = (value) => !!value && typeof value.then === 'function';
8
+ export class BaseForm {
9
+ /**
10
+ * Current form values.
11
+ *
12
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#values)
13
+ */
14
+ values;
15
+ /**
16
+ * Cached default values used by reset, resetField, and dirty comparison.
17
+ * Updated by reset unless `keepDefaultValues` is passed.
18
+ *
19
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#defaultvalues)
20
+ */
21
+ defaultValues;
22
+ errorStore = new PathStore();
23
+ fieldStateStore = new PathStore();
24
+ /** Validation errors nested by field path. */
25
+ get errors() {
26
+ return this.errorStore.proxy();
27
+ }
28
+ /**
29
+ * Field paths whose values differ from their defaults.
30
+ *
31
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#dirtyfields)
32
+ */
33
+ dirtyFields = {};
34
+ /**
35
+ * Field paths that have been touched.
36
+ *
37
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#touchedfields)
38
+ */
39
+ touchedFields = {};
40
+ /**
41
+ * Field paths that are currently being validated.
42
+ *
43
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#validatingfields)
44
+ */
45
+ validatingFields = {};
46
+ /**
47
+ * Observable state for each registered field, nested by field path.
48
+ *
49
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#fieldstate)
50
+ */
51
+ get fieldState() {
52
+ return this.fieldStateStore.proxy();
53
+ }
54
+ /**
55
+ * Whether a submission is currently running.
56
+ *
57
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#issubmitting)
58
+ */
59
+ isSubmitting = false;
60
+ /**
61
+ * Whether the form has been submitted at least once.
62
+ *
63
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#issubmitted)
64
+ */
65
+ isSubmitted = false;
66
+ /**
67
+ * Whether the latest submission succeeded.
68
+ *
69
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#issubmitsuccessful)
70
+ */
71
+ isSubmitSuccessful = false;
72
+ /**
73
+ * Number of submission attempts.
74
+ *
75
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#submitcount)
76
+ */
77
+ submitCount = 0;
78
+ /**
79
+ * Refs registered for fields.
80
+ *
81
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#refs)
82
+ */
83
+ refs = new Map();
84
+ options;
85
+ fieldOptions = new Map();
86
+ touchedValidationFields = new Set();
87
+ validator;
88
+ activeSubmissions = 0;
89
+ resetVersion = 0;
90
+ isValidOverride = observable.box(undefined);
91
+ validationVersion = 0;
92
+ fieldValidationVersions = new Map();
93
+ /** Whether at least one validation pass has completed since construction or reset. */
94
+ hasValidationRun = false;
95
+ constructor(options = {}) {
96
+ this.options = {
97
+ ...options,
98
+ mode: options.mode ?? 'onSubmit',
99
+ reValidateMode: options.reValidateMode ?? 'onChange',
100
+ disabled: options.disabled ?? false,
101
+ shouldFocusError: options.shouldFocusError ?? true,
102
+ };
103
+ this.defaultValues = clone((options.defaultValues ?? {}));
104
+ this.values = clone((options.values ?? options.defaultValues ?? {}));
105
+ this.validator = new FormValidator({
106
+ options: this.options,
107
+ fieldOptions: this.fieldOptions,
108
+ refs: this.refs,
109
+ valueAt: (path) => getAtPath(this.values, path),
110
+ snapshot: () => this.snapshot,
111
+ applyError: (path, error) => this.applyError(path, error),
112
+ });
113
+ makeObservable(this, {
114
+ values: observable.deep,
115
+ defaultValues: observable.deep,
116
+ dirtyFields: observable.shallow,
117
+ touchedFields: observable.shallow,
118
+ validatingFields: observable.shallow,
119
+ isSubmitting: observable,
120
+ isSubmitted: observable,
121
+ isSubmitSuccessful: observable,
122
+ submitCount: observable,
123
+ disabled: computed,
124
+ isDirty: computed,
125
+ isTouched: computed,
126
+ isValidating: computed,
127
+ isValid: computed,
128
+ snapshot: computed,
129
+ register: action,
130
+ unregister: action,
131
+ setValue: action,
132
+ mutate: action,
133
+ setError: action,
134
+ clearErrors: action,
135
+ trigger: action,
136
+ handleSubmit: action,
137
+ reset: action,
138
+ resetField: action,
139
+ setFocus: action,
140
+ });
141
+ }
142
+ /**
143
+ * Whether registered event handlers ignore changes and blur events.
144
+ *
145
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#disabled)
146
+ */
147
+ get disabled() { return this.options.disabled; }
148
+ /**
149
+ * Whether any field is dirty.
150
+ *
151
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#isdirty)
152
+ */
153
+ get isDirty() { return Object.keys(this.dirtyFields).length > 0; }
154
+ /**
155
+ * Whether any field has been touched.
156
+ *
157
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#istouched)
158
+ */
159
+ get isTouched() { return Object.keys(this.touchedFields).length > 0; }
160
+ /** Whether any field validation is currently running. */
161
+ get isValidating() { return Object.keys(this.validatingFields).length > 0; }
162
+ /**
163
+ * Whether the form has no errors. With a schema or resolver the first read
164
+ * schedules a full validation pass, so validity reflects the schema instead
165
+ * of defaulting to true until something validates.
166
+ *
167
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#isvalid)
168
+ */
169
+ get isValid() {
170
+ const override = this.isValidOverride.get();
171
+ if (override !== undefined)
172
+ return override;
173
+ if (!this.hasValidationRun && (this.options.schema || this.options.resolver)) {
174
+ this.hasValidationRun = true;
175
+ // Deferred so this computed getter stays free of observable side effects.
176
+ queueMicrotask(() => { void this.runValidation(); });
177
+ }
178
+ return this.errorStore.size === 0;
179
+ }
180
+ /**
181
+ * Registers a field and returns its ref and event handlers.
182
+ *
183
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#registername-options)
184
+ */
185
+ register(name, options = {}) {
186
+ const path = name;
187
+ this.fieldOptions.set(path, options);
188
+ const ref = this.ref(name);
189
+ this.ensureFieldState(path);
190
+ return {
191
+ name: path,
192
+ ref,
193
+ onChange: async (eventOrValue) => {
194
+ if (this.disabled)
195
+ return;
196
+ const value = this.transformValue(extractValue(eventOrValue), options);
197
+ const shouldValidate = this.shouldValidateOnChange(path);
198
+ const deps = this.dependentFields(options.deps);
199
+ this.setValue(path, value, { shouldDirty: true, shouldValidate: false });
200
+ if (shouldValidate && deps.length) {
201
+ await Promise.all([this.trigger(path), this.trigger(deps)]);
202
+ }
203
+ else if (shouldValidate) {
204
+ await this.trigger(path);
205
+ }
206
+ else if (deps.length) {
207
+ await this.trigger(deps);
208
+ }
209
+ },
210
+ onBlur: async () => {
211
+ if (this.disabled)
212
+ return;
213
+ this.markTouched(path);
214
+ if (this.options.mode === 'onTouched')
215
+ this.touchedValidationFields.add(path);
216
+ if (this.options.mode === 'onBlur' || this.options.mode === 'onTouched' || this.options.mode === 'all' || (this.hasError(path) && this.options.reValidateMode === 'onBlur')) {
217
+ this.validator.flushDelayed(path);
218
+ await this.trigger(path);
219
+ }
220
+ },
221
+ };
222
+ }
223
+ /**
224
+ * Returns the stable MobX-aware ref for a field path, creating it on demand.
225
+ * The ref can be used by a view adapter before the field is registered.
226
+ */
227
+ ref(name) {
228
+ const path = name;
229
+ let ref = this.refs.get(path);
230
+ if (!ref) {
231
+ ref = createRef();
232
+ this.refs.set(path, ref);
233
+ }
234
+ return ref;
235
+ }
236
+ /**
237
+ * Removes a field, its value, and its associated state.
238
+ *
239
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#unregistername)
240
+ */
241
+ unregister(name) {
242
+ const path = name;
243
+ this.fieldValidationVersions.set(path, (this.fieldValidationVersions.get(path) ?? 0) + 1);
244
+ this.validator.cancelDelayed(path);
245
+ this.touchedValidationFields.delete(path);
246
+ deleteAtPath(this.values, path);
247
+ this.applyError(path, undefined);
248
+ delete this.dirtyFields[path];
249
+ delete this.touchedFields[path];
250
+ delete this.validatingFields[path];
251
+ this.fieldStateStore.delete(path);
252
+ this.fieldOptions.delete(path);
253
+ this.refs.delete(path);
254
+ }
255
+ /**
256
+ * Updates a field value and optionally changes its state or validates it.
257
+ *
258
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#setvaluename-value-config)
259
+ */
260
+ setValue(name, value, config = {}) {
261
+ const path = name;
262
+ setAtPath(this.values, path, value);
263
+ this.applyValueChange(path, config);
264
+ }
265
+ mutate(mutator, config = {}) {
266
+ const before = clone(this.values);
267
+ const result = mutator();
268
+ if (isPromiseLike(result)) {
269
+ return result.then(() => runInAction(() => this.finishMutation(before, config)), (error) => {
270
+ runInAction(() => this.finishMutation(before, config));
271
+ throw error;
272
+ });
273
+ }
274
+ this.finishMutation(before, config);
275
+ }
276
+ finishMutation(before, config) {
277
+ if (isEqual(before, this.values))
278
+ return;
279
+ const changedPaths = collectChangedPaths(before, this.values);
280
+ const dirtyPaths = collectDirtyPaths(this.values, this.defaultValues);
281
+ if (config.shouldDirty ?? true)
282
+ this.syncDirtyFields(dirtyPaths);
283
+ if (config.shouldTouch ?? true)
284
+ for (const path of changedPaths)
285
+ this.markTouched(path);
286
+ if (config.shouldValidate ?? true)
287
+ void this.trigger();
288
+ }
289
+ applyValueChange(path, config) {
290
+ if (config.shouldDirty ?? true)
291
+ this.updateDirty(path);
292
+ if (config.shouldTouch ?? true)
293
+ this.markTouched(path);
294
+ if (config.shouldValidate)
295
+ void this.trigger(path);
296
+ }
297
+ /**
298
+ * Sets an error for a field and can focus it.
299
+ *
300
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#seterrorname-error)
301
+ */
302
+ setError(name, error, config = {}) {
303
+ const path = name;
304
+ this.validator.cancelDelayed(path);
305
+ this.applyError(path, error);
306
+ if (config.shouldFocus)
307
+ this.refs.get(path)?.current?.focus();
308
+ }
309
+ /**
310
+ * Clears one, several, or all field errors.
311
+ *
312
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#clearerrorsname)
313
+ */
314
+ clearErrors(name) {
315
+ if (!name) {
316
+ this.validator.cancelAllDelayed();
317
+ this.isValidOverride.set(undefined);
318
+ this.errorStore.clear();
319
+ for (const [, state] of this.fieldStateStore.entries())
320
+ this.applyFieldState(state, undefined);
321
+ return;
322
+ }
323
+ for (const path of Array.isArray(name) ? name : [name]) {
324
+ this.clearErrorPath(path);
325
+ }
326
+ }
327
+ /**
328
+ * Validates one field, several fields, or the complete form.
329
+ *
330
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#triggername)
331
+ */
332
+ async trigger(name, config = {}) {
333
+ const paths = name ? (Array.isArray(name) ? name : [name]).map(String) : [...this.fieldOptions.keys()];
334
+ if (config.shouldTouch)
335
+ for (const path of paths)
336
+ this.markTouched(path);
337
+ const valid = (await this.runValidation(name)).valid;
338
+ if (!valid && config.shouldFocus)
339
+ this.focusFirstError(paths);
340
+ return valid;
341
+ }
342
+ async runValidation(name) {
343
+ this.hasValidationRun = true;
344
+ const paths = name ? (Array.isArray(name) ? name : [name]).map(String) : undefined;
345
+ const run = ++this.validationVersion;
346
+ const fieldVersions = new Map();
347
+ for (const path of paths ?? this.fieldOptions.keys()) {
348
+ if (this.fieldOptions.has(path))
349
+ fieldVersions.set(path, this.fieldValidationVersions.get(path) ?? 0);
350
+ }
351
+ for (const path of paths ?? this.fieldOptions.keys()) {
352
+ this.validatingFields[path] = true;
353
+ this.ensureFieldState(path).isValidating = true;
354
+ }
355
+ let valid = true;
356
+ try {
357
+ const schemaResult = await this.validator.resolveSchemaErrors(paths);
358
+ const schemaErrors = schemaResult.errors;
359
+ const validationPaths = paths ?? [...new Set([...this.fieldOptions.keys(), ...this.errorStore.paths(), ...collectErrorPaths(schemaErrors)])];
360
+ for (const path of validationPaths) {
361
+ const ruleError = await this.validator.validateRules(path);
362
+ runInAction(() => {
363
+ if (this.isValidationCurrent(path, run, fieldVersions)) {
364
+ const schemaError = findErrorAtPath(schemaErrors, path);
365
+ const error = this.validator.mergeErrors(schemaError, ruleError);
366
+ if (error)
367
+ valid = false;
368
+ this.validator.applyValidationError(path, error);
369
+ }
370
+ });
371
+ }
372
+ return { valid, values: valid ? schemaResult.values : undefined };
373
+ }
374
+ finally {
375
+ runInAction(() => {
376
+ if (this.validationVersion === run) {
377
+ for (const path of paths ?? this.fieldOptions.keys()) {
378
+ if (!this.isValidationCurrent(path, run, fieldVersions))
379
+ continue;
380
+ delete this.validatingFields[path];
381
+ this.ensureFieldState(path).isValidating = false;
382
+ }
383
+ }
384
+ });
385
+ }
386
+ }
387
+ /**
388
+ * Creates an asynchronous submit handler with validation and result callbacks.
389
+ *
390
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#handlesubmithandlers)
391
+ */
392
+ handleSubmit({ onValid, onInvalid }) {
393
+ return async () => {
394
+ this.activeSubmissions += 1;
395
+ this.isSubmitting = true;
396
+ this.isSubmitted = true;
397
+ this.submitCount += 1;
398
+ const submissionResetVersion = this.resetVersion;
399
+ try {
400
+ const { valid, values } = await this.runValidation();
401
+ if (valid) {
402
+ await onValid(values ?? this.snapshot, this);
403
+ runInAction(() => {
404
+ if (this.resetVersion === submissionResetVersion)
405
+ this.isSubmitSuccessful = true;
406
+ });
407
+ }
408
+ else {
409
+ if (this.options.shouldFocusError)
410
+ this.focusFirstError();
411
+ await onInvalid?.(this.errors, this);
412
+ runInAction(() => {
413
+ if (this.resetVersion === submissionResetVersion)
414
+ this.isSubmitSuccessful = false;
415
+ });
416
+ }
417
+ }
418
+ finally {
419
+ runInAction(() => {
420
+ this.activeSubmissions -= 1;
421
+ this.isSubmitting = this.activeSubmissions > 0;
422
+ });
423
+ }
424
+ };
425
+ }
426
+ /**
427
+ * Resets values and selected form state.
428
+ *
429
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#resetvalues-options)
430
+ */
431
+ reset(values, options = {}) {
432
+ this.validator.cancelAllDelayed();
433
+ this.touchedValidationFields.clear();
434
+ this.resetVersion += 1;
435
+ this.validationVersion += 1;
436
+ if (!options.keepIsValidating) {
437
+ for (const path of Object.keys(this.validatingFields)) {
438
+ delete this.validatingFields[path];
439
+ const state = this.fieldStateStore.get(path);
440
+ if (state)
441
+ state.isValidating = false;
442
+ }
443
+ }
444
+ if (!options.keepValues) {
445
+ const next = clone((values ?? this.defaultValues));
446
+ if (options.keepDirtyValues) {
447
+ for (const path of Object.keys(this.dirtyFields)) {
448
+ const value = getAtPath(this.values, path);
449
+ if (value !== undefined)
450
+ setAtPath(next, path, clone(value));
451
+ }
452
+ }
453
+ this.values = next;
454
+ // New values need a fresh validity check on the next isValid read.
455
+ this.hasValidationRun = false;
456
+ }
457
+ if (!options.keepDefaultValues && values)
458
+ Object.assign(this.defaultValues, clone(values));
459
+ if (!options.keepDirty && !options.keepDirtyValues)
460
+ this.dirtyFields = {};
461
+ if (!options.keepTouched)
462
+ this.touchedFields = {};
463
+ // Raw validity without the lazy warm-up: reset must never schedule validation.
464
+ const wasValid = this.isValidOverride.get() ?? this.errorStore.size === 0;
465
+ if (!options.keepErrors) {
466
+ this.errorStore.clear();
467
+ for (const [, state] of this.fieldStateStore.entries())
468
+ this.applyFieldState(state, undefined);
469
+ }
470
+ this.isValidOverride.set(options.keepIsValid ? wasValid : undefined);
471
+ if (!options.keepIsSubmitted)
472
+ this.isSubmitted = false;
473
+ if (!options.keepIsSubmitSuccessful)
474
+ this.isSubmitSuccessful = false;
475
+ if (!options.keepSubmitCount)
476
+ this.submitCount = 0;
477
+ for (const [path, state] of this.fieldStateStore.entries()) {
478
+ state.isDirty = !!this.dirtyFields[path];
479
+ state.isTouched = !!this.touchedFields[path];
480
+ }
481
+ }
482
+ /**
483
+ * Resets one field to its current default value and clears its state.
484
+ *
485
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#resetfieldname)
486
+ */
487
+ resetField(name, options = {}) {
488
+ const path = name;
489
+ this.validator.cancelDelayed(path);
490
+ this.touchedValidationFields.delete(path);
491
+ this.fieldValidationVersions.set(path, (this.fieldValidationVersions.get(path) ?? 0) + 1);
492
+ if ('defaultValue' in options)
493
+ setAtPath(this.defaultValues, path, clone(options.defaultValue));
494
+ setAtPath(this.values, path, clone('defaultValue' in options ? options.defaultValue : getAtPath(this.defaultValues, path)));
495
+ if (!options.keepError)
496
+ this.applyError(path, undefined);
497
+ if (!options.keepDirty)
498
+ delete this.dirtyFields[path];
499
+ if (!options.keepTouched)
500
+ delete this.touchedFields[path];
501
+ delete this.validatingFields[path];
502
+ const state = this.ensureFieldState(path);
503
+ if (!options.keepError)
504
+ this.applyFieldState(state, undefined);
505
+ state.isDirty = options.keepDirty ? !!this.dirtyFields[path] : false;
506
+ state.isTouched = options.keepTouched ? !!this.touchedFields[path] : false;
507
+ state.isValidating = false;
508
+ }
509
+ /**
510
+ * Focuses a registered field when its ref points to a focusable element.
511
+ *
512
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#setfocusname)
513
+ */
514
+ setFocus(name) { this.refs.get(name)?.current?.focus(); }
515
+ /**
516
+ * Returns a plain copy of the current values.
517
+ *
518
+ * [**Documentation**](https://js2me.github.io/mobx-formly/api/form.html#snapshot)
519
+ */
520
+ get snapshot() { return clone(this.values); }
521
+ /** Reconciles dirty paths against the complete current value tree. */
522
+ syncDirtyFields(paths) {
523
+ const dirty = new Set(paths);
524
+ this.dirtyFields = Object.fromEntries(paths.map((path) => [path, true]));
525
+ for (const [path, state] of this.fieldStateStore.entries())
526
+ state.isDirty = dirty.has(path);
527
+ for (const path of paths)
528
+ this.ensureFieldState(path).isDirty = true;
529
+ }
530
+ /** Clears an exact error path and every nested error below it. */
531
+ clearErrorPath(path) {
532
+ const paths = this.errorStore.paths().filter((stored) => stored === path || stored.startsWith(`${path}.`));
533
+ if (!paths.length) {
534
+ this.validator.cancelDelayed(path);
535
+ this.applyError(path, undefined);
536
+ return;
537
+ }
538
+ for (const storedPath of paths) {
539
+ this.validator.cancelDelayed(storedPath);
540
+ this.applyError(storedPath, undefined);
541
+ }
542
+ }
543
+ /** Focuses the first errored registered field, skipping form-level errors. */
544
+ focusFirstError(paths) {
545
+ const candidates = paths ?? this.errorStore.paths();
546
+ for (const path of candidates) {
547
+ if (!this.hasError(path))
548
+ continue;
549
+ const ref = this.refs.get(path)?.current;
550
+ if (ref?.focus) {
551
+ ref.focus();
552
+ return;
553
+ }
554
+ }
555
+ }
556
+ dependentFields(deps) {
557
+ if (!deps)
558
+ return [];
559
+ return (Array.isArray(deps) ? deps : [deps]).map(String);
560
+ }
561
+ markTouched(path) {
562
+ this.touchedFields[path] = true;
563
+ this.ensureFieldState(path).isTouched = true;
564
+ }
565
+ updateDirty(path) {
566
+ if (isEqual(getAtPath(this.values, path), getAtPath(this.defaultValues, path)))
567
+ delete this.dirtyFields[path];
568
+ else
569
+ this.dirtyFields[path] = true;
570
+ this.ensureFieldState(path).isDirty = !!this.dirtyFields[path];
571
+ }
572
+ shouldValidateOnChange(path) {
573
+ return this.options.mode === 'onChange'
574
+ || this.options.mode === 'all'
575
+ || (this.options.mode === 'onTouched' && this.touchedValidationFields.has(path))
576
+ || (this.hasError(path) && this.options.reValidateMode === 'onChange');
577
+ }
578
+ isValidationCurrent(path, run, fieldVersions) {
579
+ if (this.validationVersion !== run)
580
+ return false;
581
+ const version = fieldVersions.get(path);
582
+ return version === undefined
583
+ || (this.fieldOptions.has(path) && (this.fieldValidationVersions.get(path) ?? 0) === version);
584
+ }
585
+ transformValue(value, options) {
586
+ if (options.setValueAs)
587
+ return options.setValueAs(value);
588
+ if (options.valueAsNumber)
589
+ return value === '' ? Number.NaN : Number(value);
590
+ if (options.valueAsDate)
591
+ return new Date(String(value));
592
+ return value;
593
+ }
594
+ ensureFieldState(path) {
595
+ return this.fieldStateStore.ensure(path, () => ({ invalid: false, isDirty: false, isTouched: false, isValidating: false, error: undefined }));
596
+ }
597
+ applyFieldState(state, error) {
598
+ state.error = error;
599
+ state.invalid = !!error;
600
+ }
601
+ applyError(path, error) {
602
+ this.isValidOverride.set(undefined);
603
+ if (error)
604
+ this.errorStore.set(path, error);
605
+ else
606
+ this.errorStore.delete(path);
607
+ const state = this.ensureFieldState(path);
608
+ state.error = error;
609
+ state.invalid = !!error;
610
+ }
611
+ hasError(path) { return this.errorStore.has(path); }
612
+ }
@@ -0,0 +1,5 @@
1
+ export { BaseForm } from './form.js';
2
+ export type { Form } from './form.js';
3
+ export { createForm } from './create-form.js';
4
+ export type { InferredFormValues } from './create-form.js';
5
+ export type { ErrorNamespacePath, FieldError, FieldErrors, FieldPath, FieldPathValue, FieldState, FieldStateTree, FieldValues, FieldValidate, FormOptions, FormSchema, GlobalErrors, ResetFieldOptions, SafeParseFormSchema, SchemaIssue, SchemaOutput, SchemaResult, StandardFormSchema, StandardSchemaIssue, StandardSchemaResult, ValibotFormSchema, ValibotRunResult, ValidateResult, Resolver, ResolverOptions, ResolverResult, RegisterOptions, RegisterReturn, ResetOptions, SetErrorConfig, SetValueConfig, SubmitHandlers, TriggerConfig, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BaseForm } from './form.js';
2
+ export { createForm } from './create-form.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Path-indexed observable store with lazy nested proxies.
3
+ *
4
+ * Values are stored flat by dot-separated path while reads go through
5
+ * stable proxies that expose the same data as a nested tree. A reverse
6
+ * index of path counts and children sets keeps parent branches alive
7
+ * while any child path exists.
8
+ */
9
+ export declare class PathStore<V extends object> {
10
+ private readonly store;
11
+ private readonly counts;
12
+ private readonly children;
13
+ private readonly proxies;
14
+ /** Whether a value is stored at the exact path. */
15
+ has(path: string): boolean;
16
+ /** Number of stored values. */
17
+ get size(): number;
18
+ /** Returns the stored value at the exact path. */
19
+ get(path: string): V | undefined;
20
+ /** All stored path and value pairs. */
21
+ entries(): Array<[string, V]>;
22
+ /** All stored paths. */
23
+ paths(): string[];
24
+ /** Stores a value and registers its path in the reverse index. */
25
+ set(path: string, value: V): void;
26
+ /** Removes a value and prunes empty parent branches of the index. */
27
+ delete(path: string): void;
28
+ /** Removes all values and index data. */
29
+ clear(): void;
30
+ /** Returns the stored value or creates, stores, and returns it. */
31
+ ensure(path: string, create: () => V): V;
32
+ /** Returns the stable read-only nested view of the stored values. */
33
+ proxy(): object;
34
+ private createProxy;
35
+ /** Drops cached proxies for the removed path and its children so churn does not accumulate. */
36
+ private evictProxies;
37
+ private addPathToIndex;
38
+ }