@homebound/form-state 2.33.0 → 3.0.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/index.d.ts +437 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1109 -14
- package/dist/index.js.map +1 -0
- package/package.json +32 -35
- package/dist/FormStateApp.d.ts +0 -5
- package/dist/FormStateApp.js +0 -54
- package/dist/FormStateApp.test.d.ts +0 -1
- package/dist/FormStateApp.test.js +0 -26
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -2
- package/dist/configBuilders.d.ts +0 -67
- package/dist/configBuilders.js +0 -115
- package/dist/configBuilders.test.d.ts +0 -1
- package/dist/configBuilders.test.js +0 -65
- package/dist/fields/deepEquals.d.ts +0 -7
- package/dist/fields/deepEquals.js +0 -53
- package/dist/fields/fragmentField.d.ts +0 -4
- package/dist/fields/fragmentField.js +0 -58
- package/dist/fields/listField.d.ts +0 -11
- package/dist/fields/listField.js +0 -355
- package/dist/fields/objectField.d.ts +0 -63
- package/dist/fields/objectField.js +0 -277
- package/dist/fields/valueField.d.ts +0 -81
- package/dist/fields/valueField.js +0 -244
- package/dist/formState.stories.d.ts +0 -4
- package/dist/formState.stories.js +0 -12
- package/dist/formState.test.d.ts +0 -11
- package/dist/formState.test.js +0 -2109
- package/dist/formStateDomain.d.ts +0 -47
- package/dist/formStateDomain.js +0 -28
- package/dist/mobx-behavior.test.d.ts +0 -1
- package/dist/mobx-behavior.test.js +0 -51
- package/dist/proxies.d.ts +0 -5
- package/dist/proxies.js +0 -20
- package/dist/rules.d.ts +0 -10
- package/dist/rules.js +0 -11
- package/dist/setupTests.d.ts +0 -1
- package/dist/setupTests.js +0 -15
- package/dist/stackoverlfow.d.ts +0 -17
- package/dist/stackoverlfow.js +0 -17
- package/dist/temporal.d.ts +0 -6
- package/dist/temporal.js +0 -34
- package/dist/temporal.test.d.ts +0 -1
- package/dist/temporal.test.js +0 -97
- package/dist/useFormState.d.ts +0 -82
- package/dist/useFormState.js +0 -158
- package/dist/useFormState.test.d.ts +0 -1
- package/dist/useFormState.test.js +0 -787
- package/dist/useFormStates.d.ts +0 -68
- package/dist/useFormStates.js +0 -101
- package/dist/useFormStates.test.d.ts +0 -1
- package/dist/useFormStates.test.js +0 -223
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -198
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -102
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
export declare const jan1: Date;
|
|
2
|
-
export declare const jan2: Date;
|
|
3
|
-
export declare const dd100: DeweyDecimalClassification;
|
|
4
|
-
export declare const dd200: DeweyDecimalClassification;
|
|
5
|
-
export declare enum Color {
|
|
6
|
-
Red = "RED",
|
|
7
|
-
Blue = "BLUE",
|
|
8
|
-
Green = "GREEN"
|
|
9
|
-
}
|
|
10
|
-
export interface AuthorInput {
|
|
11
|
-
id?: string | null;
|
|
12
|
-
otherId?: string | null;
|
|
13
|
-
firstName?: string | null;
|
|
14
|
-
lastName?: string | null;
|
|
15
|
-
birthday?: Date | null;
|
|
16
|
-
books?: BookInput[] | null;
|
|
17
|
-
address?: AuthorAddress | null;
|
|
18
|
-
favoriteColors?: Color[] | null;
|
|
19
|
-
}
|
|
20
|
-
export interface AuthorAddress {
|
|
21
|
-
id?: string | null;
|
|
22
|
-
street?: string | null;
|
|
23
|
-
city?: string | null;
|
|
24
|
-
}
|
|
25
|
-
export interface BookInput {
|
|
26
|
-
id?: string | null | undefined;
|
|
27
|
-
title?: string | null | undefined;
|
|
28
|
-
classification?: DeweyDecimalClassification;
|
|
29
|
-
delete?: boolean | null | undefined;
|
|
30
|
-
isPublished?: boolean;
|
|
31
|
-
op?: "include" | "delete" | "remove";
|
|
32
|
-
reviews?: ReviewInput[];
|
|
33
|
-
}
|
|
34
|
-
export interface ReviewInput {
|
|
35
|
-
id?: string | null | undefined;
|
|
36
|
-
rating?: number | null | undefined;
|
|
37
|
-
}
|
|
38
|
-
export interface DeweyDecimalClassification {
|
|
39
|
-
number: string;
|
|
40
|
-
category: string;
|
|
41
|
-
}
|
|
42
|
-
export declare class DateOnly {
|
|
43
|
-
private readonly date;
|
|
44
|
-
constructor(date: Date);
|
|
45
|
-
toString(): string;
|
|
46
|
-
toJSON(): string;
|
|
47
|
-
}
|
package/dist/formStateDomain.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Pretend domain objects for editing in the form, i.e. as generated
|
|
3
|
-
// by a GraphQL schema for a `saveAuthor` mutation that takes an author
|
|
4
|
-
// plus the author's books.
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DateOnly = exports.Color = exports.dd200 = exports.dd100 = exports.jan2 = exports.jan1 = void 0;
|
|
7
|
-
exports.jan1 = new Date(2020, 0, 1);
|
|
8
|
-
exports.jan2 = new Date(2020, 0, 2);
|
|
9
|
-
exports.dd100 = { number: "100", category: "Philosophy" };
|
|
10
|
-
exports.dd200 = { number: "200", category: "Religion" };
|
|
11
|
-
var Color;
|
|
12
|
-
(function (Color) {
|
|
13
|
-
Color["Red"] = "RED";
|
|
14
|
-
Color["Blue"] = "BLUE";
|
|
15
|
-
Color["Green"] = "GREEN";
|
|
16
|
-
})(Color || (exports.Color = Color = {}));
|
|
17
|
-
class DateOnly {
|
|
18
|
-
constructor(date) {
|
|
19
|
-
this.date = date;
|
|
20
|
-
}
|
|
21
|
-
toString() {
|
|
22
|
-
return this.date.toISOString().split("T")[0];
|
|
23
|
-
}
|
|
24
|
-
toJSON() {
|
|
25
|
-
return this.toString();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
exports.DateOnly = DateOnly;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const mobx_1 = require("mobx");
|
|
4
|
-
const formState_test_1 = require("./formState.test");
|
|
5
|
-
describe("mobx behavior", () => {
|
|
6
|
-
it("mobx batches if in actions", () => {
|
|
7
|
-
const a = (0, mobx_1.observable)({ name: "a1" });
|
|
8
|
-
const b = (0, mobx_1.observable)({ name: "b1" });
|
|
9
|
-
const map = new mobx_1.ObservableMap();
|
|
10
|
-
map.set("a", a);
|
|
11
|
-
map.set("b", b);
|
|
12
|
-
let runs = 0;
|
|
13
|
-
(0, mobx_1.autorun)(() => {
|
|
14
|
-
noop([...map.values()].filter((v) => v.name.includes("1")));
|
|
15
|
-
runs++;
|
|
16
|
-
});
|
|
17
|
-
expect(runs).toBe(1);
|
|
18
|
-
// When we change both a and b
|
|
19
|
-
(0, mobx_1.runInAction)(() => {
|
|
20
|
-
a.name = "a11";
|
|
21
|
-
b.name = "b2";
|
|
22
|
-
});
|
|
23
|
-
// Then the reaction waited and only ran once
|
|
24
|
-
expect(runs).toBe(2);
|
|
25
|
-
});
|
|
26
|
-
it("mobx can watch undefined keys", () => {
|
|
27
|
-
const a = new formState_test_1.ObservableObject();
|
|
28
|
-
const b = new formState_test_1.ObservableObject();
|
|
29
|
-
const map = new mobx_1.ObservableMap();
|
|
30
|
-
map.set("a", a);
|
|
31
|
-
map.set("b", b);
|
|
32
|
-
let runs = 0;
|
|
33
|
-
(0, mobx_1.autorun)(() => {
|
|
34
|
-
noop([...map.values()].map((a) => a.age));
|
|
35
|
-
runs++;
|
|
36
|
-
});
|
|
37
|
-
expect(runs).toBe(1);
|
|
38
|
-
b.age = 1;
|
|
39
|
-
expect(runs).toBe(2);
|
|
40
|
-
});
|
|
41
|
-
it("mobx lists maintain observable identity", () => {
|
|
42
|
-
// given a parent observable
|
|
43
|
-
const a = (0, mobx_1.observable)({ list: [] });
|
|
44
|
-
// if we observable-ize a value being pushing it on the list
|
|
45
|
-
const c1 = (0, mobx_1.observable)({});
|
|
46
|
-
a.list.push(c1);
|
|
47
|
-
// then we get identify equality on the list lookups
|
|
48
|
-
expect(a.list[0] === c1).toEqual(true);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
function noop(t) { }
|
package/dist/proxies.d.ts
DELETED
package/dist/proxies.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.newDelegateProxy = newDelegateProxy;
|
|
4
|
-
/**
|
|
5
|
-
* Creates a new combined object with keys in `overrides` taking precedence, and then
|
|
6
|
-
* any other keys falling back to `delegate`.
|
|
7
|
-
*/
|
|
8
|
-
function newDelegateProxy(delegate, overrides) {
|
|
9
|
-
function pickTarget(key) {
|
|
10
|
-
return Reflect.has(overrides, key) ? overrides : delegate;
|
|
11
|
-
}
|
|
12
|
-
return new Proxy(delegate, {
|
|
13
|
-
get(object, key) {
|
|
14
|
-
return Reflect.get(pickTarget(key), key);
|
|
15
|
-
},
|
|
16
|
-
set(object, key, value) {
|
|
17
|
-
return Reflect.set(pickTarget(key), key, value);
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
}
|
package/dist/rules.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** A validation rule, given the value and name, return the error string if valid, or undefined if valid. */
|
|
2
|
-
export type Rule<V> = (opts: {
|
|
3
|
-
value: V;
|
|
4
|
-
key: string;
|
|
5
|
-
originalValue: V;
|
|
6
|
-
}) => string | undefined;
|
|
7
|
-
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
8
|
-
export declare const required: <V>({ value: v }: {
|
|
9
|
-
value: V;
|
|
10
|
-
}) => string | undefined;
|
package/dist/rules.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.required = void 0;
|
|
4
|
-
const mobx_1 = require("mobx");
|
|
5
|
-
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
6
|
-
// We pre-emptively make this a mobx action so that it's identity doesn't change when proxied
|
|
7
|
-
// and breaks our ability to do `rules.some(r => r === required)`.
|
|
8
|
-
exports.required = (0, mobx_1.action)(({ value: v }) => {
|
|
9
|
-
const isEmptyString = typeof v === "string" ? v.trim() === "" : false;
|
|
10
|
-
return v !== undefined && v !== null && !isEmptyString ? undefined : "Required";
|
|
11
|
-
});
|
package/dist/setupTests.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
package/dist/setupTests.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
require("@testing-library/jest-dom");
|
|
4
|
-
const mobx_1 = require("mobx");
|
|
5
|
-
// rtl-utils looks for the test runner on globalThis, and jest only injects `jest` into module scope
|
|
6
|
-
globalThis.jest = jest;
|
|
7
|
-
// formState doesn't use actions
|
|
8
|
-
(0, mobx_1.configure)({ enforceActions: "never" });
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
jest.useFakeTimers();
|
|
11
|
-
});
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
jest.runOnlyPendingTimers();
|
|
14
|
-
jest.useRealTimers();
|
|
15
|
-
});
|
package/dist/stackoverlfow.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
declare const c: {
|
|
2
|
-
value<V>(): ValueConfig<V>;
|
|
3
|
-
};
|
|
4
|
-
type Config<T> = {
|
|
5
|
-
[P in keyof T]: ValueConfig<T[P]>;
|
|
6
|
-
};
|
|
7
|
-
declare function config<T>(object: Config<T>): void;
|
|
8
|
-
interface ValueConfig<V> {
|
|
9
|
-
req(): ValueConfig<V>;
|
|
10
|
-
readOnly(): ValueConfig<V>;
|
|
11
|
-
validate(fn: (value: V) => boolean): ValueConfig<V>;
|
|
12
|
-
}
|
|
13
|
-
type Author = {
|
|
14
|
-
name: string;
|
|
15
|
-
address: string;
|
|
16
|
-
city: string;
|
|
17
|
-
};
|
package/dist/stackoverlfow.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Create a Zod-style fluent DSL for building config
|
|
3
|
-
const c = {
|
|
4
|
-
value() {
|
|
5
|
-
return null;
|
|
6
|
-
},
|
|
7
|
-
};
|
|
8
|
-
function config(object) { }
|
|
9
|
-
// Try and configure the author
|
|
10
|
-
config({
|
|
11
|
-
// correctly inferred as c.value<string> --> ValueConfig<string>
|
|
12
|
-
name: c.value(),
|
|
13
|
-
// inferred only as c.value<unknown> --> ValueConfig<unknown>
|
|
14
|
-
address: c.value().req(),
|
|
15
|
-
// compile error, v is implicitly typed as any
|
|
16
|
-
city: c.value().validate((v) => v.length > 0),
|
|
17
|
-
});
|
package/dist/temporal.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type Temporal } from "temporal-polyfill";
|
|
2
|
-
export type SupportedTemporal = Temporal.PlainDate | Temporal.ZonedDateTime;
|
|
3
|
-
/** Detects the Temporal value types that form-state treats as builtins. */
|
|
4
|
-
export declare function isSupportedTemporal(value: unknown): value is SupportedTemporal;
|
|
5
|
-
/** Compares supported Temporal values with Temporal's `.equals(...)` semantics. */
|
|
6
|
-
export declare function areSupportedTemporalValuesEqual(a: unknown, b: unknown): boolean | undefined;
|
package/dist/temporal.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isSupportedTemporal = isSupportedTemporal;
|
|
4
|
-
exports.areSupportedTemporalValuesEqual = areSupportedTemporalValuesEqual;
|
|
5
|
-
// Use Temporal's well-known tags for runtime detection instead of `instanceof` because this
|
|
6
|
-
// library can receive values from native Temporal, a polyfill, another copy of a polyfill,
|
|
7
|
-
// or another realm. Tag checks are stable across those boundaries, while `instanceof` is not.
|
|
8
|
-
const temporalPlainDateTag = "Temporal.PlainDate";
|
|
9
|
-
const temporalZonedDateTimeTag = "Temporal.ZonedDateTime";
|
|
10
|
-
/** Detects the Temporal value types that form-state treats as builtins. */
|
|
11
|
-
function isSupportedTemporal(value) {
|
|
12
|
-
if (!value || typeof value !== "object") {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
const record = value;
|
|
16
|
-
return (isSupportedTemporalTag(record[Symbol.toStringTag]) &&
|
|
17
|
-
typeof record.equals === "function" &&
|
|
18
|
-
typeof record.toJSON === "function");
|
|
19
|
-
}
|
|
20
|
-
/** Compares supported Temporal values with Temporal's `.equals(...)` semantics. */
|
|
21
|
-
function areSupportedTemporalValuesEqual(a, b) {
|
|
22
|
-
const aIsTemporal = isSupportedTemporal(a);
|
|
23
|
-
const bIsTemporal = isSupportedTemporal(b);
|
|
24
|
-
if (!aIsTemporal && !bIsTemporal) {
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
if (!aIsTemporal || !bIsTemporal || a[Symbol.toStringTag] !== b[Symbol.toStringTag]) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return a.equals(b);
|
|
31
|
-
}
|
|
32
|
-
function isSupportedTemporalTag(value) {
|
|
33
|
-
return value === temporalPlainDateTag || value === temporalZonedDateTimeTag;
|
|
34
|
-
}
|
package/dist/temporal.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/temporal.test.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const temporal_polyfill_1 = require("temporal-polyfill");
|
|
4
|
-
const configBuilders_1 = require("./configBuilders");
|
|
5
|
-
const objectField_1 = require("./fields/objectField");
|
|
6
|
-
describe("Temporal support", () => {
|
|
7
|
-
it("treats equal Temporal value fields as unchanged", () => {
|
|
8
|
-
const form = (0, objectField_1.createObjectState)(temporalValueConfig, {
|
|
9
|
-
publishedOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"),
|
|
10
|
-
scheduledAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"),
|
|
11
|
-
});
|
|
12
|
-
form.publishedOn.set(temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"));
|
|
13
|
-
form.scheduledAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"));
|
|
14
|
-
expect(form.publishedOn.dirty).toBe(false);
|
|
15
|
-
expect(form.scheduledAt.dirty).toBe(false);
|
|
16
|
-
expect(form.dirty).toBe(false);
|
|
17
|
-
});
|
|
18
|
-
it("tracks dirty nested object fields with Temporal values", () => {
|
|
19
|
-
const form = (0, objectField_1.createObjectState)(temporalObjectConfig, {
|
|
20
|
-
details: {
|
|
21
|
-
dueOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"),
|
|
22
|
-
startsAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"),
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
form.details.dueOn.set(temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"));
|
|
26
|
-
form.details.startsAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"));
|
|
27
|
-
expect(form.details.dirty).toBe(false);
|
|
28
|
-
expect(form.dirty).toBe(false);
|
|
29
|
-
form.details.startsAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-18T09:00:00-04:00[America/New_York]"));
|
|
30
|
-
expect(form.details.startsAt.dirty).toBe(true);
|
|
31
|
-
expect(form.details.dirty).toBe(true);
|
|
32
|
-
expect(form.dirty).toBe(true);
|
|
33
|
-
form.details.revertChanges();
|
|
34
|
-
expect(form.details.startsAt.dirty).toBe(false);
|
|
35
|
-
expect(form.details.dirty).toBe(false);
|
|
36
|
-
expect(form.dirty).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
it("supports list row hashing for Temporal.ZonedDateTime values", () => {
|
|
39
|
-
var _a;
|
|
40
|
-
const form = (0, objectField_1.createObjectState)(temporalListConfig, { events: [], milestones: [] });
|
|
41
|
-
expect(() => {
|
|
42
|
-
form.events.add({ startsAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]") });
|
|
43
|
-
}).not.toThrow();
|
|
44
|
-
expect(form.events.dirty).toBe(true);
|
|
45
|
-
const ackedRow = {
|
|
46
|
-
id: "event:1",
|
|
47
|
-
startsAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"),
|
|
48
|
-
};
|
|
49
|
-
expect(() => form.set({ events: [ackedRow] }, { refreshing: true })).not.toThrow();
|
|
50
|
-
expect(form.events.dirty).toBe(false);
|
|
51
|
-
expect(form.events.value[0].id).toEqual("event:1");
|
|
52
|
-
expect((_a = form.events.rows[0].startsAt.value) === null || _a === void 0 ? void 0 : _a.equals(ackedRow.startsAt)).toBe(true);
|
|
53
|
-
form.events.rows[0].startsAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-18T09:00:00-04:00[America/New_York]"));
|
|
54
|
-
expect(form.events.rows[0].startsAt.dirty).toBe(true);
|
|
55
|
-
expect(form.events.dirty).toBe(true);
|
|
56
|
-
expect(() => form.events.remove(0)).not.toThrow();
|
|
57
|
-
expect(form.events.value).toEqual([]);
|
|
58
|
-
});
|
|
59
|
-
it("supports list row hashing for Temporal.PlainDate values", () => {
|
|
60
|
-
var _a;
|
|
61
|
-
const form = (0, objectField_1.createObjectState)(temporalListConfig, { events: [], milestones: [] });
|
|
62
|
-
expect(() => {
|
|
63
|
-
form.milestones.add({ dueOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17") });
|
|
64
|
-
}).not.toThrow();
|
|
65
|
-
expect(form.milestones.dirty).toBe(true);
|
|
66
|
-
const ackedRow = { id: "milestone:1", dueOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17") };
|
|
67
|
-
expect(() => form.set({ milestones: [ackedRow] }, { refreshing: true })).not.toThrow();
|
|
68
|
-
expect(form.milestones.dirty).toBe(false);
|
|
69
|
-
expect(form.milestones.value[0].id).toEqual("milestone:1");
|
|
70
|
-
expect((_a = form.milestones.rows[0].dueOn.value) === null || _a === void 0 ? void 0 : _a.equals(ackedRow.dueOn)).toBe(true);
|
|
71
|
-
form.milestones.rows[0].dueOn.set(temporal_polyfill_1.Temporal.PlainDate.from("2026-04-18"));
|
|
72
|
-
expect(form.milestones.rows[0].dueOn.dirty).toBe(true);
|
|
73
|
-
expect(form.milestones.dirty).toBe(true);
|
|
74
|
-
expect(() => form.milestones.remove(0)).not.toThrow();
|
|
75
|
-
expect(form.milestones.value).toEqual([]);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
const temporalValueConfig = configBuilders_1.f.config({
|
|
79
|
-
publishedOn: configBuilders_1.f.value(),
|
|
80
|
-
scheduledAt: configBuilders_1.f.value(),
|
|
81
|
-
});
|
|
82
|
-
const temporalObjectConfig = configBuilders_1.f.config({
|
|
83
|
-
details: configBuilders_1.f.object({
|
|
84
|
-
dueOn: configBuilders_1.f.value(),
|
|
85
|
-
startsAt: configBuilders_1.f.value(),
|
|
86
|
-
}),
|
|
87
|
-
});
|
|
88
|
-
const temporalListConfig = configBuilders_1.f.config({
|
|
89
|
-
events: configBuilders_1.f.list({
|
|
90
|
-
id: configBuilders_1.f.value(),
|
|
91
|
-
startsAt: configBuilders_1.f.value(),
|
|
92
|
-
}),
|
|
93
|
-
milestones: configBuilders_1.f.list({
|
|
94
|
-
id: configBuilders_1.f.value(),
|
|
95
|
-
dueOn: configBuilders_1.f.value(),
|
|
96
|
-
}),
|
|
97
|
-
});
|
package/dist/useFormState.d.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { type ObjectConfig } from "./config";
|
|
2
|
-
import { type ObjectState } from "./fields/objectField";
|
|
3
|
-
export type Query<I> = {
|
|
4
|
-
data: I;
|
|
5
|
-
loading: boolean;
|
|
6
|
-
error?: any;
|
|
7
|
-
};
|
|
8
|
-
export type InputAndMap<T, I> = {
|
|
9
|
-
input: I;
|
|
10
|
-
map?: (input: Exclude<I, null | undefined>) => T;
|
|
11
|
-
ifUndefined?: T;
|
|
12
|
-
onlyOnce?: boolean;
|
|
13
|
-
};
|
|
14
|
-
export type QueryAndMap<T, I> = {
|
|
15
|
-
query: Query<I>;
|
|
16
|
-
map: (input: Exclude<I, null | undefined>) => T;
|
|
17
|
-
ifUndefined?: T;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* The opts has for `useFormState`.
|
|
21
|
-
*
|
|
22
|
-
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
23
|
-
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
24
|
-
*/
|
|
25
|
-
export type UseFormStateOpts<T, I> = {
|
|
26
|
-
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
27
|
-
config: ObjectConfig<T>;
|
|
28
|
-
/**
|
|
29
|
-
* Provides the form's initial value.
|
|
30
|
-
*
|
|
31
|
-
* User's can either:
|
|
32
|
-
*
|
|
33
|
-
* - Provide no initial value (don't set `init` at all)
|
|
34
|
-
* - Provide an initial value that already matches the form type `T` (i.e. set `init: data`)
|
|
35
|
-
* - Provide an initial value from an object that _almost_ matches the form type `T`,
|
|
36
|
-
* but needs to be mapped from it's input type `I` to the form type
|
|
37
|
-
* (i.e. set `init: { input: data, map: (data) => ...}`).
|
|
38
|
-
*
|
|
39
|
-
* The value of using the 3rd option is that: a) we internally `useMemo` on the identity of the
|
|
40
|
-
* `init.input` (i.e. a response from an Apollo hook) and don't require the `map` function
|
|
41
|
-
* to have a stable identity, and also b) we will null-check/undefined-check `init.input` and
|
|
42
|
-
* only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
|
|
43
|
-
* from having to null check within your `init.map` function.
|
|
44
|
-
*/
|
|
45
|
-
init?: InputAndMap<T, I> | QueryAndMap<T, I>;
|
|
46
|
-
/**
|
|
47
|
-
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
48
|
-
*
|
|
49
|
-
* Does not need to be stable/useMemo'd.
|
|
50
|
-
*/
|
|
51
|
-
addRules?: (state: ObjectState<T>) => void;
|
|
52
|
-
/** Whether the form should be read only, when changed it won't re-create the whole form. */
|
|
53
|
-
readOnly?: boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Whether the form is loading.
|
|
56
|
-
*
|
|
57
|
-
* Note that we also will infer loading from `init.input === undefined` or `init.query.loading`,
|
|
58
|
-
* so you only need to set this directly if you're not using either of those conventions.
|
|
59
|
-
*/
|
|
60
|
-
loading?: boolean;
|
|
61
|
-
/**
|
|
62
|
-
* Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
|
|
63
|
-
*
|
|
64
|
-
* Does not need to be stable/useMemo'd.
|
|
65
|
-
*/
|
|
66
|
-
autoSave?: (state: ObjectState<T>) => Promise<unknown>;
|
|
67
|
-
};
|
|
68
|
-
export type FormObjectState<T, I> = ObjectState<T> & {
|
|
69
|
-
/**
|
|
70
|
-
* Refreshes the form with a server-acked value, i.e. `form.update(savedAuthor)`.
|
|
71
|
-
*
|
|
72
|
-
* This is intended for save callbacks that need `form.dirty` to become `false` immediately, before the
|
|
73
|
-
* normal query/cache refresh has a chance to rerender `useFormState`. Pass the value returned by the
|
|
74
|
-
* server so the form can map it through `init.map` and then do a field-by-field refresh of only the
|
|
75
|
-
* changes the server has acknowledged.
|
|
76
|
-
*/
|
|
77
|
-
update(input: Exclude<I, null | undefined>): void;
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* Creates a formState instance for editing in a form.
|
|
81
|
-
*/
|
|
82
|
-
export declare function useFormState<T, I>(opts: UseFormStateOpts<T, I>): FormObjectState<T, I>;
|
package/dist/useFormState.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useFormState = useFormState;
|
|
4
|
-
const is_plain_object_1 = require("is-plain-object");
|
|
5
|
-
const react_1 = require("react");
|
|
6
|
-
const objectField_1 = require("./fields/objectField");
|
|
7
|
-
const utils_1 = require("./utils");
|
|
8
|
-
// If the user's autoSave hook makes some last-minute `.set` calls to sneak
|
|
9
|
-
// in some business logic right before their GraphQL mutation call, ignore it
|
|
10
|
-
// so that we don't infinite loop.
|
|
11
|
-
let isAutoSaving = false;
|
|
12
|
-
// `pendingAutoSave` is a flag for determining if we need to immediately call `maybeAutoSave` again after the initial Promise finishes
|
|
13
|
-
// This could happen if a field triggers auto-save while another field's auto-save is already in progress.
|
|
14
|
-
let pendingAutoSave = false;
|
|
15
|
-
/**
|
|
16
|
-
* Creates a formState instance for editing in a form.
|
|
17
|
-
*/
|
|
18
|
-
function useFormState(opts) {
|
|
19
|
-
const { config, init, addRules, readOnly = false, loading, autoSave } = opts;
|
|
20
|
-
// Use a ref so our memo'ized `onBlur` always see the latest value
|
|
21
|
-
const autoSaveRef = (0, react_1.useRef)(autoSave);
|
|
22
|
-
autoSaveRef.current = autoSave;
|
|
23
|
-
// Use a ref so our memo'ized `update` always sees the latest init/map
|
|
24
|
-
const initRef = (0, react_1.useRef)(init);
|
|
25
|
-
initRef.current = init;
|
|
26
|
-
const firstRunRef = (0, react_1.useRef)(true);
|
|
27
|
-
// 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
|
|
28
|
-
const [firstInitValue] = (0, react_1.useState)(() => (0, utils_1.initValue)(config, init));
|
|
29
|
-
const isWrappingMobxProxy = !(0, is_plain_object_1.isPlainObject)(firstInitValue);
|
|
30
|
-
// If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
|
|
31
|
-
const dep = (0, utils_1.isInput)(init)
|
|
32
|
-
? init.onlyOnce
|
|
33
|
-
? []
|
|
34
|
-
: makeArray(init.input)
|
|
35
|
-
: (0, utils_1.isQuery)(init)
|
|
36
|
-
? [init.query.data, init.query.loading]
|
|
37
|
-
: [];
|
|
38
|
-
const form = (0, react_1.useMemo)(() => {
|
|
39
|
-
function maybeAutoSave() {
|
|
40
|
-
if (isAutoSaving === "in-flight") {
|
|
41
|
-
pendingAutoSave = true;
|
|
42
|
-
}
|
|
43
|
-
else if (isAutoSaving === "queued") {
|
|
44
|
-
// If we've queued up an auto-save, that means we haven't called the `autoSave(...)` function yet, so
|
|
45
|
-
// we assume that the scheduled-but-not-yet-invoked invocation will find both it's original change, and
|
|
46
|
-
// also this potentially new change, and put them both onto the wire as a single operation.
|
|
47
|
-
// So we can just ignore this call.
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Don't use canSave() because we don't want to set touched for all the fields
|
|
51
|
-
if (autoSaveRef.current && form.dirty && !isAutoSaving) {
|
|
52
|
-
// It's very frustrating to not know why the form is savings, to go ahead and log these
|
|
53
|
-
if (!form.valid) {
|
|
54
|
-
console.debug("Skipping auto-save b/c form is invalid: ", form.errors);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
isAutoSaving = "queued";
|
|
58
|
-
let maybeError;
|
|
59
|
-
// We use setTimeout as a cheap way to wait until the end of the current event listener
|
|
60
|
-
setTimeout(async () => {
|
|
61
|
-
try {
|
|
62
|
-
// Tell commitChanges to blow up.
|
|
63
|
-
form._isAutoSaving = true;
|
|
64
|
-
// We technically don't flip to in-flight until after the call in case the
|
|
65
|
-
// user's autoSave function itself wants to call a .set (which would call `maybeAutoSave`,
|
|
66
|
-
// and we don't want it to see `isAutoSaving=in-flight` and think it "missed the boat",
|
|
67
|
-
// and so schedule an unnecessary follow-up autosave.)
|
|
68
|
-
const promise = autoSaveRef.current(form);
|
|
69
|
-
isAutoSaving = "in-flight";
|
|
70
|
-
await promise;
|
|
71
|
-
}
|
|
72
|
-
catch (e) {
|
|
73
|
-
maybeError = String(e);
|
|
74
|
-
throw e;
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
isAutoSaving = false;
|
|
78
|
-
form._isAutoSaving = false;
|
|
79
|
-
if (pendingAutoSave) {
|
|
80
|
-
pendingAutoSave = false;
|
|
81
|
-
// Push out the follow-up by 1 tick to allow refreshes to happen to potentially
|
|
82
|
-
// un-dirty the just-saved data (e.g. if we run right away, the caller's maybeAutoSave
|
|
83
|
-
// will see a form.changedValue that thinks the just-saved data is still dirty).
|
|
84
|
-
setTimeout(maybeAutoSave, 0);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}, 0);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const value = firstRunRef.current ? firstInitValue : (0, utils_1.initValue)(config, init);
|
|
91
|
-
const form = (0, objectField_1.createObjectState)(config, value, { maybeAutoSave });
|
|
92
|
-
form.update = function update(input) {
|
|
93
|
-
const init = initRef.current;
|
|
94
|
-
const updateInit = (0, utils_1.isInput)(init)
|
|
95
|
-
? { ...init, input }
|
|
96
|
-
: (0, utils_1.isQuery)(init)
|
|
97
|
-
? { input, map: init.map, ifUndefined: init.ifUndefined }
|
|
98
|
-
: { input };
|
|
99
|
-
form.set((0, utils_1.initValue)(config, updateInit), {
|
|
100
|
-
refreshing: true,
|
|
101
|
-
});
|
|
102
|
-
};
|
|
103
|
-
form.readOnly = readOnly;
|
|
104
|
-
setLoading(form, opts);
|
|
105
|
-
// The identity of `addRules` is not stable, but assume that it is for better UX.
|
|
106
|
-
(addRules || (() => { }))(form);
|
|
107
|
-
firstRunRef.current = true;
|
|
108
|
-
return form;
|
|
109
|
-
},
|
|
110
|
-
// For this useMemo, we (almost) never re-run so that we can have a stable `form` identity across query refreshes.
|
|
111
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
|
-
[config, ...(isWrappingMobxProxy ? dep : [])]);
|
|
113
|
-
// We use useEffect so that any mutations to the proxies, which will call `setState`s on any observers to
|
|
114
|
-
// queue their components' render), don't happen during our render, per https://fb.me/setstate-in-render.
|
|
115
|
-
(0, react_1.useEffect)(() => {
|
|
116
|
-
// Ignore the 1st run b/c our 1st useMemo already initialized `form` with the current `init` value.
|
|
117
|
-
// Also for mobx proxies, we recreate a new form-state every time init changes, so that our
|
|
118
|
-
// fields fundamentally pointing to the right proxy. So just defer to the ^ useMemo.
|
|
119
|
-
if (firstRunRef.current || isWrappingMobxProxy) {
|
|
120
|
-
firstRunRef.current = false;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
setLoading(form, opts);
|
|
124
|
-
// Note: maybe someday we want to watch the `id` value and notice if it's changing (i.e. 2 -> 3),
|
|
125
|
-
// and treat this more as a reset than a refresh, b/c the id changing means it's probably
|
|
126
|
-
// not "the cache refreshed after a mutation save" but "the cache changed b/c of changing
|
|
127
|
-
// rows in the table that our side panel's form has open/is focused on" (which ideally would
|
|
128
|
-
// be treated as a full component remount by having an `key` field somewhere in the parent
|
|
129
|
-
// component, but it's unlikely the user will always remember to do this).
|
|
130
|
-
form.set((0, utils_1.initValue)(config, init), { refreshing: true });
|
|
131
|
-
}, [form, ...dep]);
|
|
132
|
-
// Use useEffect so that we don't touch the form.init proxy during a render
|
|
133
|
-
(0, react_1.useEffect)(() => {
|
|
134
|
-
form.readOnly = readOnly;
|
|
135
|
-
if (loading !== undefined) {
|
|
136
|
-
form.loading = loading;
|
|
137
|
-
}
|
|
138
|
-
}, [form, readOnly, loading]);
|
|
139
|
-
return form;
|
|
140
|
-
}
|
|
141
|
-
function setLoading(form, opts) {
|
|
142
|
-
const { loading, init } = opts;
|
|
143
|
-
if (loading !== undefined) {
|
|
144
|
-
// Prefer the explicit/top-level opts.loading if it's set
|
|
145
|
-
form.loading = loading;
|
|
146
|
-
}
|
|
147
|
-
else if ((0, utils_1.isInput)(init) && !init.ifUndefined) {
|
|
148
|
-
// Otherwise, check for `init.input`
|
|
149
|
-
form.loading = init.input === undefined;
|
|
150
|
-
}
|
|
151
|
-
else if ((0, utils_1.isQuery)(init)) {
|
|
152
|
-
// Or `query.loading`
|
|
153
|
-
form.loading = init.query.loading;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function makeArray(input) {
|
|
157
|
-
return Array.isArray(input) ? input : [input];
|
|
158
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|