@homebound/form-state 2.22.2 → 2.22.4

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.
@@ -31,7 +31,9 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
31
31
  if (!childState) {
32
32
  for (const [, otherState] of rowMap.entries()) {
33
33
  if (otherState.isSameEntity(child)) {
34
- otherState.set(child, opts);
34
+ // If we're being called from `list.rows`, we should be careful to not trigger mutations
35
+ if (!(opts === null || opts === void 0 ? void 0 : opts.skipSet))
36
+ otherState.set(child, opts);
35
37
  rowMap.set(child, otherState);
36
38
  return otherState;
37
39
  }
@@ -100,7 +102,7 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
100
102
  (0, utils_1.fail)();
101
103
  // Avoid using `this.value` to avoid registering `_childTick` as a dependency
102
104
  const value = parentInstance[key];
103
- return (value || []).map((child) => getOrCreateChildState(child));
105
+ return (value || []).map((child) => getOrCreateChildState(child, { skipSet: true }));
104
106
  },
105
107
  // TODO Should this be true when all rows are touched?
106
108
  get touched() {
@@ -163,6 +163,8 @@ function newObjectState(config, parentState, parentListState, instance, key, may
163
163
  get changedValue() {
164
164
  const result = {};
165
165
  getFields(this).forEach((f) => {
166
+ if (f._computed)
167
+ return;
166
168
  if (f.dirty ||
167
169
  // If the caller used useFormState.ifUndefined to provide some default values, then those keys may not
168
170
  // look dirty, but if we're new we should include them anyway.
@@ -18,6 +18,8 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
18
18
  _readOnly: readOnly || false,
19
19
  _loading: false,
20
20
  _focused: false,
21
+ // Expose so computed can be skipped in changedValue
22
+ _computed: computed,
21
23
  _isIdKey: isIdKey,
22
24
  _isDeleteKey: isDeleteKey,
23
25
  _isReadOnlyKey: isReadOnlyKey,
@@ -54,6 +56,12 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
54
56
  },
55
57
  // For primitive fields, the changed value is just the value.
56
58
  get changedValue() {
59
+ // Usually if we see a field being unset, we set `parent[key] = null`, but if we're wrapping
60
+ // a mobx observable object, it might have changed to `undefined` without us being able to
61
+ // tell it to be `null` (and potentially break the type contract of `string | undefined`).
62
+ // So detect un-set-ness here and return `null`.
63
+ if (this.value === undefined && this.originalValue !== undefined)
64
+ return null;
57
65
  return this.value;
58
66
  },
59
67
  get valid() {
@@ -442,6 +442,20 @@ describe("formState", () => {
442
442
  a1.books.remove(0);
443
443
  expect(a1.dirty).toBeFalsy();
444
444
  });
445
+ it("handles list fields with duplicate ids", () => {
446
+ // Given an author that incorrectly has duplicate books
447
+ const a1 = createAuthorInputState({
448
+ books: [
449
+ { id: "b:1", title: "b1" },
450
+ { id: "b:1", title: "b2" },
451
+ ],
452
+ });
453
+ // Then it didn't blow up
454
+ expect(a1.books.dirty).toBeFalsy();
455
+ // And the 1st book won
456
+ expect(a1.books.rows[0].title.value).toBe("b1");
457
+ expect(a1.books.rows[1].title.value).toBe("b1");
458
+ });
445
459
  it("calls maybeAutoSave when adding or removing to a list", () => {
446
460
  const maybeAutoSave = jest.fn();
447
461
  const a1 = createAuthorInputState({ books: [] }, maybeAutoSave);
@@ -979,11 +993,7 @@ describe("formState", () => {
979
993
  it("can observe observable objects being mutated directly", () => {
980
994
  // Given a mobx class with a computed
981
995
  const instance = new ObservableObject();
982
- const formState = (0, objectField_1.createObjectState)({
983
- firstName: { type: "value" },
984
- lastName: { type: "value" },
985
- fullName: { type: "value", computed: true },
986
- }, instance);
996
+ const formState = (0, objectField_1.createObjectState)(authorWithFullName, instance);
987
997
  expect(formState.firstName.value).toEqual("first");
988
998
  expect(formState.fullName.value).toEqual("first last");
989
999
  // And we hook up reactivity to the computed
@@ -999,9 +1009,11 @@ describe("formState", () => {
999
1009
  expect(formState.fullName.value).toEqual("change last");
1000
1010
  // And when the underlying mobx class has a field unset
1001
1011
  instance.firstName = undefined;
1002
- // Then our fomr-state computed re-runs again
1012
+ // Then our form-state computed re-runs again
1003
1013
  expect(numCalcs).toBe(3);
1004
1014
  expect(formState.fullName.value).toEqual("undefined last");
1015
+ // And knows the field should be sent as null
1016
+ expect(formState.changedValue).toEqual({ firstName: null });
1005
1017
  });
1006
1018
  it("can set computeds that have setters", () => {
1007
1019
  const formState = (0, objectField_1.createObjectState)({
@@ -1086,6 +1098,18 @@ describe("formState", () => {
1086
1098
  // because this might trigger creating a throw-away entity
1087
1099
  expect(formState.changedValue).toEqual({ firstName: "f" });
1088
1100
  });
1101
+ it("changedValue skips computeds", () => {
1102
+ // Given a new author that calls full name
1103
+ const formState = (0, objectField_1.createObjectState)(authorWithFullName, new ObservableObject());
1104
+ expect(formState.fullName.value).toEqual("first last");
1105
+ // When the firstName changes
1106
+ formState.firstName.value = "First";
1107
+ // Then the fullName changes
1108
+ expect(formState.fullName.value).toBe("First last");
1109
+ expect(formState.fullName.dirty).toBe(true);
1110
+ // And we don't include it in the changedValue
1111
+ expect(formState.changedValue).toEqual({ firstName: "First" });
1112
+ });
1089
1113
  it("can return only changed list fields", () => {
1090
1114
  // Given an author with some books
1091
1115
  const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
@@ -1579,6 +1603,11 @@ const authorWithBooksConfig = configBuilders_1.f.config({
1579
1603
  function createAuthorInputState(input, maybeAutoSave) {
1580
1604
  return (0, objectField_1.createObjectState)(authorWithBooksConfig, input, { maybeAutoSave });
1581
1605
  }
1606
+ const authorWithFullName = {
1607
+ firstName: { type: "value" },
1608
+ lastName: { type: "value" },
1609
+ fullName: { type: "value", computed: true },
1610
+ };
1582
1611
  const authorWithAddressConfig = {
1583
1612
  id: { type: "value" },
1584
1613
  firstName: { type: "value" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.22.2",
3
+ "version": "2.22.4",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",