@playfast/reform-forms 1.0.1 → 1.1.1
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/package.json +1 -1
- package/src/form.test.ts +62 -63
- package/src/form.ts +47 -553
- package/src/formState.ts +166 -0
- package/src/formTypes.ts +211 -0
- package/src/formView.ts +202 -0
- package/src/path.ts +1 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-forms",
|
|
3
3
|
"playbook": "./playbook",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Headless, schema-driven form state for reform — values, validation, field limitations, list operations, and submit flow, rendering nothing.",
|
|
7
7
|
"keywords": [
|
package/src/form.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { expect,
|
|
2
|
-
import { Effect, Layer,
|
|
3
|
-
import { Engine } from
|
|
4
|
-
import * as Form from
|
|
1
|
+
import { expect, it } from "@effect/vitest";
|
|
2
|
+
import { Effect, Layer, Option, Schema as S } from "effect";
|
|
3
|
+
import { Engine } from "@playfast/reform";
|
|
4
|
+
import * as Form from "./form";
|
|
5
5
|
|
|
6
6
|
const waitUntil = <A, R>(
|
|
7
7
|
read: Effect.Effect<A, never, R>,
|
|
@@ -11,82 +11,81 @@ const waitUntil = <A, R>(
|
|
|
11
11
|
Effect.flatMap(read, (value) =>
|
|
12
12
|
pred(value) || rounds <= 0
|
|
13
13
|
? Effect.succeed(value)
|
|
14
|
-
: Effect.flatMap(Effect.yieldNow(), () =>
|
|
15
|
-
|
|
14
|
+
: Effect.flatMap(Effect.yieldNow(), () =>
|
|
15
|
+
waitUntil(read, pred, rounds - 1),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
16
18
|
|
|
17
19
|
const CheckoutSchema = S.Struct({
|
|
18
20
|
email: S.String,
|
|
19
21
|
items: S.Array(S.Struct({ name: S.String, qty: S.Number })),
|
|
20
22
|
payment: S.Union(
|
|
21
|
-
S.TaggedStruct(
|
|
22
|
-
S.TaggedStruct(
|
|
23
|
+
S.TaggedStruct("Card", { cardNumber: S.String }),
|
|
24
|
+
S.TaggedStruct("Paypal", { email: S.String }),
|
|
23
25
|
),
|
|
24
|
-
})
|
|
26
|
+
});
|
|
25
27
|
|
|
26
|
-
class CheckoutForm extends Form.make(
|
|
28
|
+
class CheckoutForm extends Form.make("CheckoutForm", {
|
|
29
|
+
schema: CheckoutSchema,
|
|
30
|
+
}) {}
|
|
27
31
|
|
|
28
32
|
const layer = Form.live(CheckoutForm, {
|
|
29
33
|
initial: {
|
|
30
|
-
email:
|
|
34
|
+
email: "",
|
|
31
35
|
items: [],
|
|
32
|
-
payment: { _tag:
|
|
36
|
+
payment: { _tag: "Card", cardNumber: "" },
|
|
33
37
|
},
|
|
34
38
|
validate: ({ values }) =>
|
|
35
|
-
values.email.includes(
|
|
36
|
-
|
|
39
|
+
values.email.includes("@")
|
|
40
|
+
? undefined
|
|
41
|
+
: Form.error("email", "Email must contain @"),
|
|
42
|
+
}).pipe(Layer.provideMerge(Engine));
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Effect.gen(function* () {
|
|
43
|
-
const view = yield* Form.view(CheckoutForm)
|
|
44
|
-
view.field('email').set('a@example.com')
|
|
44
|
+
it.scopedLive("field bindings write through the form reducer", () =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const view = yield* Form.view(CheckoutForm);
|
|
47
|
+
view.field("email").set("a@example.com");
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
49
|
+
const next = yield* waitUntil(
|
|
50
|
+
Form.view(CheckoutForm),
|
|
51
|
+
(v) => v.values.email === "a@example.com",
|
|
52
|
+
);
|
|
53
|
+
expect(next.field("email").dirty).toBe(true);
|
|
54
|
+
}).pipe(Effect.provide(layer)),
|
|
55
|
+
);
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Effect.gen(function* () {
|
|
60
|
-
const view = yield* Form.view(CheckoutForm)
|
|
61
|
-
view.array('items').append({ name: 'milk', qty: 1 })
|
|
57
|
+
it.scopedLive("array bindings append and remove items", () =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const view = yield* Form.view(CheckoutForm);
|
|
60
|
+
view.array("items").append({ name: "milk", qty: 1 });
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const withItem = yield* waitUntil(
|
|
63
|
+
Form.view(CheckoutForm),
|
|
64
|
+
(v) => v.values.items.length === 1,
|
|
65
|
+
);
|
|
66
|
+
expect(withItem.array("items").items[0]?.key).toMatch(/^form-item-/);
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
68
|
+
withItem.array("items").remove(0);
|
|
69
|
+
const empty = yield* waitUntil(
|
|
70
|
+
Form.view(CheckoutForm),
|
|
71
|
+
(v) => v.values.items.length === 0,
|
|
72
|
+
);
|
|
73
|
+
expect(empty.values.items).toEqual([]);
|
|
74
|
+
}).pipe(Effect.provide(layer)),
|
|
75
|
+
);
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Effect.gen(function* () {
|
|
81
|
-
const view = yield* Form.view(CheckoutForm)
|
|
82
|
-
view.submit()
|
|
77
|
+
it.scopedLive("submit routes custom validation errors to fields", () =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const view = yield* Form.view(CheckoutForm);
|
|
80
|
+
view.submit();
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
})
|
|
82
|
+
const invalid = yield* waitUntil(
|
|
83
|
+
Form.view(CheckoutForm),
|
|
84
|
+
(v) => v.validationCount > 0,
|
|
85
|
+
);
|
|
86
|
+
const error = invalid.field("email").error;
|
|
87
|
+
expect(Option.isSome(error) ? error.value : undefined).toBe(
|
|
88
|
+
"Email must contain @",
|
|
89
|
+
);
|
|
90
|
+
}).pipe(Effect.provide(layer)),
|
|
91
|
+
);
|