@tanstack/form-core 0.0.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.
@@ -0,0 +1,634 @@
1
+ /**
2
+ * form-core
3
+ *
4
+ * Copyright (c) TanStack
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE.md file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import { Store } from '@tanstack/store';
12
+
13
+ function functionalUpdate(updater, input) {
14
+ return typeof updater === 'function' ? updater(input) : updater;
15
+ }
16
+ function getBy(obj, path) {
17
+ if (!path) {
18
+ throw new Error('A path string is required to use getBy');
19
+ }
20
+
21
+ const pathArray = makePathArray(path);
22
+ const pathObj = pathArray;
23
+ return pathObj.reduce((current, pathPart) => {
24
+ if (typeof current !== 'undefined') {
25
+ return current[pathPart];
26
+ }
27
+
28
+ return undefined;
29
+ }, obj);
30
+ }
31
+ function setBy(obj, _path, updater) {
32
+ const path = makePathArray(_path);
33
+
34
+ function doSet(parent) {
35
+ if (!path.length) {
36
+ return functionalUpdate(updater, parent);
37
+ }
38
+
39
+ const key = path.shift();
40
+
41
+ if (typeof key === 'string') {
42
+ if (typeof parent === 'object') {
43
+ return { ...parent,
44
+ [key]: doSet(parent[key])
45
+ };
46
+ }
47
+
48
+ return {
49
+ [key]: doSet()
50
+ };
51
+ }
52
+
53
+ if (typeof key === 'number') {
54
+ if (Array.isArray(parent)) {
55
+ const prefix = parent.slice(0, key);
56
+ return [...(prefix.length ? prefix : new Array(key)), doSet(parent[key]), ...parent.slice(key + 1)];
57
+ }
58
+
59
+ return [...new Array(key), doSet()];
60
+ }
61
+
62
+ throw new Error('Uh oh!');
63
+ }
64
+
65
+ return doSet(obj);
66
+ }
67
+ const reFindNumbers0 = /^(\d*)$/gm;
68
+ const reFindNumbers1 = /\.(\d*)\./gm;
69
+ const reFindNumbers2 = /^(\d*)\./gm;
70
+ const reFindNumbers3 = /\.(\d*$)/gm;
71
+ const reFindMultiplePeriods = /\.{2,}/gm;
72
+
73
+ function makePathArray(str) {
74
+ return str.replace('[', '.').replace(']', '').replace(reFindNumbers0, '__int__$1').replace(reFindNumbers1, '.__int__$1.').replace(reFindNumbers2, '__int__$1.').replace(reFindNumbers3, '.__int__$1').replace(reFindMultiplePeriods, '.').split('.').map(d => {
75
+ if (d.indexOf('__int__') === 0) {
76
+ return parseInt(d.substring('__int__'.length), 10);
77
+ }
78
+
79
+ return d;
80
+ });
81
+ }
82
+
83
+ function getDefaultFormState(defaultState) {
84
+ return {
85
+ values: {},
86
+ fieldMeta: {},
87
+ canSubmit: true,
88
+ isFieldsValid: false,
89
+ isFieldsValidating: false,
90
+ isFormValid: false,
91
+ isFormValidating: false,
92
+ isSubmitted: false,
93
+ isSubmitting: false,
94
+ isTouched: false,
95
+ isValid: false,
96
+ isValidating: false,
97
+ submissionAttempts: 0,
98
+ formValidationCount: 0,
99
+ ...defaultState
100
+ };
101
+ }
102
+ class FormApi {
103
+ // // This carries the context for nested fields
104
+ constructor(_opts) {
105
+ var _opts$defaultValues, _opts$defaultState;
106
+
107
+ this.options = {};
108
+ this.fieldInfo = {};
109
+ this.validationMeta = {};
110
+
111
+ this.update = options => {
112
+ this.store.batch(() => {
113
+ if (options.defaultState && options.defaultState !== this.options.defaultState) {
114
+ this.store.setState(prev => ({ ...prev,
115
+ ...options.defaultState
116
+ }));
117
+ }
118
+
119
+ if (options.defaultValues !== this.options.defaultValues) {
120
+ this.store.setState(prev => ({ ...prev,
121
+ values: options.defaultValues
122
+ }));
123
+ }
124
+ });
125
+ this.options = options;
126
+ };
127
+
128
+ this.reset = () => this.store.setState(() => getDefaultFormState(this.options.defaultValues));
129
+
130
+ this.validateAllFields = async () => {
131
+ const fieldValidationPromises = [];
132
+ this.store.batch(() => {
133
+ void Object.values(this.fieldInfo).forEach(field => {
134
+ Object.values(field.instances).forEach(instance => {
135
+ // If any fields are not touched
136
+ if (!instance.state.meta.isTouched) {
137
+ // Mark them as touched
138
+ instance.setMeta(prev => ({ ...prev,
139
+ isTouched: true
140
+ })); // Validate the field
141
+
142
+ if (instance.options.validate) {
143
+ fieldValidationPromises.push(instance.validate());
144
+ }
145
+ }
146
+ });
147
+ });
148
+ });
149
+ return Promise.all(fieldValidationPromises);
150
+ };
151
+
152
+ this.validateForm = async () => {
153
+ const {
154
+ validate
155
+ } = this.options;
156
+
157
+ if (!validate) {
158
+ return;
159
+ } // Use the formValidationCount for all field instances to
160
+ // track freshness of the validation
161
+
162
+
163
+ this.store.setState(prev => ({ ...prev,
164
+ isValidating: true,
165
+ formValidationCount: prev.formValidationCount + 1
166
+ }));
167
+ const formValidationCount = this.state.formValidationCount;
168
+
169
+ const checkLatest = () => formValidationCount === this.state.formValidationCount;
170
+
171
+ if (!this.validationMeta.validationPromise) {
172
+ this.validationMeta.validationPromise = new Promise((resolve, reject) => {
173
+ this.validationMeta.validationResolve = resolve;
174
+ this.validationMeta.validationReject = reject;
175
+ });
176
+ }
177
+
178
+ const doValidation = async () => {
179
+ try {
180
+ const error = await validate(this.state.values, this);
181
+
182
+ if (checkLatest()) {
183
+ var _this$validationMeta$, _this$validationMeta;
184
+
185
+ this.store.setState(prev => ({ ...prev,
186
+ isValidating: false,
187
+ error: error ? typeof error === 'string' ? error : 'Invalid Form Values' : null
188
+ }));
189
+ (_this$validationMeta$ = (_this$validationMeta = this.validationMeta).validationResolve) == null ? void 0 : _this$validationMeta$.call(_this$validationMeta, error);
190
+ }
191
+ } catch (err) {
192
+ if (checkLatest()) {
193
+ var _this$validationMeta$2, _this$validationMeta2;
194
+
195
+ (_this$validationMeta$2 = (_this$validationMeta2 = this.validationMeta).validationReject) == null ? void 0 : _this$validationMeta$2.call(_this$validationMeta2, err);
196
+ }
197
+ } finally {
198
+ delete this.validationMeta.validationPromise;
199
+ }
200
+ };
201
+
202
+ doValidation();
203
+ return this.validationMeta.validationPromise;
204
+ };
205
+
206
+ this.handleSubmit = async e => {
207
+ e.preventDefault();
208
+ e.stopPropagation(); // Check to see that the form and all fields have been touched
209
+ // If they have not, touch them all and run validation
210
+ // Run form validation
211
+ // Submit the form
212
+
213
+ this.store.setState(old => ({ ...old,
214
+ // Submittion attempts mark the form as not submitted
215
+ isSubmitted: false,
216
+ // Count submission attempts
217
+ submissionAttempts: old.submissionAttempts + 1
218
+ })); // Don't let invalid forms submit
219
+
220
+ if (!this.state.canSubmit) return;
221
+ this.store.setState(d => ({ ...d,
222
+ isSubmitting: true
223
+ }));
224
+
225
+ const done = () => {
226
+ this.store.setState(prev => ({ ...prev,
227
+ isSubmitting: false
228
+ }));
229
+ }; // Validate all fields
230
+
231
+
232
+ await this.validateAllFields(); // Fields are invalid, do not submit
233
+
234
+ if (!this.state.isFieldsValid) {
235
+ var _this$options$onInval, _this$options;
236
+
237
+ done();
238
+ (_this$options$onInval = (_this$options = this.options).onInvalidSubmit) == null ? void 0 : _this$options$onInval.call(_this$options, this.state.values, this);
239
+ return;
240
+ } // Run validation for the form
241
+
242
+
243
+ await this.validateForm();
244
+
245
+ if (!this.state.isValid) {
246
+ var _this$options$onInval2, _this$options2;
247
+
248
+ done();
249
+ (_this$options$onInval2 = (_this$options2 = this.options).onInvalidSubmit) == null ? void 0 : _this$options$onInval2.call(_this$options2, this.state.values, this);
250
+ return;
251
+ }
252
+
253
+ try {
254
+ var _this$options$onSubmi, _this$options3;
255
+
256
+ // Run the submit code
257
+ await ((_this$options$onSubmi = (_this$options3 = this.options).onSubmit) == null ? void 0 : _this$options$onSubmi.call(_this$options3, this.state.values, this));
258
+ this.store.batch(() => {
259
+ this.store.setState(prev => ({ ...prev,
260
+ isSubmitted: true
261
+ }));
262
+ done();
263
+ });
264
+ } catch (err) {
265
+ done();
266
+ throw err;
267
+ }
268
+ };
269
+
270
+ this.getFieldValue = field => getBy(this.state.values, field);
271
+
272
+ this.getFieldMeta = field => {
273
+ return this.state.fieldMeta[field];
274
+ };
275
+
276
+ this.getFieldInfo = field => {
277
+ var _this$fieldInfo;
278
+
279
+ return (_this$fieldInfo = this.fieldInfo)[field] || (_this$fieldInfo[field] = {
280
+ instances: {}
281
+ });
282
+ };
283
+
284
+ this.setFieldMeta = (field, updater) => {
285
+ this.store.setState(prev => {
286
+ return { ...prev,
287
+ fieldMeta: { ...prev.fieldMeta,
288
+ [field]: functionalUpdate(updater, prev.fieldMeta[field])
289
+ }
290
+ };
291
+ });
292
+ };
293
+
294
+ this.setFieldValue = (field, updater, opts) => {
295
+ var _opts$touch;
296
+
297
+ const touch = (_opts$touch = opts == null ? void 0 : opts.touch) != null ? _opts$touch : true;
298
+ this.store.batch(() => {
299
+ this.store.setState(prev => {
300
+ return { ...prev,
301
+ values: setBy(prev.values, field, updater)
302
+ };
303
+ });
304
+
305
+ if (touch) {
306
+ this.setFieldMeta(field, prev => ({ ...prev,
307
+ isTouched: true
308
+ }));
309
+ }
310
+ });
311
+ };
312
+
313
+ this.pushFieldValue = (field, value, opts) => {
314
+ return this.setFieldValue(field, prev => [...(Array.isArray(prev) ? prev : []), value], opts);
315
+ };
316
+
317
+ this.insertFieldValue = (field, index, value, opts) => {
318
+ this.setFieldValue(field, prev => {
319
+ // invariant( // TODO: bring in invariant
320
+ // Array.isArray(prev),
321
+ // `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
322
+ // )
323
+ return prev.map((d, i) => i === index ? value : d);
324
+ }, opts);
325
+ };
326
+
327
+ this.spliceFieldValue = (field, index, opts) => {
328
+ this.setFieldValue(field, prev => {
329
+ // invariant( // TODO: bring in invariant
330
+ // Array.isArray(prev),
331
+ // `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
332
+ // )
333
+ return prev.filter((_d, i) => i !== index);
334
+ }, opts);
335
+ };
336
+
337
+ this.swapFieldValues = (field, index1, index2) => {
338
+ this.setFieldValue(field, prev => {
339
+ const prev1 = prev[index1];
340
+ const prev2 = prev[index2];
341
+ return setBy(setBy(prev, [index1], prev2), [index2], prev1);
342
+ });
343
+ };
344
+
345
+ this.store = new Store(getDefaultFormState({ ...(_opts == null ? void 0 : _opts.defaultState),
346
+ values: (_opts$defaultValues = _opts == null ? void 0 : _opts.defaultValues) != null ? _opts$defaultValues : _opts == null ? void 0 : (_opts$defaultState = _opts.defaultState) == null ? void 0 : _opts$defaultState.values,
347
+ isFormValid: !(_opts != null && _opts.validate)
348
+ }), {
349
+ onUpdate: next => {
350
+ // Computed state
351
+ const fieldMetaValues = Object.values(next.fieldMeta);
352
+ const isFieldsValidating = fieldMetaValues.some(field => field == null ? void 0 : field.isValidating);
353
+ const isFieldsValid = !fieldMetaValues.some(field => field == null ? void 0 : field.error);
354
+ const isTouched = fieldMetaValues.some(field => field == null ? void 0 : field.isTouched);
355
+ const isValidating = isFieldsValidating || next.isFormValidating;
356
+ const isFormValid = !next.formError;
357
+ const isValid = isFieldsValid && isFormValid;
358
+ const canSubmit = next.submissionAttempts === 0 && !isTouched || !isValidating && !next.isSubmitting && isValid;
359
+ next = { ...next,
360
+ isFieldsValidating,
361
+ isFieldsValid,
362
+ isFormValid,
363
+ isValid,
364
+ canSubmit,
365
+ isTouched
366
+ }; // Create a shortcut for the state
367
+ // Write it back to the store
368
+
369
+ this.store.state = next;
370
+ this.state = next;
371
+ }
372
+ });
373
+ this.state = this.store.state;
374
+ this.update(_opts || {});
375
+ }
376
+
377
+ }
378
+
379
+ var id = 0;
380
+
381
+ function _classPrivateFieldLooseKey(name) {
382
+ return "__private_" + id++ + "_" + name;
383
+ }
384
+
385
+ function _classPrivateFieldLooseBase(receiver, privateKey) {
386
+ if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
387
+ throw new TypeError("attempted to use private field on non-instance");
388
+ }
389
+
390
+ return receiver;
391
+ }
392
+
393
+ let uid = 0;
394
+
395
+ var _updateStore = /*#__PURE__*/_classPrivateFieldLooseKey("updateStore");
396
+
397
+ var _validate = /*#__PURE__*/_classPrivateFieldLooseKey("validate");
398
+
399
+ class FieldApi {
400
+ constructor(_opts) {
401
+ var _this$getMeta;
402
+
403
+ this.options = {};
404
+
405
+ this.mount = () => {
406
+ const info = this.getInfo();
407
+ info.instances[this.uid] = this;
408
+ const unsubscribe = this.form.store.subscribe(() => {
409
+ _classPrivateFieldLooseBase(this, _updateStore)[_updateStore]();
410
+ });
411
+ return () => {
412
+ unsubscribe();
413
+ delete info.instances[this.uid];
414
+
415
+ if (!Object.keys(info.instances).length) {
416
+ delete this.form.fieldInfo[this.name];
417
+ }
418
+ };
419
+ };
420
+
421
+ Object.defineProperty(this, _updateStore, {
422
+ writable: true,
423
+ value: () => {
424
+ this.store.batch(() => {
425
+ const nextValue = this.getValue();
426
+ const nextMeta = this.getMeta();
427
+
428
+ if (nextValue !== this.state.value) {
429
+ this.store.setState(prev => ({ ...prev,
430
+ value: nextValue
431
+ }));
432
+ }
433
+
434
+ if (nextMeta !== this.state.meta) {
435
+ this.store.setState(prev => ({ ...prev,
436
+ meta: nextMeta
437
+ }));
438
+ }
439
+ });
440
+ }
441
+ });
442
+
443
+ this.update = opts => {
444
+ this.options = {
445
+ validateOn: 'change',
446
+ validateAsyncOn: 'blur',
447
+ validateAsyncDebounceMs: 0,
448
+ ...opts
449
+ }; // Default Value
450
+
451
+ if (this.state.value === undefined && this.options.defaultValue !== undefined) {
452
+ this.setValue(this.options.defaultValue);
453
+ } // Default Meta
454
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
455
+
456
+
457
+ if (this.getMeta() === undefined) {
458
+ this.setMeta(this.state.meta);
459
+ }
460
+ };
461
+
462
+ this.getValue = () => this.form.getFieldValue(this.name);
463
+
464
+ this.setValue = (updater, options) => this.form.setFieldValue(this.name, updater, options);
465
+
466
+ this.getMeta = () => this.form.getFieldMeta(this.name);
467
+
468
+ this.setMeta = updater => this.form.setFieldMeta(this.name, updater);
469
+
470
+ this.getInfo = () => this.form.getFieldInfo(this.name);
471
+
472
+ this.pushValue = value => this.form.pushFieldValue(this.name, value);
473
+
474
+ this.insertValue = (index, value) => this.form.insertFieldValue(this.name, index, value);
475
+
476
+ this.removeValue = index => this.form.spliceFieldValue(this.name, index);
477
+
478
+ this.swapValues = (aIndex, bIndex) => this.form.swapFieldValues(this.name, aIndex, bIndex);
479
+
480
+ this.getSubField = name => new FieldApi({
481
+ name: this.name + "." + name,
482
+ form: this.form
483
+ });
484
+
485
+ Object.defineProperty(this, _validate, {
486
+ writable: true,
487
+ value: async isAsync => {
488
+ if (!this.options.validate) {
489
+ return;
490
+ }
491
+
492
+ this.setMeta(prev => ({ ...prev,
493
+ isValidating: true
494
+ })); // Use the validationCount for all field instances to
495
+ // track freshness of the validation
496
+
497
+ const validationCount = (this.getInfo().validationCount || 0) + 1;
498
+ this.getInfo().validationCount = validationCount;
499
+
500
+ const checkLatest = () => validationCount === this.getInfo().validationCount;
501
+
502
+ if (!this.getInfo().validationPromise) {
503
+ this.getInfo().validationPromise = new Promise((resolve, reject) => {
504
+ this.getInfo().validationResolve = resolve;
505
+ this.getInfo().validationReject = reject;
506
+ });
507
+ }
508
+
509
+ try {
510
+ const rawError = await this.options.validate(this.state.value, this);
511
+
512
+ if (checkLatest()) {
513
+ var _this$getInfo$validat, _this$getInfo;
514
+
515
+ const error = (() => {
516
+ if (rawError) {
517
+ if (typeof rawError !== 'string') {
518
+ return 'Invalid Form Values';
519
+ }
520
+
521
+ return rawError;
522
+ }
523
+
524
+ return undefined;
525
+ })();
526
+
527
+ this.setMeta(prev => ({ ...prev,
528
+ isValidating: false,
529
+ error
530
+ }));
531
+ (_this$getInfo$validat = (_this$getInfo = this.getInfo()).validationResolve) == null ? void 0 : _this$getInfo$validat.call(_this$getInfo, error);
532
+ }
533
+ } catch (error) {
534
+ if (checkLatest()) {
535
+ var _this$getInfo$validat2, _this$getInfo2;
536
+
537
+ (_this$getInfo$validat2 = (_this$getInfo2 = this.getInfo()).validationReject) == null ? void 0 : _this$getInfo$validat2.call(_this$getInfo2, error);
538
+ throw error;
539
+ }
540
+ } finally {
541
+ if (checkLatest()) {
542
+ this.setMeta(prev => ({ ...prev,
543
+ isValidating: false
544
+ }));
545
+ delete this.getInfo().validationPromise;
546
+ }
547
+ }
548
+
549
+ return this.getInfo().validationPromise;
550
+ }
551
+ });
552
+
553
+ this.validate = () => _classPrivateFieldLooseBase(this, _validate)[_validate](false);
554
+
555
+ this.validateAsync = () => _classPrivateFieldLooseBase(this, _validate)[_validate](true);
556
+
557
+ this.getChangeProps = (props = {}) => {
558
+ return { ...props,
559
+ value: this.state.value,
560
+ onChange: value => {
561
+ this.setValue(value);
562
+ props.onChange == null ? void 0 : props.onChange(value);
563
+ },
564
+ onBlur: e => {
565
+ this.setMeta(prev => ({ ...prev,
566
+ isTouched: true
567
+ }));
568
+ const {
569
+ validateOn
570
+ } = this.options;
571
+
572
+ if (validateOn === 'blur' || validateOn.split('-')[0] === 'blur') {
573
+ this.validate();
574
+ }
575
+
576
+ props.onBlur == null ? void 0 : props.onBlur(e);
577
+ }
578
+ };
579
+ };
580
+
581
+ this.getInputProps = (props = {}) => {
582
+ return { ...props,
583
+ value: String(this.state.value),
584
+ onChange: e => {
585
+ this.setValue(e.target.value);
586
+ props.onChange == null ? void 0 : props.onChange(e.target.value);
587
+ },
588
+ onBlur: this.getChangeProps(props).onBlur
589
+ };
590
+ };
591
+
592
+ this.form = _opts.form;
593
+ this.uid = uid++; // Support field prefixing from FieldScope
594
+
595
+ let fieldPrefix = '';
596
+
597
+ if (this.form.fieldName) {
598
+ fieldPrefix = this.form.fieldName + ".";
599
+ }
600
+
601
+ this.name = fieldPrefix + _opts.name;
602
+ this.store = new Store({
603
+ value: this.getValue(),
604
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
605
+ meta: (_this$getMeta = this.getMeta()) != null ? _this$getMeta : {
606
+ isValidating: false,
607
+ isTouched: false,
608
+ ...this.options.defaultMeta
609
+ }
610
+ }, {
611
+ onUpdate: next => {
612
+ next.meta.touchedError = next.meta.isTouched ? next.meta.error : undefined; // Do not validate pristine fields
613
+
614
+ if (!this.options.validatePristine && !next.meta.isTouched) return; // If validateOn is set to a variation of change, run the validation
615
+
616
+ if (this.options.validateOn === 'change' || this.options.validateOn.split('-')[0] === 'change') {
617
+ try {
618
+ this.validate();
619
+ } catch (err) {
620
+ console.error('An error occurred during validation', err);
621
+ }
622
+ }
623
+
624
+ this.state = next;
625
+ }
626
+ });
627
+ this.state = this.store.state;
628
+ this.update(_opts);
629
+ }
630
+
631
+ }
632
+
633
+ export { FieldApi, FormApi, functionalUpdate, getBy, getDefaultFormState, setBy };
634
+ //# sourceMappingURL=index.js.map