@homebound/form-state 2.9.1 → 2.10.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.js CHANGED
@@ -24,10 +24,10 @@ exports.required = mobx_1.action(({ value: v }) => {
24
24
  */
25
25
  function createObjectState(config, instance, opts = {}) {
26
26
  const noop = () => { };
27
- return newObjectState(config, undefined, instance, undefined, opts.maybeAutoSave || noop);
27
+ return newObjectState(config, undefined, undefined, instance, undefined, opts.maybeAutoSave || noop);
28
28
  }
29
29
  exports.createObjectState = createObjectState;
30
- function newObjectState(config, parentState, instance, key, maybeAutoSave) {
30
+ function newObjectState(config, parentState, parentListState, instance, key, maybeAutoSave) {
31
31
  // This is what we return, but we only know it's value until we call `observable`, so
32
32
  // we create a mutable variable to capture it so that we can create fields/call their
33
33
  // constructors and give them a way to access it later.
@@ -52,7 +52,7 @@ function newObjectState(config, parentState, instance, key, maybeAutoSave) {
52
52
  if (!instance[key]) {
53
53
  instance[key] = {};
54
54
  }
55
- field = newObjectState(config.config, getObjectState, instance[key], key, maybeAutoSave);
55
+ field = newObjectState(config.config, getObjectState, undefined, instance[key], key, maybeAutoSave);
56
56
  }
57
57
  else {
58
58
  throw new Error("Unsupported");
@@ -93,12 +93,13 @@ function newObjectState(config, parentState, instance, key, maybeAutoSave) {
93
93
  getFields(this).forEach((f) => (f.touched = touched));
94
94
  },
95
95
  get readOnly() {
96
- return this._readOnly || this._considerReadOnly();
96
+ return (this._readOnly ||
97
+ this._considerReadOnly() ||
98
+ (parentState && parentState().readOnly) ||
99
+ !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.readOnly));
97
100
  },
98
101
  set readOnly(readOnly) {
99
102
  this._readOnly = readOnly;
100
- // Use this.readOnly so we can _considerReadOnly
101
- getFields(this).forEach((f) => (f.readOnly = this.readOnly));
102
103
  },
103
104
  get valid() {
104
105
  return getFields(this).every((f) => f.valid);
@@ -197,8 +198,6 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
197
198
  const field = {
198
199
  key,
199
200
  touched: false,
200
- /** Configuration readOnly state. Mostly used to determine original state. */
201
- _configReadOnly: readOnly,
202
201
  /** Current readOnly value. */
203
202
  _readOnly: readOnly || false,
204
203
  _focused: false,
@@ -219,25 +218,13 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
219
218
  get dirty() {
220
219
  return !areEqual(this.originalValue, this.value);
221
220
  },
221
+ /** Returns whether this field is readOnly, although if our parent is readOnly then it trumps. */
222
222
  get readOnly() {
223
- return this._readOnly;
223
+ return parentState().readOnly || this._readOnly;
224
224
  },
225
- /**
226
- * Field readOnly is only opinionated when set.
227
- * - When set to true from FormObject, accept change since true is higher priority.
228
- * - When set to false from FormObject, use original (`_configReadOnly`) value.
229
- */
225
+ /** Sets the field readOnly, but `v` will still be or-d with the parent's readOnly state. */
230
226
  set readOnly(v) {
231
- if (this._configReadOnly === undefined) {
232
- this._readOnly = v;
233
- return;
234
- }
235
- if (v) {
236
- this._readOnly = v;
237
- }
238
- else {
239
- this._readOnly = this._configReadOnly || false;
240
- }
227
+ this._readOnly = v;
241
228
  },
242
229
  // For primitive fields, the changed value is just the value.
243
230
  get changedValue() {
@@ -356,11 +343,10 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
356
343
  _focused: false,
357
344
  _readOnly: false,
358
345
  get readOnly() {
359
- return this._readOnly;
346
+ return this._readOnly || parentState().readOnly;
360
347
  },
361
348
  set readOnly(readOnly) {
362
349
  this._readOnly = readOnly;
363
- this.rows.forEach((r) => (r.readOnly = readOnly));
364
350
  },
365
351
  set value(v) {
366
352
  this.set(v);
@@ -401,7 +387,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
401
387
  // Because we're reading from this.value, child will be the proxy version
402
388
  let childState = rowMap.get(child);
403
389
  if (!childState) {
404
- childState = newObjectState(config, parentState, child, undefined, maybeAutoSave);
390
+ childState = newObjectState(config, parentState, list, child, undefined, maybeAutoSave);
405
391
  rowMap.set(child, childState);
406
392
  }
407
393
  return childState;
@@ -686,6 +686,42 @@ describe("formState", () => {
686
686
  // Then expect form
687
687
  expect(formState.title.readOnly).toBeTruthy();
688
688
  });
689
+ it("has an object-level readOnly=true override field-level readOnly=false", () => {
690
+ // Given a top-level object
691
+ const a1 = createAuthorInputState({
692
+ firstName: "a1",
693
+ lastName: "aL1",
694
+ books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
695
+ });
696
+ // And it is read-only
697
+ a1.readOnly = true;
698
+ // Then it's fields are read-only
699
+ const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
700
+ fields.forEach((f) => expect(f.readOnly).toBeTruthy());
701
+ // And even if a specific field tries to _not_ be read-only,
702
+ // i.e. due to a more granular business rule that happens to
703
+ // be allowed right now
704
+ a1.firstName.readOnly = false;
705
+ // Then the field-level rule is ignored, and it's still treated as read-only
706
+ expect(a1.firstName.readOnly).toBeTruthy();
707
+ });
708
+ it("has an field-level readOnly=true override object-level readOnly=false", () => {
709
+ // Given a top-level object
710
+ const a1 = createAuthorInputState({
711
+ firstName: "a1",
712
+ lastName: "aL1",
713
+ books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
714
+ });
715
+ // And the top-level form is explicitly set to read-only=false
716
+ a1.readOnly = false;
717
+ // Then it's fields are not read-only
718
+ const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
719
+ fields.forEach((f) => expect(f.readOnly).toBeFalsy());
720
+ // But when one of the fields opts in to readOnly
721
+ a1.firstName.readOnly = true;
722
+ // Then the field-level rule is respected
723
+ expect(a1.firstName.readOnly).toBeTruthy();
724
+ });
689
725
  it("canSave returns dirty and touches", () => {
690
726
  const a1 = formState_1.createObjectState({
691
727
  firstName: { type: "value", rules: [formState_1.required] },
@@ -34,8 +34,15 @@ declare type UseFormStatesOpts<T, I> = {
34
34
  * Does not need to be stable/useMemo'd.
35
35
  */
36
36
  map?: (input: Exclude<I, null | undefined>) => T;
37
+ /**
38
+ * Sets all `ObjectState`s to readOnly.
39
+ */
40
+ readOnly?: boolean;
37
41
  };
38
- export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): {
39
- getFormState: (input: I) => ObjectState<T>;
42
+ declare type UseFormStatesHook<T, I> = {
43
+ getFormState: (input: I, opts?: {
44
+ readOnly?: boolean;
45
+ }) => ObjectState<T>;
40
46
  };
47
+ export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): UseFormStatesHook<T, I>;
41
48
  export {};
@@ -5,7 +5,7 @@ const react_1 = require("react");
5
5
  const formState_1 = require("./formState");
6
6
  const utils_1 = require("./utils");
7
7
  function useFormStates(opts) {
8
- const { config, autoSave, getId, map, addRules } = opts;
8
+ const { config, autoSave, getId, map, addRules, readOnly = false } = opts;
9
9
  const objectStateCache = react_1.useMemo(() => ({}),
10
10
  // Force resetting the cache if config changes
11
11
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -15,7 +15,10 @@ function useFormStates(opts) {
15
15
  // Use a ref so our memo'ized `autoSave` always see the latest value
16
16
  const autoSaveRef = react_1.useRef(autoSave);
17
17
  autoSaveRef.current = autoSave;
18
- const getFormState = react_1.useCallback((input) => {
18
+ // Use a ref b/c we're memod
19
+ const readOnlyRef = react_1.useRef(readOnly);
20
+ readOnlyRef.current = readOnly;
21
+ const getFormState = react_1.useCallback((input, opts = {}) => {
19
22
  const existing = objectStateCache[getId(input)];
20
23
  let form = existing === null || existing === void 0 ? void 0 : existing[0];
21
24
  async function maybeAutoSave(form) {
@@ -59,6 +62,7 @@ function useFormStates(opts) {
59
62
  form.set(utils_1.initValue(config, map ? { map, input } : input), { refreshing: true });
60
63
  existing[1] = input;
61
64
  }
65
+ form.readOnly = readOnlyRef.current || !!opts.readOnly;
62
66
  return form;
63
67
  },
64
68
  // Allow the user to not stable-ize getId, map, addRules, and autoSave
@@ -156,11 +156,6 @@ describe("useFormStates", () => {
156
156
  // Given a user wants to use auto save
157
157
  const autoSave = jest.fn();
158
158
  function TestComponent() {
159
- const config = {
160
- id: { type: "value" },
161
- firstName: { type: "value" },
162
- lastName: { type: "value" },
163
- };
164
159
  const { getFormState } = useFormStates_1.useFormStates({
165
160
  config,
166
161
  getId: (o) => o.id,
@@ -184,7 +179,51 @@ describe("useFormStates", () => {
184
179
  expect(autoSave).toHaveBeenCalledTimes(1);
185
180
  expect(autoSave).toHaveBeenCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
186
181
  });
182
+ it("can set readOnly via the hook opt", async () => {
183
+ // Given a test component
184
+ function TestComponent({ readOnly }) {
185
+ const { getFormState } = useFormStates_1.useFormStates({
186
+ config,
187
+ getId: (o) => o.id,
188
+ // And it passes readOnly directly to useFormStates
189
+ readOnly,
190
+ });
191
+ return jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0);
192
+ }
193
+ // When we render
194
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { readOnly: true }, void 0));
195
+ // Then it's read only
196
+ expect(r.firstName()).toHaveAttribute("data-readonly", "true");
197
+ // And when we rerender
198
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { readOnly: false }, void 0));
199
+ // Then it's not read only
200
+ expect(r.firstName()).toHaveAttribute("data-readonly", "false");
201
+ });
202
+ it("can set readOnly via the getFormState function", async () => {
203
+ // Given a test component
204
+ function TestComponent({ readOnly }) {
205
+ const { getFormState } = useFormStates_1.useFormStates({
206
+ config,
207
+ getId: (o) => o.id,
208
+ });
209
+ // And it passes readOnly directly to getFormState
210
+ return jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }, { readOnly }) }, void 0);
211
+ }
212
+ // When we render
213
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { readOnly: true }, void 0));
214
+ // Then it's read only
215
+ expect(r.firstName()).toHaveAttribute("data-readonly", "true");
216
+ // And when we rerender
217
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { readOnly: false }, void 0));
218
+ // Then it's not read only
219
+ expect(r.firstName()).toHaveAttribute("data-readonly", "false");
220
+ });
187
221
  });
222
+ const config = {
223
+ id: { type: "value" },
224
+ firstName: { type: "value" },
225
+ lastName: { type: "value" },
226
+ };
188
227
  function ChildComponent({ os }) {
189
- return jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: os.firstName.value }), void 0);
228
+ return (jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName", "data-readonly": os.firstName.readOnly }, { children: os.firstName.value }), void 0));
190
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.9.1",
3
+ "version": "2.10.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",