@homebound/form-state 2.2.8 → 2.2.12

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.
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FormStateApp = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const mobx_react_1 = require("mobx-react");
6
- const formState_1 = require("./formState");
6
+ const index_1 = require("./index");
7
7
  function FormStateApp() {
8
- const formState = formState_1.useFormState({
8
+ const formState = index_1.useFormState({
9
9
  config: formConfig,
10
10
  // Simulate getting the initial form state back from a server call
11
11
  init: {
@@ -60,13 +60,13 @@ function FormStateApp() {
60
60
  exports.FormStateApp = FormStateApp;
61
61
  // Configure the fields/behavior for AuthorInput's fields
62
62
  const formConfig = {
63
- firstName: { type: "value", rules: [formState_1.required] },
64
- lastName: { type: "value", rules: [formState_1.required], readOnly: true },
63
+ firstName: { type: "value", rules: [index_1.required] },
64
+ lastName: { type: "value", rules: [index_1.required], readOnly: true },
65
65
  books: {
66
66
  type: "list",
67
67
  rules: [({ value: list }) => ((list || []).length === 0 ? "Empty" : undefined)],
68
68
  config: {
69
- title: { type: "value", rules: [formState_1.required] },
69
+ title: { type: "value", rules: [index_1.required] },
70
70
  },
71
71
  },
72
72
  };
@@ -1,47 +1,3 @@
1
- export declare type UseFormStateOpts<T, I> = {
2
- /** The form configuration, should be a module-level const or useMemo'd. */
3
- config: ObjectConfig<T>;
4
- /**
5
- * Provides the form's initial value.
6
- *
7
- * User's can either:
8
- *
9
- * - Provide no initial value (don't set `init` at all)
10
- * - Provide an initial value that already matches the form type `T` (i.e. set `init: data`)
11
- * - Provide an initial value from an object that _almost_ matches the form type `T`,
12
- * but needs to be mapped from it's input type `I` to the form type
13
- * (i.e. set `init: { input: data, map: (data) => ...}`).
14
- *
15
- * The value of using the 3rd option is that: a) we internally `useMemo` on the identity of the
16
- * `init.input` (i.e. a response from an Apollo hook) and don't require the `map` function
17
- * to have a stable identity, and also b) we will null-check/undefined-check `init.input` and
18
- * only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
19
- * from having to null check within your `init.map` function.
20
- */
21
- init?: T | {
22
- input: I;
23
- map: (input: Exclude<I, null | undefined>) => T;
24
- ifUndefined?: T;
25
- };
26
- /**
27
- * A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
28
- *
29
- * Does not need to be stable/useMemo'd.
30
- */
31
- addRules?: (state: ObjectState<T>) => void;
32
- /** Whether the form should be read only, when changed it won't re-create the whole form. */
33
- readOnly?: boolean;
34
- /**
35
- * Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
36
- *
37
- * Does not need to be stable/useMemo'd.
38
- */
39
- autoSave?: (state: ObjectState<T>) => void;
40
- };
41
- /**
42
- * Creates a formState instance for editing in a form.
43
- */
44
- export declare function useFormState<T, I>(opts: UseFormStateOpts<T, I>): ObjectState<T>;
45
1
  /**
46
2
  * Wraps a given input/on-the-wire type `T` for editing in a form.
47
3
  *
@@ -143,7 +99,7 @@ declare type OmitIf<Base, Condition> = Pick<Base, {
143
99
  [Key in keyof Base]: Base[Key] extends Condition ? never : Key;
144
100
  }[keyof Base]>;
145
101
  /** Field configuration for primitive values, i.e. strings/numbers/Dates/user-defined types. */
146
- declare type ValueFieldConfig<T, V> = {
102
+ export declare type ValueFieldConfig<T, V> = {
147
103
  type: "value";
148
104
  rules?: Rule<T, V | null | undefined>[];
149
105
  /**
@@ -168,7 +124,7 @@ declare type ValueFieldConfig<T, V> = {
168
124
  readOnly?: boolean;
169
125
  };
170
126
  /** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
171
- declare type ListFieldConfig<T, U> = {
127
+ export declare type ListFieldConfig<T, U> = {
172
128
  type: "list";
173
129
  /** Rules that can run on the full list of children. */
174
130
  rules?: Rule<T, readonly ObjectState<U>[]>[];
@@ -187,7 +143,7 @@ declare type ListFieldConfig<T, U> = {
187
143
  */
188
144
  update?: "exhaustive" | "incremental";
189
145
  };
190
- declare type ObjectFieldConfig<U> = {
146
+ export declare type ObjectFieldConfig<U> = {
191
147
  type: "object";
192
148
  /** Config for the child's form state, i.e. each book. */
193
149
  config: ObjectConfig<U>;
@@ -202,18 +158,6 @@ declare type ObjectFieldConfig<U> = {
202
158
  export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
203
159
  onBlur?: () => void;
204
160
  }): ObjectState<T>;
205
- /**
206
- * Picks ony fields out of `instance` that are in the `formConfig`.
207
- *
208
- * This is useful for creating a form input from a GraphQL query result,
209
- * but ignoring extra data in the query that we don't want/need in the form.
210
- *
211
- * (Especially because the form.value will become our mutation's input type
212
- * that goes on the wire, we don't want superfluous data in it.)
213
- */
214
- export declare function pickFields<T, I>(formConfig: ObjectConfig<T>, instance: I): {
215
- [K in keyof T]: K extends keyof I ? I[K] : never;
216
- };
217
161
  declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
218
162
  toJSON(): any;
219
163
  };
package/dist/formState.js CHANGED
@@ -3,64 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.pickFields = exports.createObjectState = exports.required = exports.useFormState = void 0;
6
+ exports.createObjectState = exports.required = void 0;
7
7
  const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
8
- const is_plain_object_1 = __importDefault(require("is-plain-object"));
8
+ const is_plain_object_1 = require("is-plain-object");
9
9
  const mobx_1 = require("mobx");
10
- const react_1 = require("react");
11
10
  const utils_1 = require("./utils");
12
- /**
13
- * Creates a formState instance for editing in a form.
14
- */
15
- function useFormState(opts) {
16
- const { config, init, addRules, readOnly = false, autoSave } = opts;
17
- // Use a ref so our memo'ized `onBlur` always see the latest value
18
- const autoSaveRef = react_1.useRef(autoSave);
19
- autoSaveRef.current = autoSave;
20
- const firstRunRef = react_1.useRef(true);
21
- // This is a little weird, but we need to know ahead of time, before the form useMemo, if we're working with classes/mobx proxies
22
- const [firstInitValue] = react_1.useState(() => initValue(config, init));
23
- const isWrappingMobxProxy = !is_plain_object_1.default(firstInitValue);
24
- // If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
25
- const dep = init && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
26
- const form = react_1.useMemo(() => {
27
- function onBlur() {
28
- // Don't use canSave() because we don't want to set touched for all of the field
29
- if (autoSaveRef.current && form.dirty && form.valid) {
30
- autoSaveRef.current(form);
31
- }
32
- }
33
- const value = firstRunRef.current ? firstInitValue : initValue(config, init);
34
- const form = createObjectState(config, value, { onBlur });
35
- form.readOnly = readOnly;
36
- // The identity of `addRules` is not stable, but assume that it is for better UX.
37
- // eslint-disable-next-line react-hooks/exhaustive-deps
38
- (addRules || (() => { }))(form);
39
- firstRunRef.current = true;
40
- return form;
41
- },
42
- // For this useMemo, we (almost) never re-run so that we can have a stable `form` identity across query refreshes.
43
- // eslint-disable-next-line react-hooks/exhaustive-deps
44
- [config, ...(isWrappingMobxProxy ? dep : [])]);
45
- // We use useEffect so that any mutations to the proxies, which will call `setState`s on any observers to
46
- // queue their components' render), don't happen during our render, per https://fb.me/setstate-in-render.
47
- react_1.useEffect(() => {
48
- // Ignore the 1st run b/c our 1st useMemo already initialized `form` with the current `init` value.
49
- // Also for mobx proxies, we recreate a new form-state every time init changes, so that our
50
- // fields fundamentally pointing to the right proxy. So just defer to the ^ useMemo.
51
- if (firstRunRef.current || isWrappingMobxProxy) {
52
- firstRunRef.current = false;
53
- return;
54
- }
55
- form.set(initValue(config, init), { refreshing: true });
56
- }, [form, ...dep]);
57
- // Use useEffect so that we don't touch the form.init proxy during a render
58
- react_1.useEffect(() => {
59
- form.readOnly = readOnly;
60
- }, [form, readOnly]);
61
- return form;
62
- }
63
- exports.useFormState = useFormState;
64
11
  /** A rule that validates `value` is not `undefined`, `null`, or empty string. */
65
12
  // We pre-emptively make this a mobx action so that it's identity doesn't change when proxy-ified
66
13
  // and breaks our ability to do `rules.some(r => r === required)`.
@@ -241,7 +188,7 @@ function newObjectState(config, parentState, instance, key, onBlur) {
241
188
  function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, onBlur) {
242
189
  // keep a copy here for reference equality
243
190
  const value = parentInstance[key];
244
- let _originalValue = value === null ? undefined : is_plain_object_1.default(value) ? mobx_1.toJS(value) : value;
191
+ let _originalValue = value === null ? undefined : is_plain_object_1.isPlainObject(value) ? mobx_1.toJS(value) : value;
245
192
  // Because we read/write the value directly back into parentInstance[key],
246
193
  // which itself is not a proxy, we use this as our "value changed" trigger.
247
194
  const _tick = mobx_1.observable({ value: 1 });
@@ -314,11 +261,8 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
314
261
  },
315
262
  blur() {
316
263
  // Now that the user is done editing the field, we sneak in some trim logic
317
- if (typeof this.value === "string") {
318
- this.set(this.value.trim());
319
- if (this.value === "") {
320
- this.set(undefined);
321
- }
264
+ if (this._focused) {
265
+ this.maybeTrim();
322
266
  }
323
267
  this._focused = false;
324
268
  // touched is readonly, but we're allowed to change it
@@ -346,11 +290,17 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
346
290
  const coerceEmptyList = value && value instanceof Array && value.length === 0 && isEmpty(this.originalValue);
347
291
  const newValue = keepNull ? null : isEmpty(value) || coerceEmptyList ? undefined : value;
348
292
  // Set the value on our parent object
293
+ const changed = !areEqual(newValue, this.value);
349
294
  parentInstance[key] = newValue;
350
295
  _tick.value++;
351
296
  if (opts.refreshing) {
352
297
  this.originalValue = newValue;
353
298
  }
299
+ // If we're being set programmatically, i.e. we don't currently have focus,
300
+ // call blur to trigger any auto-saves.
301
+ if (!this._focused && !opts.refreshing && !opts.resetting && this.dirty && changed) {
302
+ this.blur();
303
+ }
354
304
  },
355
305
  reset() {
356
306
  if (!computed) {
@@ -359,7 +309,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
359
309
  this.touched = false;
360
310
  },
361
311
  save() {
362
- if (is_plain_object_1.default(this.originalValue)) {
312
+ if (is_plain_object_1.isPlainObject(this.originalValue)) {
363
313
  this.originalValue = mobx_1.toJS(this.value);
364
314
  }
365
315
  else {
@@ -375,6 +325,17 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
375
325
  _originalValue = v;
376
326
  _originalValueTick.value++;
377
327
  },
328
+ maybeTrim() {
329
+ if (typeof this.value === "string") {
330
+ let newValue = this.value.trim();
331
+ if (newValue === "") {
332
+ newValue = undefined;
333
+ }
334
+ if (newValue !== this.value) {
335
+ this.set(newValue);
336
+ }
337
+ }
338
+ },
378
339
  };
379
340
  return field;
380
341
  }
@@ -556,7 +517,10 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
556
517
  _tick.value++;
557
518
  },
558
519
  };
559
- return list;
520
+ return mobx_1.makeAutoObservable(list, {
521
+ // See other makeAutoObservable comment
522
+ value: mobx_1.computed({ equals: () => false }),
523
+ });
560
524
  }
561
525
  function isNotUndefined(value) {
562
526
  return value !== undefined;
@@ -574,7 +538,7 @@ function isEmpty(value) {
574
538
  *
575
539
  */
576
540
  function areEqual(a, b) {
577
- if (is_plain_object_1.default(a)) {
541
+ if (is_plain_object_1.isPlainObject(a)) {
578
542
  return fast_deep_equal_1.default(mobx_1.toJS(a), mobx_1.toJS(b));
579
543
  }
580
544
  if (hasToJSON(a) || hasToJSON(b)) {
@@ -590,59 +554,3 @@ function areEqual(a, b) {
590
554
  function hasToJSON(o) {
591
555
  return !!(o && typeof o === "object" && "toJSON" in o);
592
556
  }
593
- /**
594
- * Picks ony fields out of `instance` that are in the `formConfig`.
595
- *
596
- * This is useful for creating a form input from a GraphQL query result,
597
- * but ignoring extra data in the query that we don't want/need in the form.
598
- *
599
- * (Especially because the form.value will become our mutation's input type
600
- * that goes on the wire, we don't want superfluous data in it.)
601
- */
602
- function pickFields(formConfig, instance) {
603
- // If the caller is using classes, i.e. with their own custom observable behavior, then just use as-is
604
- if (!is_plain_object_1.default(instance)) {
605
- return instance;
606
- }
607
- return Object.fromEntries(Object.entries(formConfig).map(([key, _keyConfig]) => {
608
- const keyConfig = _keyConfig;
609
- const value = instance[key];
610
- if (keyConfig.type === "object") {
611
- if (value) {
612
- return [key, pickFields(keyConfig.config, value)];
613
- }
614
- else {
615
- return [key, value];
616
- }
617
- }
618
- else if (keyConfig.type === "list") {
619
- if (mobx_1.isObservable(value)) {
620
- // If we hit an observable array, leave it as the existing proxy so the our
621
- // ListFieldState will react to changes in the original array.
622
- return [key, value];
623
- }
624
- else if (value) {
625
- return [key, value.map((u) => pickFields(keyConfig.config, u))];
626
- }
627
- else {
628
- return [key, value];
629
- }
630
- }
631
- else if (keyConfig.type === "value") {
632
- return [key, value];
633
- }
634
- else {
635
- return utils_1.assertNever(keyConfig);
636
- }
637
- }));
638
- }
639
- exports.pickFields = pickFields;
640
- /** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
641
- function initValue(config, init) {
642
- const initValue = init && "input" in init && "map" in init
643
- ? init.input
644
- ? init.map(init.input)
645
- : init.ifUndefined || {}
646
- : init || {};
647
- return pickFields(config, initValue);
648
- }