@homebound/form-state 2.3.0 → 2.4.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/formState.d.ts +4 -2
- package/dist/formState.js +24 -17
- package/dist/formState.test.js +114 -39
- package/dist/useFormState.js +6 -3
- package/dist/useFormState.test.js +7 -2
- package/package.json +1 -1
package/dist/formState.d.ts
CHANGED
|
@@ -72,8 +72,10 @@ export interface FieldState<T, V> {
|
|
|
72
72
|
readonly changedValue: V;
|
|
73
73
|
/** Focuses the field. Disables changes from `ObjectState.set` calls. */
|
|
74
74
|
focus(): void;
|
|
75
|
-
/** Blur
|
|
75
|
+
/** Blur marks the field as touched and triggers an auto-save. */
|
|
76
76
|
blur(): void;
|
|
77
|
+
/** Triggers an auto-save; the caller (i.e. useFormState) should still dirty & valid check. */
|
|
78
|
+
maybeAutoSave(): void;
|
|
77
79
|
set(value: V, opts?: SetOpts): void;
|
|
78
80
|
/** Reverts back to the original value and resets dirty/touched. */
|
|
79
81
|
reset(): void;
|
|
@@ -161,7 +163,7 @@ export declare type ObjectFieldConfig<U> = {
|
|
|
161
163
|
* individual fields as well as the top-level form/object itself.
|
|
162
164
|
*/
|
|
163
165
|
export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
|
|
164
|
-
|
|
166
|
+
maybeAutoSave?: () => void;
|
|
165
167
|
}): ObjectState<T>;
|
|
166
168
|
declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
167
169
|
toJSON(): any;
|
package/dist/formState.js
CHANGED
|
@@ -12,7 +12,8 @@ const utils_1 = require("./utils");
|
|
|
12
12
|
// We pre-emptively make this a mobx action so that it's identity doesn't change when proxy-ified
|
|
13
13
|
// and breaks our ability to do `rules.some(r => r === required)`.
|
|
14
14
|
exports.required = mobx_1.action(({ value: v }) => {
|
|
15
|
-
|
|
15
|
+
const isEmptyString = typeof v === "string" ? v.trim() === "" : false;
|
|
16
|
+
return v !== undefined && v !== null && !isEmptyString ? undefined : "Required";
|
|
16
17
|
});
|
|
17
18
|
/**
|
|
18
19
|
* Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
|
|
@@ -23,10 +24,10 @@ exports.required = mobx_1.action(({ value: v }) => {
|
|
|
23
24
|
*/
|
|
24
25
|
function createObjectState(config, instance, opts = {}) {
|
|
25
26
|
const noop = () => { };
|
|
26
|
-
return newObjectState(config, undefined, instance, undefined, opts.
|
|
27
|
+
return newObjectState(config, undefined, instance, undefined, opts.maybeAutoSave || noop);
|
|
27
28
|
}
|
|
28
29
|
exports.createObjectState = createObjectState;
|
|
29
|
-
function newObjectState(config, parentState, instance, key,
|
|
30
|
+
function newObjectState(config, parentState, instance, key, maybeAutoSave) {
|
|
30
31
|
// This is what we return, but we only know it's value until we call `observable`, so
|
|
31
32
|
// we create a mutable variable to capture it so that we can create fields/call their
|
|
32
33
|
// constructors and give them a way to access it later.
|
|
@@ -42,16 +43,16 @@ function newObjectState(config, parentState, instance, key, onBlur) {
|
|
|
42
43
|
const config = _config;
|
|
43
44
|
let field;
|
|
44
45
|
if (config.type === "value") {
|
|
45
|
-
field = newValueFieldState(instance, getObjectState, key, config.rules || [], config.isIdKey || key === "id", config.isDeleteKey || false, config.isReadOnlyKey || false, config.computed || false, config.readOnly || false,
|
|
46
|
+
field = newValueFieldState(instance, getObjectState, key, config.rules || [], config.isIdKey || key === "id", config.isDeleteKey || false, config.isReadOnlyKey || false, config.computed || false, config.readOnly || false, maybeAutoSave);
|
|
46
47
|
}
|
|
47
48
|
else if (config.type === "list") {
|
|
48
|
-
field = newListFieldState(instance, getObjectState, key, config.rules || [], config, config.config,
|
|
49
|
+
field = newListFieldState(instance, getObjectState, key, config.rules || [], config, config.config, maybeAutoSave);
|
|
49
50
|
}
|
|
50
51
|
else if (config.type === "object") {
|
|
51
52
|
if (!instance[key]) {
|
|
52
53
|
instance[key] = {};
|
|
53
54
|
}
|
|
54
|
-
field = newObjectState(config.config, getObjectState, instance[key], key,
|
|
55
|
+
field = newObjectState(config.config, getObjectState, instance[key], key, maybeAutoSave);
|
|
55
56
|
}
|
|
56
57
|
else {
|
|
57
58
|
throw new Error("Unsupported");
|
|
@@ -185,7 +186,7 @@ function newObjectState(config, parentState, instance, key, onBlur) {
|
|
|
185
186
|
mobx_1.reaction(() => getFields(proxy).map((f) => f.value), () => _tick.value++);
|
|
186
187
|
return proxy;
|
|
187
188
|
}
|
|
188
|
-
function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly,
|
|
189
|
+
function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, maybeAutoSave) {
|
|
189
190
|
// keep a copy here for reference equality
|
|
190
191
|
const value = parentInstance[key];
|
|
191
192
|
let _originalValue = value === null ? undefined : is_plain_object_1.isPlainObject(value) ? mobx_1.toJS(value) : value;
|
|
@@ -262,12 +263,15 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
262
263
|
blur() {
|
|
263
264
|
// Now that the user is done editing the field, we sneak in some trim logic
|
|
264
265
|
if (this._focused) {
|
|
265
|
-
this.
|
|
266
|
+
this.maybeAutoSave();
|
|
266
267
|
}
|
|
267
268
|
this._focused = false;
|
|
269
|
+
},
|
|
270
|
+
maybeAutoSave() {
|
|
271
|
+
this.maybeTrim();
|
|
268
272
|
// touched is readonly, but we're allowed to change it
|
|
269
273
|
this.touched = true;
|
|
270
|
-
|
|
274
|
+
maybeAutoSave();
|
|
271
275
|
},
|
|
272
276
|
set(value, opts = {}) {
|
|
273
277
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
@@ -299,7 +303,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
299
303
|
// If we're being set programmatically, i.e. we don't currently have focus,
|
|
300
304
|
// call blur to trigger any auto-saves.
|
|
301
305
|
if (!this._focused && !opts.refreshing && !opts.resetting && this.dirty && changed && opts.autoSave !== false) {
|
|
302
|
-
this.
|
|
306
|
+
this.maybeAutoSave();
|
|
303
307
|
}
|
|
304
308
|
},
|
|
305
309
|
reset() {
|
|
@@ -339,7 +343,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
339
343
|
};
|
|
340
344
|
return field;
|
|
341
345
|
}
|
|
342
|
-
function newListFieldState(parentInstance, parentState, key, rules, listConfig, config,
|
|
346
|
+
function newListFieldState(parentInstance, parentState, key, rules, listConfig, config, maybeAutoSave) {
|
|
343
347
|
// Keep a map of "item in the parent list" -> "that item's ObjectState"
|
|
344
348
|
const rowMap = new Map();
|
|
345
349
|
const _tick = mobx_1.observable({ value: 1 });
|
|
@@ -399,7 +403,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
399
403
|
// Because we're reading from this.value, child will be the proxy version
|
|
400
404
|
let childState = rowMap.get(child);
|
|
401
405
|
if (!childState) {
|
|
402
|
-
childState = newObjectState(config, parentState, child, undefined,
|
|
406
|
+
childState = newObjectState(config, parentState, child, undefined, maybeAutoSave);
|
|
403
407
|
rowMap.set(child, childState);
|
|
404
408
|
}
|
|
405
409
|
return childState;
|
|
@@ -442,8 +446,11 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
442
446
|
},
|
|
443
447
|
blur() {
|
|
444
448
|
this._focused = false;
|
|
449
|
+
this.maybeAutoSave();
|
|
450
|
+
},
|
|
451
|
+
maybeAutoSave() {
|
|
445
452
|
this.touched = true;
|
|
446
|
-
|
|
453
|
+
maybeAutoSave();
|
|
447
454
|
},
|
|
448
455
|
set(values, opts = {}) {
|
|
449
456
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
@@ -462,7 +469,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
462
469
|
}
|
|
463
470
|
}
|
|
464
471
|
// If we didn't have an existing child, just make a new object state
|
|
465
|
-
childState = createObjectState(config, value, {
|
|
472
|
+
childState = createObjectState(config, value, { maybeAutoSave });
|
|
466
473
|
rowMap.set(value, childState);
|
|
467
474
|
}
|
|
468
475
|
// Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
|
|
@@ -476,12 +483,12 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
476
483
|
},
|
|
477
484
|
add(value, spliceIndex) {
|
|
478
485
|
// This is called by the user, so value should be a non-proxy value we should keep
|
|
479
|
-
const childState = createObjectState(config, value, {
|
|
486
|
+
const childState = createObjectState(config, value, { maybeAutoSave });
|
|
480
487
|
rowMap.set(value, childState);
|
|
481
488
|
this.ensureSet();
|
|
482
489
|
this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
|
|
483
490
|
_tick.value++;
|
|
484
|
-
|
|
491
|
+
maybeAutoSave();
|
|
485
492
|
},
|
|
486
493
|
remove(indexOrValue) {
|
|
487
494
|
this.ensureSet();
|
|
@@ -495,7 +502,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
495
502
|
}
|
|
496
503
|
}
|
|
497
504
|
_tick.value++;
|
|
498
|
-
|
|
505
|
+
maybeAutoSave();
|
|
499
506
|
},
|
|
500
507
|
reset() {
|
|
501
508
|
if (originalCopy) {
|
package/dist/formState.test.js
CHANGED
|
@@ -39,6 +39,21 @@ describe("formState", () => {
|
|
|
39
39
|
expect(a.title.required).toBeTruthy();
|
|
40
40
|
expect(a.isPublished.required).toBeFalsy();
|
|
41
41
|
});
|
|
42
|
+
it("trims whitespace within the required rule", () => {
|
|
43
|
+
const a = formState_1.createObjectState({ title: { type: "value", rules: [formState_1.required] } }, { title: "initial valid title" });
|
|
44
|
+
let numErrors = 0;
|
|
45
|
+
mobx_1.autorun(() => {
|
|
46
|
+
numErrors = a.title.errors.length;
|
|
47
|
+
});
|
|
48
|
+
expect(a.valid).toBeTruthy();
|
|
49
|
+
expect(numErrors).toEqual(0);
|
|
50
|
+
// When a value contains only whitespace
|
|
51
|
+
a.title.value = " ";
|
|
52
|
+
expect(a.title.valid).toBeFalsy();
|
|
53
|
+
expect(numErrors).toEqual(1);
|
|
54
|
+
expect(a.title.errors).toEqual(["Required"]);
|
|
55
|
+
expect(a.errors).toEqual(["title: Required"]);
|
|
56
|
+
});
|
|
42
57
|
it("can set values", () => {
|
|
43
58
|
const a = formState_1.createObjectState({ title: { type: "value" } }, { title: "b1" });
|
|
44
59
|
expect(a.title.value).toEqual("b1");
|
|
@@ -285,6 +300,52 @@ describe("formState", () => {
|
|
|
285
300
|
expect(a.firstName.valid).toBeTruthy();
|
|
286
301
|
expect(a.lastName.errors).toEqual(["Last name cannot be first name"]);
|
|
287
302
|
});
|
|
303
|
+
it("passes sanitized values to validation rules", () => {
|
|
304
|
+
// Given a field with a validation rule
|
|
305
|
+
const rule = jest.fn().mockReturnValue("Required");
|
|
306
|
+
const a = formState_1.createObjectState({ firstName: { type: "value", rules: [rule] } }, {});
|
|
307
|
+
expect(rule).toHaveBeenCalledTimes(0);
|
|
308
|
+
// When the field is set to empty string
|
|
309
|
+
a.firstName.value = "";
|
|
310
|
+
expect(a.firstName.errors).toEqual(["Required"]);
|
|
311
|
+
expect(rule).toHaveBeenCalledTimes(1);
|
|
312
|
+
// Then the validation rule was passed undefined
|
|
313
|
+
expect(rule).toHaveBeenCalledWith({
|
|
314
|
+
value: undefined,
|
|
315
|
+
originalValue: undefined,
|
|
316
|
+
key: "firstName",
|
|
317
|
+
object: expect.anything(),
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
it("passes trimmed values to validation rules on maybeAutoSave", () => {
|
|
321
|
+
// Given we have a required rule
|
|
322
|
+
const rule = jest.fn().mockReturnValue("Required");
|
|
323
|
+
const a = formState_1.createObjectState({ firstName: { type: "value", rules: [rule] } }, {});
|
|
324
|
+
expect(rule).toHaveBeenCalledTimes(0);
|
|
325
|
+
// When the user initially sets an empty string value
|
|
326
|
+
a.firstName.focus();
|
|
327
|
+
a.firstName.value = " ";
|
|
328
|
+
expect(a.firstName.errors).toEqual(["Required"]);
|
|
329
|
+
// Then initially the rule is called with the empty string
|
|
330
|
+
expect(rule).toHaveBeenCalledTimes(1);
|
|
331
|
+
expect(rule).toHaveBeenCalledWith({
|
|
332
|
+
value: " ",
|
|
333
|
+
originalValue: undefined,
|
|
334
|
+
key: "firstName",
|
|
335
|
+
object: expect.anything(),
|
|
336
|
+
});
|
|
337
|
+
// But then when the field maybeAutoSaves (i.e. via blur)
|
|
338
|
+
a.firstName.maybeAutoSave();
|
|
339
|
+
expect(a.firstName.errors).toEqual(["Required"]);
|
|
340
|
+
// Then the rule is called with the undefined
|
|
341
|
+
expect(rule).toHaveBeenCalledTimes(2);
|
|
342
|
+
expect(rule).toHaveBeenCalledWith({
|
|
343
|
+
value: undefined,
|
|
344
|
+
originalValue: undefined,
|
|
345
|
+
key: "firstName",
|
|
346
|
+
object: expect.anything(),
|
|
347
|
+
});
|
|
348
|
+
});
|
|
288
349
|
it("simple value changes trigger observers", () => {
|
|
289
350
|
const a = formState_1.createObjectState({
|
|
290
351
|
title: { type: "value", rules: [formState_1.required] },
|
|
@@ -330,78 +391,78 @@ describe("formState", () => {
|
|
|
330
391
|
a1.books.remove(0);
|
|
331
392
|
expect(a1.dirty).toBeFalsy();
|
|
332
393
|
});
|
|
333
|
-
it("calls
|
|
334
|
-
const
|
|
335
|
-
const a1 = createAuthorInputState({ books: [] },
|
|
394
|
+
it("calls maybeAutoSave when adding or removing to a list", () => {
|
|
395
|
+
const maybeAutoSave = jest.fn();
|
|
396
|
+
const a1 = createAuthorInputState({ books: [] }, maybeAutoSave);
|
|
336
397
|
expect(a1.books.dirty).toBeFalsy();
|
|
337
398
|
a1.books.add({ title: "t2" });
|
|
338
399
|
expect(a1.books.dirty).toBeTruthy();
|
|
339
400
|
a1.books.remove(0);
|
|
340
401
|
expect(a1.dirty).toBeFalsy();
|
|
341
|
-
expect(
|
|
402
|
+
expect(maybeAutoSave).toBeCalledTimes(2);
|
|
342
403
|
});
|
|
343
|
-
it("calls
|
|
344
|
-
const
|
|
404
|
+
it("calls maybeAutoSave when programmatically setting a value", () => {
|
|
405
|
+
const maybeAutoSave = jest.fn();
|
|
345
406
|
// Given an author listening for blur
|
|
346
|
-
const a1 = createAuthorInputState({ books: [{}] },
|
|
407
|
+
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
347
408
|
// When we programmatically set a field that isn't focused
|
|
348
409
|
a1.firstName.value = "first";
|
|
349
|
-
// Then we call
|
|
350
|
-
expect(
|
|
410
|
+
// Then we call maybeAutoSave
|
|
411
|
+
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
351
412
|
// And when we set a nested value
|
|
352
413
|
a1.books.rows[0].title.value = "title";
|
|
353
|
-
// Then we called
|
|
354
|
-
expect(
|
|
414
|
+
// Then we called maybeAutoSave again
|
|
415
|
+
expect(maybeAutoSave).toBeCalledTimes(2);
|
|
355
416
|
});
|
|
356
|
-
it("can skip
|
|
357
|
-
const
|
|
417
|
+
it("can skip maybeAutoSave when programmatically setting a value", () => {
|
|
418
|
+
const maybeAutoSave = jest.fn();
|
|
358
419
|
// Given an author listening for blur
|
|
359
|
-
const a1 = createAuthorInputState({ books: [{}] },
|
|
420
|
+
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
360
421
|
// When we programmatically set a field that isn't focused
|
|
361
422
|
a1.set({ firstName: "first" }, { autoSave: false });
|
|
362
|
-
// Then we don't call
|
|
363
|
-
expect(
|
|
423
|
+
// Then we don't call maybeAutoSave
|
|
424
|
+
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
364
425
|
});
|
|
365
|
-
it("defers calling
|
|
366
|
-
const
|
|
426
|
+
it("defers calling maybeAutoSave when setting a focused value", () => {
|
|
427
|
+
const maybeAutoSave = jest.fn();
|
|
367
428
|
// Given an author listening for blur
|
|
368
|
-
const a1 = createAuthorInputState({ books: [{}] },
|
|
429
|
+
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
369
430
|
// And the field is focused
|
|
370
431
|
a1.firstName.focus();
|
|
371
432
|
// When we we set the field
|
|
372
433
|
a1.firstName.value = "first";
|
|
373
|
-
// Then we don't call
|
|
374
|
-
expect(
|
|
434
|
+
// Then we don't call maybeAutoSave
|
|
435
|
+
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
375
436
|
});
|
|
376
|
-
it("skips
|
|
377
|
-
const
|
|
437
|
+
it("skips maybeAutoSave when refreshing", () => {
|
|
438
|
+
const maybeAutoSave = jest.fn();
|
|
378
439
|
// Given an author listening for blur
|
|
379
|
-
const a1 = createAuthorInputState({ books: [{}] },
|
|
440
|
+
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
380
441
|
// When we programmatically set a field that isn't focused
|
|
381
442
|
a1.set({ firstName: "first" }, { refreshing: true });
|
|
382
|
-
// Then we don't call
|
|
383
|
-
expect(
|
|
443
|
+
// Then we don't call maybeAutoSave
|
|
444
|
+
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
384
445
|
});
|
|
385
|
-
it("skips
|
|
386
|
-
const
|
|
446
|
+
it("skips maybeAutoSave when resetting", () => {
|
|
447
|
+
const maybeAutoSave = jest.fn();
|
|
387
448
|
// Given an author listening for blur
|
|
388
|
-
const a1 = createAuthorInputState({ books: [{}] },
|
|
389
|
-
// And we called
|
|
449
|
+
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
450
|
+
// And we called maybeAutoSave once
|
|
390
451
|
a1.set({ firstName: "first" });
|
|
391
|
-
expect(
|
|
452
|
+
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
392
453
|
// When we reset
|
|
393
454
|
a1.reset();
|
|
394
455
|
// We don't call blur again
|
|
395
|
-
expect(
|
|
456
|
+
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
396
457
|
});
|
|
397
|
-
it("skips
|
|
398
|
-
const
|
|
458
|
+
it("skips maybeAutoSave when not dirty", () => {
|
|
459
|
+
const maybeAutoSave = jest.fn();
|
|
399
460
|
// Given an author listening for blur
|
|
400
|
-
const a1 = createAuthorInputState({ firstName: "first", books: [{}] },
|
|
461
|
+
const a1 = createAuthorInputState({ firstName: "first", books: [{}] }, maybeAutoSave);
|
|
401
462
|
// When we programmatically set a field to it's existing valued
|
|
402
463
|
a1.firstName.value = "first";
|
|
403
|
-
// Then we don't call
|
|
404
|
-
expect(
|
|
464
|
+
// Then we don't call maybeAutoSave
|
|
465
|
+
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
405
466
|
});
|
|
406
467
|
it("knows list of primitives are dirty", () => {
|
|
407
468
|
const a1 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, {});
|
|
@@ -964,6 +1025,20 @@ describe("formState", () => {
|
|
|
964
1025
|
// Then we trim it
|
|
965
1026
|
expect(formState.firstName.value).toEqual("f");
|
|
966
1027
|
});
|
|
1028
|
+
it("trims string values on maybeAutoSave", () => {
|
|
1029
|
+
const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1030
|
+
// Given the user is typing with spaces
|
|
1031
|
+
formState.firstName.focus();
|
|
1032
|
+
formState.firstName.set("f ");
|
|
1033
|
+
// And we initially keep the space
|
|
1034
|
+
expect(formState.firstName.value).toEqual("f ");
|
|
1035
|
+
// When the user hits Enter (which will call maybeAutoSave)
|
|
1036
|
+
formState.firstName.maybeAutoSave();
|
|
1037
|
+
// Then we trim it
|
|
1038
|
+
expect(formState.firstName.value).toEqual("f");
|
|
1039
|
+
// And the field is considered touched
|
|
1040
|
+
expect(formState.firstName.touched).toEqual(true);
|
|
1041
|
+
});
|
|
967
1042
|
it("trims empty values to undefined on blur", () => {
|
|
968
1043
|
const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
969
1044
|
// Given the user is typing with only spaces
|
|
@@ -1102,8 +1177,8 @@ const authorWithBooksConfig = {
|
|
|
1102
1177
|
},
|
|
1103
1178
|
},
|
|
1104
1179
|
};
|
|
1105
|
-
function createAuthorInputState(input,
|
|
1106
|
-
return formState_1.createObjectState(authorWithBooksConfig, input, {
|
|
1180
|
+
function createAuthorInputState(input, maybeAutoSave) {
|
|
1181
|
+
return formState_1.createObjectState(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1107
1182
|
}
|
|
1108
1183
|
const authorWithAddressConfig = {
|
|
1109
1184
|
id: { type: "value" },
|
package/dist/useFormState.js
CHANGED
|
@@ -6,6 +6,9 @@ const mobx_1 = require("mobx");
|
|
|
6
6
|
const react_1 = require("react");
|
|
7
7
|
const formState_1 = require("./formState");
|
|
8
8
|
const utils_1 = require("./utils");
|
|
9
|
+
// If the user's autoSave hook makes some last-minute `.set` calls to sneak
|
|
10
|
+
// in some business logic right before their GraphQL mutation call, ignore it
|
|
11
|
+
// so that we don't infinite loop.
|
|
9
12
|
let isAutoSaving = false;
|
|
10
13
|
/**
|
|
11
14
|
* Creates a formState instance for editing in a form.
|
|
@@ -22,8 +25,8 @@ function useFormState(opts) {
|
|
|
22
25
|
// If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
|
|
23
26
|
const dep = init && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
|
|
24
27
|
const form = react_1.useMemo(() => {
|
|
25
|
-
function
|
|
26
|
-
// Don't use canSave() because we don't want to set touched for all
|
|
28
|
+
function maybeAutoSave() {
|
|
29
|
+
// Don't use canSave() because we don't want to set touched for all the fields
|
|
27
30
|
if (autoSaveRef.current && form.dirty && form.valid && !isAutoSaving) {
|
|
28
31
|
try {
|
|
29
32
|
isAutoSaving = true;
|
|
@@ -35,7 +38,7 @@ function useFormState(opts) {
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
const value = firstRunRef.current ? firstInitValue : initValue(config, init);
|
|
38
|
-
const form = formState_1.createObjectState(config, value, {
|
|
41
|
+
const form = formState_1.createObjectState(config, value, { maybeAutoSave });
|
|
39
42
|
form.readOnly = readOnly;
|
|
40
43
|
// The identity of `addRules` is not stable, but assume that it is for better UX.
|
|
41
44
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -306,8 +306,8 @@ describe("useFormState", () => {
|
|
|
306
306
|
});
|
|
307
307
|
return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
|
|
308
308
|
jsx_runtime_1.jsx("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }, void 0),
|
|
309
|
-
jsx_runtime_1.jsx("button", { "data-testid": "blurBookOne", onClick: () => form.books.rows[0].title
|
|
310
|
-
jsx_runtime_1.jsx("button", { "data-testid": "blurBookTwo", onClick: () => form.books.rows[1].title
|
|
309
|
+
jsx_runtime_1.jsx("button", { "data-testid": "blurBookOne", onClick: () => focusAndBlur(form.books.rows[0].title) }, void 0),
|
|
310
|
+
jsx_runtime_1.jsx("button", { "data-testid": "blurBookTwo", onClick: () => focusAndBlur(form.books.rows[1].title) }, void 0)] }, void 0)) }, void 0));
|
|
311
311
|
}
|
|
312
312
|
// Given a formState with `onBlur` set
|
|
313
313
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
@@ -472,3 +472,8 @@ const authorWithAddressAndBooksConfig = {
|
|
|
472
472
|
},
|
|
473
473
|
},
|
|
474
474
|
};
|
|
475
|
+
/** Emulates a user focusing and then blurring a field. */
|
|
476
|
+
function focusAndBlur(state) {
|
|
477
|
+
state.focus();
|
|
478
|
+
state.blur();
|
|
479
|
+
}
|