@embeddables/forms 0.0.5 → 0.2.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/README.md CHANGED
@@ -14,8 +14,9 @@ npm install @embeddables/forms @embeddables/core
14
14
  ```
15
15
 
16
16
  For React apps, also install `react` (peer dependency) and wrap your tree in
17
- `EmbeddablesProvider` from `@embeddables/core/react`. Register forms through the
18
- provider's `modules` prop.
17
+ `EmbeddablesProvider` from `@embeddables/core/react`. Register the static module
18
+ through `modules`, then pass its runtime configuration through the provider's
19
+ top-level `forms` prop.
19
20
 
20
21
  ## React
21
22
 
@@ -26,16 +27,9 @@ Example:
26
27
  ```tsx
27
28
  import { EmbeddablesProvider } from '@embeddables/core/react'
28
29
  import { analytics } from '@embeddables/analytics/react'
29
- import { forms, useForm, useFormField } from '@embeddables/forms/react'
30
+ import { forms, useForm, useFormField, useFormFileUpload } from '@embeddables/forms/react'
30
31
  import { config } from './embeddables/_dist/config.ts'
31
32
 
32
- const schemas = {
33
- signup: {
34
- id: 'signup',
35
- fields: [{ key: 'email', label: 'Email', type: 'email', validations: { required: true } }],
36
- },
37
- } as const
38
-
39
33
  function SignupField() {
40
34
  // No type arguments: the id narrows the form, and the schema map comes from
41
35
  // the registry `em build` writes into embeddables/_dist/register.ts.
@@ -55,11 +49,21 @@ export function App() {
55
49
  return (
56
50
  <EmbeddablesProvider
57
51
  config={config}
58
- modules={[
59
- analytics(),
60
- // auto-wires analytics from Core when analytics() ran first
61
- forms({ schemas }),
62
- ]}
52
+ modules={[forms(), analytics()]}
53
+ forms={{
54
+ customValidations: [
55
+ {
56
+ formId: 'signup',
57
+ customValidations: {
58
+ // Return a message to fail, or null to pass.
59
+ email: ({ value }) =>
60
+ typeof value === 'string' && value.endsWith('@example.com')
61
+ ? 'example.com addresses are not accepted.'
62
+ : null,
63
+ },
64
+ },
65
+ ],
66
+ }}
63
67
  >
64
68
  <SignupField />
65
69
  </EmbeddablesProvider>
@@ -67,13 +71,49 @@ export function App() {
67
71
  }
68
72
  ```
69
73
 
70
- - `forms({ schemas, analyticsInstance? })` returns an `EmbeddablesReactModule`. When `analytics()` initialized
71
- the same Core instance first, Forms auto-wires `core.getAnalyticsInstance()`override
72
- with `analyticsInstance` on the factory if needed.
73
- - `useFormField` uses commit-on-blur: local typing updates draft state; `form.set()` runs on blur.
74
+ - `customValidations` and `serverFormData` go on the provider's `forms` prop.
75
+ - Form schemas come from Core (`config.forms`, populated by `em build`) — they are not
76
+ passed to the provider.
77
+ - A custom validator receives `{ value, values }` and returns a message to fail or
78
+ `null` to pass. `values` is the whole form, so a validator can read sibling fields.
79
+ - When `analytics()` is enabled, form events are tracked automatically — the order of
80
+ `modules` does not matter. Add `'forms'` to `excludeAnalyticsInModules` to turn that off.
81
+ - `useFormField` gives you two ways to write. `setValue` + `onBlur` is commit-on-blur, for
82
+ text inputs: typing updates local state and `form.set()` runs on blur. `commit(value)`
83
+ writes immediately, for controls with no meaningful blur — `select`, checkboxes, radios.
84
+ - `form.set({ field: undefined })` clears one field; `form.clear()` clears the whole form.
74
85
  - One live `FormInstance` per `schema.id` is shared across hooks in the same client.
75
86
  - Imperative consumers can also call `form.subscribe(listener)` to observe value and error changes.
76
87
 
88
+ ### File inputs
89
+
90
+ For `type: file` fields, use `useFormFileUpload`. It uploads through the publishable-key API, commits the returned `FormFileRef`, and exposes `inputProps` for a native `<input type="file">`.
91
+
92
+ Example:
93
+
94
+ ```tsx
95
+ function IdPhotoField() {
96
+ const { form } = useForm({ formId: 'intake' })
97
+ const { value, error, isLoading, uploadError, inputProps } = useFormFileUpload({
98
+ form,
99
+ key: 'id_photo',
100
+ })
101
+
102
+ return (
103
+ <label>
104
+ ID photo
105
+ <input {...inputProps} accept="image/jpeg,image/png" />
106
+ {isLoading && <span>Uploading…</span>}
107
+ {uploadError && <span>{uploadError}</span>}
108
+ {error?.[0]}
109
+ {value?.name && <span>{value.name}</span>}
110
+ </label>
111
+ )
112
+ }
113
+ ```
114
+
115
+ Imperative equivalent: `const ref = await form.uploadFile({ key: 'id_photo', file })` then `await form.set({ id_photo: ref })`.
116
+
77
117
  ## Quick start
78
118
 
79
119
  ```typescript
@@ -95,11 +135,11 @@ const schema = {
95
135
 
96
136
  const core = initEmbeddables({
97
137
  projectId: 'proj_…',
98
- forms: [],
138
+ forms: { [schema.id]: schema },
99
139
  experiments: [],
100
140
  })
101
141
 
102
- const { getForm } = initForms({ core, schemas: { signup: schema } })
142
+ const { getForm } = initForms({ core })
103
143
  const form = getForm({ formId: 'signup' })
104
144
 
105
145
  const result = await form.set({ email: 'maria@gmail.com' })
@@ -116,12 +156,33 @@ from `embeddables/_dist` are already typed.
116
156
  ## Schema
117
157
 
118
158
  Each form is one object: `id`, optional `name`, and `fields`. Supported field
119
- types: `text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json`.
159
+ types: `text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json`,
160
+ `file`.
120
161
 
121
162
  Declarative rules: `required`, `minLength`, `maxLength`, `min`, `max`,
122
- `pattern`, `patternFlags`, `oneOf`, and optional synchronous `validations.custom`.
123
- Use string patterns, not `RegExp` objects. For YAML/JSON schemas without inline
124
- functions, pass validators via `customValidations` on `getForm`.
163
+ `pattern`, `oneOf`, and optional synchronous `validations.custom`. File fields
164
+ also accept `validations.accept` (MIME list) and `validations.maxSize` (bytes).
165
+ Compatible field types may declare `protocolFieldId` to link a form field to a
166
+ protocol question. `json` fields cannot. `FormFileRef` is exported for typed
167
+ file values.
168
+
169
+ A `select` or `multiselect` field declares its choices in a field-level `options`
170
+ array of `{ value, label?, exclusive? }`, and the field's `type` — not the name of
171
+ a rule — decides whether one or many of them may be selected. An option marked
172
+ `exclusive` on a `multiselect` cannot coexist with any other value: selecting it
173
+ clears the rest, and selecting a regular option afterwards clears it. Those two
174
+ types no longer accept `validations.oneOf`, which stays available on every other
175
+ field type. `em build` migrates it with a warning, carrying across the unique
176
+ non-empty string entries and dropping the rest — an entry that is not a string
177
+ could never have matched a stored value — so a field whose entries are all
178
+ unusable keeps no constraint at all. A schema that reaches `initForms` still
179
+ carrying `oneOf` on either type throws a `SchemaError`.
180
+
181
+ Use string patterns, not `RegExp` objects. Regex flags go in an optional leading
182
+ `(?flags)` prefix inside `pattern` — `(?u)^\p{L}+$` — using any of `dgimsuvy`;
183
+ `g` and `y` are stripped before compilation. For YAML/JSON schemas without inline
184
+ functions, pass validators via `customValidations: [{ formId, customValidations }]`
185
+ on `initForms` / `forms()`.
125
186
 
126
187
  ## Form API
127
188
 
@@ -132,6 +193,7 @@ functions, pass validators via `customValidations` on `getForm`.
132
193
  | `getValueByProtocolFieldId(protocolFieldId)` | Read by schema `protocolFieldId` (typed like `get()` for the backing field; `undefined` if unknown or unset) |
133
194
  | `validate({ … })` | Check values without writing or tracking |
134
195
  | `submit()` | Validate all fields, best-effort durable R2 write, and emit `form:submitted` when analytics is configured |
196
+ | `uploadFile({ key, file, fileName? })` | Upload bytes for a `type: file` field; returns `FormFileRef` (caller commits with `.set()`) |
135
197
  | `errors()` | Current validation messages |
136
198
  | `clear()` | Remove this form's stored values |
137
199
 
@@ -150,22 +212,43 @@ backend writes are also best-effort: a failing persistence request never rejects
150
212
 
151
213
  | Option | Purpose |
152
214
  | ------ | ------- |
153
- | `core` | Initialized `@embeddables/core` instance (required) |
154
- | `schemas` | Every form the app can open, keyed by form id (required). Each schema's own `id` must equal its key `embeddables/_dist` already exports this shape as `forms` |
215
+ | `core` | Initialized `@embeddables/core` instance with form schemas in `forms` (required) |
216
+ | `customValidations` | Optional `{ formId, customValidations }[]` for schemas that need runtime validators |
217
+ | `serverFormData` | Optional `{ formId, serverFormData }[]`; SSR hydration overrides localStorage on overlapping keys |
155
218
  | `analyticsInstance` | Optional analytics client for event tracking only |
156
219
 
157
- **`getForm({ formId, customValidations? })`**
220
+ **`getForm({ formId })`**
158
221
 
159
222
  | Argument | Purpose |
160
223
  | ------ | ------- |
161
- | `formId` | Key of a schema declared on `initForms` (required); an unknown id throws `FormsError` |
162
- | `customValidations` | Per-field validators; overrides inline `validations.custom` |
224
+ | `formId` | Id of a schema registered on Core (required); an unknown id throws `FormsError` |
163
225
 
164
226
  `getForm` returns **one instance per form id**: the first call builds it, every
165
227
  later call returns that same one, so two handles can never race each other for
166
- the same storage entry. `customValidations` belong to the first call a later
167
- call that passes different validators gets the instance that already exists.
168
- The cache lives on the client, so hold one client per app.
228
+ the same storage entry. The cache lives on the client, so hold one client per app.
229
+
230
+ ## Server (`@embeddables/forms/server`)
231
+
232
+ ```typescript
233
+ import { initEmbeddablesServer } from '@embeddables/core/server'
234
+ import { initFormsServer } from '@embeddables/forms/server'
235
+ import { schema as signupSchema } from './embeddables/_dist/forms/<form-id>/schema.js'
236
+
237
+ const server = initEmbeddablesServer({
238
+ projectId: '…',
239
+ forms: { [signupSchema.id]: signupSchema },
240
+ experiments: [],
241
+ })
242
+ const forms = initFormsServer({ server })
243
+
244
+ await forms.getForm({ formId: signupSchema.id }).set({ email: 'maria@gmail.com' })
245
+ const serverFormData = forms.getServerFormData()
246
+ // Pass serverFormData into client initForms({ serverFormData: [{ formId: signupSchema.id, serverFormData: serverFormData[signupSchema.id] }] })
247
+ ```
248
+
249
+ Server form instances expose `set`, `get`, `getAll`, and
250
+ `getValueByProtocolFieldId` only. Storage is in-memory; nothing is written to
251
+ `localStorage` or durable backends.
169
252
 
170
253
  ## Analytics
171
254