@playfast/reform-forms 0.0.7 → 0.0.8
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 +2 -2
- package/src/form.test.ts +92 -0
- package/src/form.ts +644 -0
- package/src/index.ts +47 -0
- package/src/path.test.ts +21 -0
- package/src/path.ts +194 -0
- package/src/validation.ts +42 -0
- package/dist/form.d.ts +0 -184
- package/dist/form.d.ts.map +0 -1
- package/dist/form.js +0 -292
- package/dist/form.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +0 -1
- package/dist/path.d.ts +0 -36
- package/dist/path.d.ts.map +0 -1
- package/dist/path.js +0 -104
- package/dist/path.js.map +0 -1
- package/dist/validation.d.ts +0 -12
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -33
- package/dist/validation.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-forms",
|
|
3
3
|
"playbook": "./playbook",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.8",
|
|
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": [
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"./*": "./src/*.ts"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
|
-
"
|
|
32
|
+
"src",
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
package/src/form.test.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
import { Effect, Layer, ManagedRuntime, Option, Schema as S } from 'effect'
|
|
3
|
+
import { Engine } from '@playfast/reform'
|
|
4
|
+
import * as Form from './form'
|
|
5
|
+
|
|
6
|
+
const waitUntil = <A, R>(
|
|
7
|
+
read: Effect.Effect<A, never, R>,
|
|
8
|
+
pred: (value: A) => boolean,
|
|
9
|
+
rounds = 100,
|
|
10
|
+
): Effect.Effect<A, never, R> =>
|
|
11
|
+
Effect.flatMap(read, (value) =>
|
|
12
|
+
pred(value) || rounds <= 0
|
|
13
|
+
? Effect.succeed(value)
|
|
14
|
+
: Effect.flatMap(Effect.yieldNow(), () => waitUntil(read, pred, rounds - 1)),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const CheckoutSchema = S.Struct({
|
|
18
|
+
email: S.String,
|
|
19
|
+
items: S.Array(S.Struct({ name: S.String, qty: S.Number })),
|
|
20
|
+
payment: S.Union(
|
|
21
|
+
S.TaggedStruct('Card', { cardNumber: S.String }),
|
|
22
|
+
S.TaggedStruct('Paypal', { email: S.String }),
|
|
23
|
+
),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
class CheckoutForm extends Form.make('CheckoutForm', { schema: CheckoutSchema }) {}
|
|
27
|
+
|
|
28
|
+
const layer = Form.live(CheckoutForm, {
|
|
29
|
+
initial: {
|
|
30
|
+
email: '',
|
|
31
|
+
items: [],
|
|
32
|
+
payment: { _tag: 'Card', cardNumber: '' },
|
|
33
|
+
},
|
|
34
|
+
validate: ({ values }) =>
|
|
35
|
+
values.email.includes('@') ? undefined : Form.error('email', 'Email must contain @'),
|
|
36
|
+
}).pipe(Layer.provideMerge(Engine))
|
|
37
|
+
|
|
38
|
+
test('field bindings write through the form reducer', async () => {
|
|
39
|
+
const runtime = ManagedRuntime.make(layer)
|
|
40
|
+
try {
|
|
41
|
+
await runtime.runPromise(
|
|
42
|
+
Effect.gen(function* () {
|
|
43
|
+
const view = yield* Form.view(CheckoutForm)
|
|
44
|
+
view.field('email').set('a@example.com')
|
|
45
|
+
|
|
46
|
+
const next = yield* waitUntil(Form.view(CheckoutForm), (v) => v.values.email === 'a@example.com')
|
|
47
|
+
expect(next.field('email').dirty).toBe(true)
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
} finally {
|
|
51
|
+
await runtime.dispose()
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('array bindings append and remove items', async () => {
|
|
56
|
+
const runtime = ManagedRuntime.make(layer)
|
|
57
|
+
try {
|
|
58
|
+
await runtime.runPromise(
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const view = yield* Form.view(CheckoutForm)
|
|
61
|
+
view.array('items').append({ name: 'milk', qty: 1 })
|
|
62
|
+
|
|
63
|
+
const withItem = yield* waitUntil(Form.view(CheckoutForm), (v) => v.values.items.length === 1)
|
|
64
|
+
expect(withItem.array('items').items[0]?.key).toMatch(/^form-item-/)
|
|
65
|
+
|
|
66
|
+
withItem.array('items').remove(0)
|
|
67
|
+
const empty = yield* waitUntil(Form.view(CheckoutForm), (v) => v.values.items.length === 0)
|
|
68
|
+
expect(empty.values.items).toEqual([])
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
} finally {
|
|
72
|
+
await runtime.dispose()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('submit routes custom validation errors to fields', async () => {
|
|
77
|
+
const runtime = ManagedRuntime.make(layer)
|
|
78
|
+
try {
|
|
79
|
+
await runtime.runPromise(
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const view = yield* Form.view(CheckoutForm)
|
|
82
|
+
view.submit()
|
|
83
|
+
|
|
84
|
+
const invalid = yield* waitUntil(Form.view(CheckoutForm), (v) => v.validationCount > 0)
|
|
85
|
+
const error = invalid.field('email').error
|
|
86
|
+
expect(Option.isSome(error) ? error.value : undefined).toBe('Email must contain @')
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
} finally {
|
|
90
|
+
await runtime.dispose()
|
|
91
|
+
}
|
|
92
|
+
})
|