@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 +116 -33
- package/dist/{form-9Tp91g6a.js → form-7wmC3G_q.js} +652 -221
- package/dist/form-7wmC3G_q.js.map +1 -0
- package/dist/{form-DzfCc5X2.cjs → form-CdIfHqNc.cjs} +675 -220
- package/dist/form-CdIfHqNc.cjs.map +1 -0
- package/dist/{index-qfBhdk5L.d.ts → form-x4EJcR9A.d.ts} +114 -122
- package/dist/form-x4EJcR9A.d.ts.map +1 -0
- package/dist/index-CmcgIxTH.d.ts +78 -0
- package/dist/index-CmcgIxTH.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/react.cjs +136 -15
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +49 -7
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +136 -16
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +50 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +49 -0
- package/dist/server.js.map +1 -0
- package/package.json +8 -3
- package/dist/form-9Tp91g6a.js.map +0 -1
- package/dist/form-DzfCc5X2.cjs.map +0 -1
- package/dist/index-qfBhdk5L.d.ts.map +0 -1
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
|
|
18
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
- `
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- `
|
|
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
|
|
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`, `
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
| `
|
|
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
|
|
220
|
+
**`getForm({ formId })`**
|
|
158
221
|
|
|
159
222
|
| Argument | Purpose |
|
|
160
223
|
| ------ | ------- |
|
|
161
|
-
| `formId` |
|
|
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.
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|