@playfast/reform-forms 1.1.1 → 1.2.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/README.md +3 -5
- package/package.json +19 -19
- package/src/form.test.ts +37 -52
- package/src/form.ts +691 -87
- package/src/index.ts +1 -7
- package/src/path.ts +32 -34
- package/src/formState.ts +0 -166
- package/src/formTypes.ts +0 -211
- package/src/formView.ts +0 -202
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Values, validation, field metadata, list operations, submit flow, and correlatio
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
`@playfast/reform-forms` owns everything a form needs
|
|
16
|
+
`@playfast/reform-forms` owns everything a form needs _except_ its widgets. It has no DOM and no React API — it produces a typed, platform-neutral view that any renderer can map. Pair it with [`@playfast/reform-forms-react`](https://www.npmjs.com/package/@playfast/reform-forms-react) for typed JSX, or consume the view directly from a custom host.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -61,9 +61,7 @@ const CheckoutFormLive = Form.live(CheckoutForm, {
|
|
|
61
61
|
items: { minItems: 1, maxItems: inputs.settings.maxItems },
|
|
62
62
|
}),
|
|
63
63
|
validate: ({ values }) =>
|
|
64
|
-
values.email.includes('@')
|
|
65
|
-
? undefined
|
|
66
|
-
: Form.error('email', 'Email must contain @'),
|
|
64
|
+
values.email.includes('@') ? undefined : Form.error('email', 'Email must contain @'),
|
|
67
65
|
submit: ({ decoded, inputs }) => saveCheckout(decoded, inputs.user.id),
|
|
68
66
|
})
|
|
69
67
|
```
|
|
@@ -71,7 +69,7 @@ const CheckoutFormLive = Form.live(CheckoutForm, {
|
|
|
71
69
|
Read the form inside compositions with `Form.view(CheckoutForm)` — a typed, platform-neutral object:
|
|
72
70
|
|
|
73
71
|
```ts
|
|
74
|
-
const form = yield* Form.view(CheckoutForm)
|
|
72
|
+
const form = yield * Form.view(CheckoutForm)
|
|
75
73
|
|
|
76
74
|
form.field('email').set('a@example.com')
|
|
77
75
|
form.array('items').append({ name: 'Milk', qty: 1 })
|
package/package.json
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-forms",
|
|
3
|
-
"
|
|
4
|
-
"version": "1.1.1",
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "1.2.0",
|
|
6
4
|
"description": "Headless, schema-driven form state for reform — values, validation, field limitations, list operations, and submit flow, rendering nothing.",
|
|
7
5
|
"keywords": [
|
|
8
|
-
"reform",
|
|
9
6
|
"effect",
|
|
10
|
-
"forms",
|
|
11
7
|
"form-state",
|
|
12
|
-
"
|
|
8
|
+
"forms",
|
|
9
|
+
"headless",
|
|
10
|
+
"reform",
|
|
13
11
|
"schema",
|
|
14
|
-
"
|
|
12
|
+
"validation"
|
|
15
13
|
],
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/playfast/reform/issues"
|
|
16
|
+
},
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
19
20
|
"url": "https://github.com/playfast/reform.git",
|
|
20
21
|
"directory": "packages/forms"
|
|
21
22
|
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
25
28
|
"sideEffects": false,
|
|
26
29
|
"exports": {
|
|
27
30
|
"./package.json": "./package.json",
|
|
28
31
|
".": "./src/index.ts",
|
|
29
32
|
"./*": "./src/*.ts"
|
|
30
33
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
35
37
|
"scripts": {
|
|
36
38
|
"clean": "rm -rf dist .tsbuildinfo",
|
|
37
39
|
"check": "tsc --noEmit",
|
|
@@ -43,10 +45,8 @@
|
|
|
43
45
|
"lint:fix": "oxlint --fix src"
|
|
44
46
|
},
|
|
45
47
|
"peerDependencies": {
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
+
"@playfast/reform": "*",
|
|
49
|
+
"effect": "*"
|
|
48
50
|
},
|
|
49
|
-
"
|
|
50
|
-
"access": "public"
|
|
51
|
-
}
|
|
51
|
+
"playbook": "./playbook"
|
|
52
52
|
}
|
package/src/form.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { expect, it } from
|
|
2
|
-
import { Effect, Layer, Option, Schema as S } from
|
|
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,81 +11,66 @@ 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
|
-
|
|
16
|
-
),
|
|
17
|
-
);
|
|
14
|
+
: Effect.flatMap(Effect.yieldNow(), () => waitUntil(read, pred, rounds - 1)),
|
|
15
|
+
)
|
|
18
16
|
|
|
19
17
|
const CheckoutSchema = S.Struct({
|
|
20
18
|
email: S.String,
|
|
21
19
|
items: S.Array(S.Struct({ name: S.String, qty: S.Number })),
|
|
22
20
|
payment: S.Union(
|
|
23
|
-
S.TaggedStruct(
|
|
24
|
-
S.TaggedStruct(
|
|
21
|
+
S.TaggedStruct('Card', { cardNumber: S.String }),
|
|
22
|
+
S.TaggedStruct('Paypal', { email: S.String }),
|
|
25
23
|
),
|
|
26
|
-
})
|
|
24
|
+
})
|
|
27
25
|
|
|
28
|
-
class CheckoutForm extends Form.make(
|
|
26
|
+
class CheckoutForm extends Form.make('CheckoutForm', {
|
|
29
27
|
schema: CheckoutSchema,
|
|
30
28
|
}) {}
|
|
31
29
|
|
|
32
30
|
const layer = Form.live(CheckoutForm, {
|
|
33
31
|
initial: {
|
|
34
|
-
email:
|
|
32
|
+
email: '',
|
|
35
33
|
items: [],
|
|
36
|
-
payment: { _tag:
|
|
34
|
+
payment: { _tag: 'Card', cardNumber: '' },
|
|
37
35
|
},
|
|
38
36
|
validate: ({ values }) =>
|
|
39
|
-
values.email.includes(
|
|
40
|
-
|
|
41
|
-
: Form.error("email", "Email must contain @"),
|
|
42
|
-
}).pipe(Layer.provideMerge(Engine));
|
|
37
|
+
values.email.includes('@') ? undefined : Form.error('email', 'Email must contain @'),
|
|
38
|
+
}).pipe(Layer.provideMerge(Engine))
|
|
43
39
|
|
|
44
|
-
it.scopedLive(
|
|
40
|
+
it.scopedLive('field bindings write through the form reducer', () =>
|
|
45
41
|
Effect.gen(function* () {
|
|
46
|
-
const view = yield* Form.view(CheckoutForm)
|
|
47
|
-
view.field(
|
|
42
|
+
const view = yield* Form.view(CheckoutForm)
|
|
43
|
+
view.field('email').set('a@example.com')
|
|
48
44
|
|
|
49
45
|
const next = yield* waitUntil(
|
|
50
46
|
Form.view(CheckoutForm),
|
|
51
|
-
(v) => v.values.email ===
|
|
52
|
-
)
|
|
53
|
-
expect(next.field(
|
|
47
|
+
(v) => v.values.email === 'a@example.com',
|
|
48
|
+
)
|
|
49
|
+
expect(next.field('email').dirty).toBe(true)
|
|
54
50
|
}).pipe(Effect.provide(layer)),
|
|
55
|
-
)
|
|
51
|
+
)
|
|
56
52
|
|
|
57
|
-
it.scopedLive(
|
|
53
|
+
it.scopedLive('array bindings append and remove items', () =>
|
|
58
54
|
Effect.gen(function* () {
|
|
59
|
-
const view = yield* Form.view(CheckoutForm)
|
|
60
|
-
view.array(
|
|
55
|
+
const view = yield* Form.view(CheckoutForm)
|
|
56
|
+
view.array('items').append({ name: 'milk', qty: 1 })
|
|
61
57
|
|
|
62
|
-
const withItem = yield* waitUntil(
|
|
63
|
-
|
|
64
|
-
(v) => v.values.items.length === 1,
|
|
65
|
-
);
|
|
66
|
-
expect(withItem.array("items").items[0]?.key).toMatch(/^form-item-/);
|
|
58
|
+
const withItem = yield* waitUntil(Form.view(CheckoutForm), (v) => v.values.items.length === 1)
|
|
59
|
+
expect(withItem.array('items').items[0]?.key).toMatch(/^form-item-/)
|
|
67
60
|
|
|
68
|
-
withItem.array(
|
|
69
|
-
const empty = yield* waitUntil(
|
|
70
|
-
|
|
71
|
-
(v) => v.values.items.length === 0,
|
|
72
|
-
);
|
|
73
|
-
expect(empty.values.items).toEqual([]);
|
|
61
|
+
withItem.array('items').remove(0)
|
|
62
|
+
const empty = yield* waitUntil(Form.view(CheckoutForm), (v) => v.values.items.length === 0)
|
|
63
|
+
expect(empty.values.items).toEqual([])
|
|
74
64
|
}).pipe(Effect.provide(layer)),
|
|
75
|
-
)
|
|
65
|
+
)
|
|
76
66
|
|
|
77
|
-
it.scopedLive(
|
|
67
|
+
it.scopedLive('submit routes custom validation errors to fields', () =>
|
|
78
68
|
Effect.gen(function* () {
|
|
79
|
-
const view = yield* Form.view(CheckoutForm)
|
|
80
|
-
view.submit()
|
|
69
|
+
const view = yield* Form.view(CheckoutForm)
|
|
70
|
+
view.submit()
|
|
81
71
|
|
|
82
|
-
const invalid = yield* waitUntil(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
86
|
-
const error = invalid.field("email").error;
|
|
87
|
-
expect(Option.isSome(error) ? error.value : undefined).toBe(
|
|
88
|
-
"Email must contain @",
|
|
89
|
-
);
|
|
72
|
+
const invalid = yield* waitUntil(Form.view(CheckoutForm), (v) => v.validationCount > 0)
|
|
73
|
+
const error = invalid.field('email').error
|
|
74
|
+
expect(Option.isSome(error) ? error.value : undefined).toBe('Email must contain @')
|
|
90
75
|
}).pipe(Effect.provide(layer)),
|
|
91
|
-
)
|
|
76
|
+
)
|