@homebound/form-state 2.22.1 → 2.22.3

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.
@@ -19,6 +19,31 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
19
19
  ((_a = parentCopy[key]) !== null && _a !== void 0 ? _a : []).forEach((copy, i) => {
20
20
  copyMap.set(copy, parentInstance[key][i]);
21
21
  });
22
+ // Given a child POJO (or a copy/clone of a child POJO), return its ObjectState wrapper.
23
+ function getOrCreateChildState(child, opts) {
24
+ let childState = rowMap.get(child);
25
+ // If we're being reverted to our originalValue, i.e. values is actually
26
+ // a list of copies, use the copyMap to recover the non-copy original value
27
+ if (!childState && copyMap.has(child)) {
28
+ childState = rowMap.get(copyMap.get(child));
29
+ }
30
+ // Look for an existing child (requires having an id key configured)
31
+ if (!childState) {
32
+ for (const [, otherState] of rowMap.entries()) {
33
+ if (otherState.isSameEntity(child)) {
34
+ otherState.set(child, opts);
35
+ rowMap.set(child, otherState);
36
+ return otherState;
37
+ }
38
+ }
39
+ }
40
+ // If we didn't have an existing child, just make a new object state
41
+ if (!childState) {
42
+ childState = (0, objectField_1.newObjectState)(config, parentState, list, child, undefined, maybeAutoSave);
43
+ rowMap.set(child, childState);
44
+ }
45
+ return childState;
46
+ }
22
47
  const list = {
23
48
  key: key,
24
49
  // Our fundamental state of wrapped Us
@@ -75,15 +100,7 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
75
100
  (0, utils_1.fail)();
76
101
  // Avoid using `this.value` to avoid registering `_childTick` as a dependency
77
102
  const value = parentInstance[key];
78
- return (value || []).map((child) => {
79
- // Because we're reading from this.value, child will be the proxy version
80
- let childState = rowMap.get(child);
81
- if (!childState) {
82
- childState = (0, objectField_1.newObjectState)(config, parentState, list, child, undefined, maybeAutoSave);
83
- rowMap.set(child, childState);
84
- }
85
- return childState;
86
- });
103
+ return (value || []).map((child) => getOrCreateChildState(child));
87
104
  },
88
105
  // TODO Should this be true when all rows are touched?
89
106
  get touched() {
@@ -144,30 +161,12 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
144
161
  if (this.readOnly && !opts.resetting && !opts.refreshing) {
145
162
  throw new Error(`${String(key)} is currently readOnly`);
146
163
  }
147
- // We should be passed values that are non-proxies.
148
- parentInstance[key] = values.map((value) => {
149
- let childState = rowMap.get(value);
150
- // If we're being reverted to our originalValue, i.e. values is actually
151
- // a list of copies, use the copyMap to recover our child states
152
- if (!childState && copyMap.has(value)) {
153
- childState = rowMap.get(copyMap.get(value));
154
- }
155
- if (!childState) {
156
- // Look for an existing child (requires having an id key configured)
157
- for (const [, otherState] of rowMap.entries()) {
158
- if (otherState.isSameEntity(value)) {
159
- otherState.set(value, opts);
160
- rowMap.set(value, otherState);
161
- return otherState.value;
162
- }
163
- }
164
- // If we didn't have an existing child, just make a new object state
165
- childState = (0, objectField_1.createObjectState)(config, value, { maybeAutoSave });
166
- rowMap.set(value, childState);
167
- }
168
- // Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
169
- return childState.value;
170
- });
164
+ // Given the values, see if we've got existing child states, and use their
165
+ // value if so. I.e. this covers revertChanges doing `set(originalValue)` and
166
+ // passing us cloned rows from the parentCopy, but `getOrCreateChildState` will
167
+ // use the `copyMap` to recover the non-cloned rows, to avoid promoting the clone
168
+ // into a real row.
169
+ parentInstance[key] = values.map((child) => getOrCreateChildState(child, opts).value);
171
170
  // Make sure to tick first so that `setOriginalValue` sees the latest `rows`
172
171
  _tick.value++;
173
172
  // Reset originalCopy so that our dirty checks have the right # of rows.
@@ -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.
@@ -188,12 +190,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
188
190
  // An internal helper method to see if `other` is for "the same entity" as our current row
189
191
  isSameEntity(other) {
190
192
  const idField = getFields(this).find((f) => f._isIdKey);
191
- if (!idField) {
193
+ if (!idField)
192
194
  return false;
193
- }
194
- // If the otherIdValue is undefined, it's a new entity so can't be the same as us
195
+ const ourIdValue = idField.value;
195
196
  const otherIdValue = other[idField.key];
196
- return otherIdValue !== undefined && otherIdValue === this[idField.key].value;
197
+ // If the otherIdValue is undefined, it's a new entity so can't be the same as us
198
+ return otherIdValue !== undefined && otherIdValue === ourIdValue;
197
199
  },
198
200
  };
