@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 +13 -27
- package/dist/formState.test.js +36 -0
- package/dist/useFormStates.d.ts +9 -2
- package/dist/useFormStates.js +6 -2
- package/dist/useFormStates.test.js +45 -6
- package/package.json +1 -1
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 ||
|
|
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
|
-
|
|
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;
|
package/dist/formState.test.js
CHANGED
|
@@ -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] },
|
package/dist/useFormStates.d.ts
CHANGED
|
@@ -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
|
-
|
|
39
|
-
getFormState: (input: I
|
|
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 {};
|
package/dist/useFormStates.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|