@playfast/reform-forms-react 1.0.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 +27 -21
- package/package.json +16 -16
- package/src/formUi.typecheck.tsx +48 -34
- package/src/index.tsx +27 -17
package/README.md
CHANGED
|
@@ -16,27 +16,33 @@ drift from it.
|
|
|
16
16
|
```tsx
|
|
17
17
|
import { FormUi } from '@playfast/reform-forms-react'
|
|
18
18
|
|
|
19
|
-
export const CheckoutUi = FormUi.make(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
19
|
+
export const CheckoutUi = FormUi.make(
|
|
20
|
+
CheckoutContract,
|
|
21
|
+
CheckoutForm,
|
|
22
|
+
({ field, array, variant, form }) => (
|
|
23
|
+
<Screen>
|
|
24
|
+
{field('email').as(EmailInput, { label: 'Email' })}
|
|
25
|
+
{array('items', ({ items, append }) => (
|
|
26
|
+
<Stack>
|
|
27
|
+
{items.map((item) => (
|
|
28
|
+
<Row key={item.key}>
|
|
29
|
+
{item.field('name').as(TextInput)}
|
|
30
|
+
<IconButton onPress={item.remove} />
|
|
31
|
+
</Row>
|
|
32
|
+
))}
|
|
33
|
+
<Button onPress={() => append()}>Add</Button>
|
|
34
|
+
</Stack>
|
|
35
|
+
))}
|
|
36
|
+
{variant('payment', {
|
|
37
|
+
Card: ({ field }) => field('cardNumber').as(CardNumberInput),
|
|
38
|
+
Paypal: ({ field }) => field('email').as(EmailInput),
|
|
39
|
+
})}
|
|
40
|
+
<Button disabled={!form.canSubmit} onPress={form.submit}>
|
|
41
|
+
Submit
|
|
42
|
+
</Button>
|
|
43
|
+
</Screen>
|
|
44
|
+
),
|
|
45
|
+
)
|
|
40
46
|
```
|
|
41
47
|
|
|
42
48
|
The render callback receives scoped helpers (`field` / `array` / `variant`), the underlying `form` view, and the
|
package/package.json
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-forms-react",
|
|
3
|
-
"
|
|
4
|
-
"version": "1.0.1",
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "1.2.0",
|
|
6
4
|
"description": "React bindings for @playfast/forms — render a typed reform form View with scoped field/array/variant helpers.",
|
|
7
5
|
"keywords": [
|
|
8
|
-
"reform",
|
|
9
6
|
"effect",
|
|
10
7
|
"forms",
|
|
11
8
|
"react",
|
|
9
|
+
"reform",
|
|
12
10
|
"ui"
|
|
13
11
|
],
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/playfast/reform/issues"
|
|
14
|
+
},
|
|
14
15
|
"license": "MIT",
|
|
15
16
|
"repository": {
|
|
16
17
|
"type": "git",
|
|
17
18
|
"url": "https://github.com/playfast/reform.git",
|
|
18
19
|
"directory": "packages/forms-react"
|
|
19
20
|
},
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
23
26
|
"sideEffects": false,
|
|
24
27
|
"exports": {
|
|
25
28
|
"./package.json": "./package.json",
|
|
26
29
|
".": "./src/index.tsx",
|
|
27
30
|
"./*": "./src/*.tsx"
|
|
28
31
|
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
33
35
|
"scripts": {
|
|
34
36
|
"clean": "rm -rf dist .tsbuildinfo",
|
|
35
37
|
"check": "tsc --noEmit",
|
|
@@ -41,12 +43,10 @@
|
|
|
41
43
|
"lint:fix": "oxlint --fix src"
|
|
42
44
|
},
|
|
43
45
|
"peerDependencies": {
|
|
46
|
+
"@playfast/reform": "*",
|
|
44
47
|
"@playfast/reform-forms": "*",
|
|
45
48
|
"effect": "*",
|
|
46
|
-
"react": "^19.0.0"
|
|
47
|
-
"@playfast/reform": "*"
|
|
49
|
+
"react": "^19.0.0"
|
|
48
50
|
},
|
|
49
|
-
"
|
|
50
|
-
"access": "public"
|
|
51
|
-
}
|
|
51
|
+
"playbook": "./playbook"
|
|
52
52
|
}
|
package/src/formUi.typecheck.tsx
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Type-level checks enforced by `tsc --noEmit`.
|
|
2
|
-
|
|
3
1
|
import { Schema as S } from 'effect'
|
|
4
2
|
import { ui, type ViewImpl, type UiContract } from '@playfast/reform'
|
|
5
3
|
import { Form } from '@playfast/reform-forms'
|
|
@@ -28,35 +26,51 @@ class CheckoutUi extends ui('CheckoutTypecheckUi')<CheckoutContract>() {}
|
|
|
28
26
|
const TextInput: FieldComponent<string> = () => null
|
|
29
27
|
const NumberInput: FieldComponent<number> = () => null
|
|
30
28
|
|
|
31
|
-
export const ok: ViewImpl<CheckoutContract> = FormUi.make(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
))
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
29
|
+
export const ok: ViewImpl<CheckoutContract> = FormUi.make(
|
|
30
|
+
CheckoutUi,
|
|
31
|
+
CheckoutForm,
|
|
32
|
+
({ field, array, variant }) => (
|
|
33
|
+
<>
|
|
34
|
+
{field('email').as(TextInput)}
|
|
35
|
+
{field('age').as(NumberInput)}
|
|
36
|
+
{array('items', ({ items: rows }) => (
|
|
37
|
+
<>{rows.map((row) => row.field('name').as(TextInput))}</>
|
|
38
|
+
))}
|
|
39
|
+
{variant('payment', {
|
|
40
|
+
Card: ({ field }) => field('cardNumber').as(TextInput),
|
|
41
|
+
Paypal: ({ field }) => field('email').as(TextInput),
|
|
42
|
+
})}
|
|
43
|
+
</>
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export const badPath: ViewImpl<CheckoutContract> = FormUi.make(
|
|
48
|
+
CheckoutUi,
|
|
49
|
+
CheckoutForm,
|
|
50
|
+
({ field }) => (
|
|
51
|
+
// @ts-expect-error unknown field path
|
|
52
|
+
<>{field('missing').as(TextInput)}</>
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export const badComponent: ViewImpl<CheckoutContract> = FormUi.make(
|
|
57
|
+
CheckoutUi,
|
|
58
|
+
CheckoutForm,
|
|
59
|
+
({ field }) => (
|
|
60
|
+
// @ts-expect-error age is a number field, not a string field
|
|
61
|
+
<>{field('age').as(TextInput)}</>
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
export const badVariant: ViewImpl<CheckoutContract> = FormUi.make(
|
|
66
|
+
CheckoutUi,
|
|
67
|
+
CheckoutForm,
|
|
68
|
+
({ variant }) => (
|
|
69
|
+
<>
|
|
70
|
+
{/* @ts-expect-error Paypal is required for exhaustive variant mapping */}
|
|
71
|
+
{variant('payment', {
|
|
72
|
+
Card: ({ field }) => field('cardNumber').as(TextInput),
|
|
73
|
+
})}
|
|
74
|
+
</>
|
|
75
|
+
),
|
|
76
|
+
)
|
package/src/index.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createElement, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { Function as Fn } from 'effect'
|
|
3
|
-
import { Ui, type UiClass, type UiContract, type ViewImpl } from '@playfast/reform'
|
|
3
|
+
import { Ui, type MadeView, type UiClass, type UiContract, type ViewImpl } from '@playfast/reform'
|
|
4
4
|
import type {
|
|
5
|
+
ArrayBinding,
|
|
5
6
|
ArrayItem,
|
|
6
7
|
ArrayPath,
|
|
7
8
|
FieldBinding,
|
|
@@ -17,7 +18,9 @@ import type {
|
|
|
17
18
|
} from '@playfast/reform-forms'
|
|
18
19
|
import { joinPath } from '@playfast/reform-forms'
|
|
19
20
|
|
|
20
|
-
export type FieldComponent<A, P extends object = {}> = ComponentType<
|
|
21
|
+
export type FieldComponent<A, P extends object = {}> = ComponentType<
|
|
22
|
+
{ readonly field: FieldBinding<A> } & P
|
|
23
|
+
>
|
|
21
24
|
|
|
22
25
|
export interface FieldHandle<A> {
|
|
23
26
|
as(component: FieldComponent<A>): ReactNode
|
|
@@ -45,7 +48,9 @@ type VariantCases<V> = {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export interface ScopeHelpers<ScopeValues> {
|
|
48
|
-
readonly field: <P extends FieldPath<ScopeValues>>(
|
|
51
|
+
readonly field: <P extends FieldPath<ScopeValues>>(
|
|
52
|
+
path: P,
|
|
53
|
+
) => FieldHandle<PathValue<ScopeValues, P>>
|
|
49
54
|
readonly array: <P extends ArrayPath<ScopeValues>>(
|
|
50
55
|
path: P,
|
|
51
56
|
render: (args: ArrayRenderArgs<ArrayItem<ScopeValues, P>>) => ReactNode,
|
|
@@ -64,9 +69,7 @@ type ContractWithForm<F extends AnyForm> = UiContract & {
|
|
|
64
69
|
readonly props: { readonly form: View<F> }
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
const makeFieldHandle = <A,>(
|
|
68
|
-
field: FieldBinding<A>,
|
|
69
|
-
): FieldHandle<A> => ({
|
|
72
|
+
const makeFieldHandle = <A,>(field: FieldBinding<A>): FieldHandle<A> => ({
|
|
70
73
|
as: <P extends object>(component: FieldComponent<A, P>, props?: P): ReactNode =>
|
|
71
74
|
createElement(
|
|
72
75
|
component,
|
|
@@ -74,15 +77,22 @@ const makeFieldHandle = <A,>(
|
|
|
74
77
|
),
|
|
75
78
|
})
|
|
76
79
|
|
|
80
|
+
interface RuntimeFormView {
|
|
81
|
+
readonly field: <A>(path: string) => FieldBinding<A>
|
|
82
|
+
readonly array: <Item>(path: string) => ArrayBinding<Item>
|
|
83
|
+
readonly variantValue: <Variant>(path: string) => Variant
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
const makeScope = <ScopeValues,>(
|
|
78
|
-
form:
|
|
87
|
+
form: RuntimeFormView,
|
|
79
88
|
prefix: string,
|
|
80
89
|
): ScopeHelpers<ScopeValues> => {
|
|
81
90
|
const full = (path: string): string => joinPath(prefix, path)
|
|
82
91
|
return {
|
|
83
|
-
field: (path) =>
|
|
92
|
+
field: (path) =>
|
|
93
|
+
makeFieldHandle(form.field<PathValue<ScopeValues, typeof path>>(full(String(path)))),
|
|
84
94
|
array: (path, render) => {
|
|
85
|
-
const binding = form.array(full(String(path)))
|
|
95
|
+
const binding = form.array<ArrayItem<ScopeValues, typeof path>>(full(String(path)))
|
|
86
96
|
return render({
|
|
87
97
|
append: binding.append,
|
|
88
98
|
remove: binding.remove,
|
|
@@ -107,29 +117,29 @@ const makeScope = <ScopeValues,>(
|
|
|
107
117
|
return null
|
|
108
118
|
}
|
|
109
119
|
const tag = String(current._tag)
|
|
110
|
-
const handlers = Fn.unsafeCoerce<
|
|
120
|
+
const handlers = Fn.unsafeCoerce<
|
|
121
|
+
typeof cases,
|
|
122
|
+
Record<string, (scope: ScopeHelpers<unknown>) => ReactNode>
|
|
123
|
+
>(cases)
|
|
111
124
|
const render = handlers[tag]
|
|
112
125
|
return render === undefined ? null : render(makeScope(form, variantPath))
|
|
113
126
|
},
|
|
114
127
|
}
|
|
115
128
|
}
|
|
116
129
|
|
|
117
|
-
export const make = <
|
|
118
|
-
|
|
119
|
-
C extends ContractWithForm<F>,
|
|
120
|
-
>(
|
|
121
|
-
contract: UiClass<C>,
|
|
130
|
+
export const make = <F extends AnyForm, C extends ContractWithForm<F>, N extends string>(
|
|
131
|
+
contract: UiClass<C, N>,
|
|
122
132
|
_form: F,
|
|
123
133
|
render: (
|
|
124
134
|
helpers: RootHelpers<F> & { readonly props: C['props'] },
|
|
125
135
|
slots: Parameters<ViewImpl<C>>[1],
|
|
126
136
|
events: Parameters<ViewImpl<C>>[2],
|
|
127
137
|
) => ReactNode,
|
|
128
|
-
):
|
|
138
|
+
): MadeView<C, N> =>
|
|
129
139
|
Ui.make(contract, (props, slots, events) =>
|
|
130
140
|
render(
|
|
131
141
|
{
|
|
132
|
-
...makeScope<Values<F>>(props.form, ''),
|
|
142
|
+
...makeScope<Values<F>>(Fn.unsafeCoerce(props.form), ''),
|
|
133
143
|
form: props.form,
|
|
134
144
|
props,
|
|
135
145
|
},
|