@playfast/reform-forms-react 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 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(CheckoutContract, CheckoutForm, ({ field, array, variant, form }) => (
20
- <Screen>
21
- {field('email').as(EmailInput, { label: 'Email' })}
22
- {array('items', ({ items, append }) => (
23
- <Stack>
24
- {items.map((item) => (
25
- <Row key={item.key}>
26
- {item.field('name').as(TextInput)}
27
- <IconButton onPress={item.remove} />
28
- </Row>
29
- ))}
30
- <Button onPress={() => append()}>Add</Button>
31
- </Stack>
32
- ))}
33
- {variant('payment', {
34
- Card: ({ field }) => field('cardNumber').as(CardNumberInput),
35
- Paypal: ({ field }) => field('email').as(EmailInput),
36
- })}
37
- <Button disabled={!form.canSubmit} onPress={form.submit}>Submit</Button>
38
- </Screen>
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
- "playbook": "./playbook",
4
- "version": "1.1.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
- "bugs": {
21
- "url": "https://github.com/playfast/reform/issues"
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
- "files": [
30
- "src",
31
- "README.md"
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
- "publishConfig": {
50
- "access": "public"
51
- }
51
+ "playbook": "./playbook"
52
52
  }
@@ -26,35 +26,51 @@ class CheckoutUi extends ui('CheckoutTypecheckUi')<CheckoutContract>() {}
26
26
  const TextInput: FieldComponent<string> = () => null
27
27
  const NumberInput: FieldComponent<number> = () => null
28
28
 
29
- export const ok: ViewImpl<CheckoutContract> = FormUi.make(CheckoutUi, CheckoutForm, ({ field, array, variant }) => (
30
- <>
31
- {field('email').as(TextInput)}
32
- {field('age').as(NumberInput)}
33
- {array('items', ({ items: rows }) => (
34
- <>{rows.map((row) => row.field('name').as(TextInput))}</>
35
- ))}
36
- {variant('payment', {
37
- Card: ({ field }) => field('cardNumber').as(TextInput),
38
- Paypal: ({ field }) => field('email').as(TextInput),
39
- })}
40
- </>
41
- ))
42
-
43
- export const badPath: ViewImpl<CheckoutContract> = FormUi.make(CheckoutUi, CheckoutForm, ({ field }) => (
44
- // @ts-expect-error unknown field path
45
- <>{field('missing').as(TextInput)}</>
46
- ))
47
-
48
- export const badComponent: ViewImpl<CheckoutContract> = FormUi.make(CheckoutUi, CheckoutForm, ({ field }) => (
49
- // @ts-expect-error age is a number field, not a string field
50
- <>{field('age').as(TextInput)}</>
51
- ))
52
-
53
- export const badVariant: ViewImpl<CheckoutContract> = FormUi.make(CheckoutUi, CheckoutForm, ({ variant }) => (
54
- <>
55
- {/* @ts-expect-error Paypal is required for exhaustive variant mapping */}
56
- {variant('payment', {
57
- Card: ({ field }) => field('cardNumber').as(TextInput),
58
- })}
59
- </>
60
- ))
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<{ readonly field: FieldBinding<A> } & P>
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>>(path: P) => FieldHandle<PathValue<ScopeValues, P>>
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: View<AnyForm>,
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) => makeFieldHandle(form.field(full(String(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<typeof cases, Record<string, (scope: ScopeHelpers<unknown>) => ReactNode>>(cases)
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
- F extends AnyForm,
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
- ): ViewImpl<C> =>
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
  },