199
201
  proxy = (0, mobx_1.makeAutoObservable)(obj, {
@@ -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() {
@@ -979,11 +979,7 @@ describe("formState", () => {
979
979
  it("can observe observable objects being mutated directly", () => {
980
980
  // Given a mobx class with a computed
981
981
  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);
982
+ const formState = (0, objectField_1.createObjectState)(authorWithFullName, instance);
987
983
  expect(formState.firstName.value).toEqual("first");
988
984
  expect(formState.fullName.value).toEqual("first last");
989
985
  // And we hook up reactivity to the computed
@@ -999,9 +995,11 @@ describe("formState", () => {
999
995
  expect(formState.fullName.value).toEqual("change last");
1000
996
  // And when the underlying mobx class has a field unset
1001
997
  instance.firstName = undefined;
1002
- // Then our fomr-state computed re-runs again
998
+ // Then our form-state computed re-runs again
1003
999
  expect(numCalcs).toBe(3);
1004
1000
  expect(formState.fullName.value).toEqual("undefined last");
1001
+ // And knows the field should be sent as null
1002
+ expect(formState.changedValue).toEqual({ firstName: null });
1005
1003
  });
1006
1004
  it("can set computeds that have setters", () => {
1007
1005
  const formState = (0, objectField_1.createObjectState)({
@@ -1086,6 +1084,18 @@ describe("formState", () => {
1086
1084
  // because this might trigger creating a throw-away entity
1087
1085
  expect(formState.changedValue).toEqual({ firstName: "f" });
1088
1086
  });
1087
+ it("changedValue skips computeds", () => {
1088
+ // Given a new author that calls full name
1089
+ const formState = (0, objectField_1.createObjectState)(authorWithFullName, new ObservableObject());
1090
+ expect(formState.fullName.value).toEqual("first last");
1091
+ // When the firstName changes
1092
+ formState.firstName.value = "First";
1093
+ // Then the fullName changes
1094
+ expect(formState.fullName.value).toBe("First last");
1095
+ expect(formState.fullName.dirty).toBe(true);
1096
+ // And we don't include it in the changedValue
1097
+ expect(formState.changedValue).toEqual({ firstName: "First" });
1098
+ });
1089
1099
  it("can return only changed list fields", () => {
1090
1100
  // Given an author with some books
1091
1101
  const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
@@ -1526,6 +1536,25 @@ describe("formState", () => {
1526
1536
  expect(a.id._isIdKey).toBe(false);
1527
1537
  expect(a.otherId._isIdKey).toBe(true);
1528
1538
  });
1539
+ it("uses id key to recognize same entities", () => {
1540
+ // With b1 having an id
1541
+ const b1 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, { id: "b:1" });
1542
+ expect(b1.isSameEntity({ id: "b:1" })).toBe(true);
1543
+ expect(b1.isSameEntity({ id: "b:2" })).toBe(false);
1544
+ expect(b1.isSameEntity({ id: undefined })).toBe(false);
1545
+ expect(b1.isSameEntity({})).toBe(false);
1546
+ // With b2 having no id
1547
+ const b2 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, {});
1548
+ expect(b2.isSameEntity({ id: "b:1" })).toBe(false);
1549
+ expect(b2.isSameEntity({ id: undefined })).toBe(false);
1550
+ expect(b2.isSameEntity({})).toBe(false);
1551
+ // With b3 having no id key
1552
+ const b3 = (0, objectField_1.createObjectState)({ title: { type: "value" } }, { title: "b3" });
1553
+ expect(b3.isSameEntity({ title: "b3" })).toBe(false);
1554
+ // With b4 having a different id key
1555
+ const b4 = (0, objectField_1.createObjectState)({ title: { type: "value", isIdKey: true } }, { title: "b4" });
1556
+ expect(b4.isSameEntity({ title: "b4" })).toBe(true);
1557
+ });
1529
1558
  });
1530
1559
  class ObservableObject {
1531
1560
  constructor() {
@@ -1560,6 +1589,11 @@ const authorWithBooksConfig = configBuilders_1.f.config({
1560
1589
  function createAuthorInputState(input, maybeAutoSave) {
1561
1590
  return (0, objectField_1.createObjectState)(authorWithBooksConfig, input, { maybeAutoSave });
1562
1591
  }
1592
+ const authorWithFullName = {
1593
+ firstName: { type: "value" },
1594
+ lastName: { type: "value" },
1595
+ fullName: { type: "value", computed: true },
1596
+ };
1563
1597
  const authorWithAddressConfig = {
1564
1598
  id: { type: "value" },
1565
1599
  firstName: { type: "value" },
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { ObjectConfig } from "./config";
2
2
  export { f } from "./configBuilders";
3
+ export { FragmentField } from "./fields/fragmentField";
3
4
  export { ListFieldState } from "./fields/listField";
4
- export { ObjectState, createObjectState } from "./fields/objectField";
5
+ export { Fragment, ObjectState, createObjectState, fragment } from "./fields/objectField";
5
6
  export { FieldState, ValueAdapter } from "./fields/valueField";
6
7
  export { Rule, required } from "./rules";
7
8
  export { useFormState } from "./useFormState";
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useFormStates = exports.useFormState = exports.required = exports.createObjectState = exports.f = void 0;
3
+ exports.useFormStates = exports.useFormState = exports.required = exports.fragment = exports.createObjectState = exports.f = void 0;
4
4
  var configBuilders_1 = require("./configBuilders");
5
5
  Object.defineProperty(exports, "f", { enumerable: true, get: function () { return configBuilders_1.f; } });
6
6
  var objectField_1 = require("./fields/objectField");
7
7
  Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return objectField_1.createObjectState; } });
8
+ Object.defineProperty(exports, "fragment", { enumerable: true, get: function () { return objectField_1.fragment; } });
8
9
  var rules_1 = require("./rules");
9
10
  Object.defineProperty(exports, "required", { enumerable: true, get: function () { return rules_1.required; } });
10
11
  var useFormState_1 = require("./useFormState");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.22.1",
3
+ "version": "2.22.3",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",