@embeddables/forms 0.0.5 → 0.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
@@ -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
 
@@ -29,13 +30,6 @@ import { analytics } from '@embeddables/analytics/react'
29
30
  import { forms, useForm, useFormField } 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,10 +71,17 @@ 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
 
@@ -95,11 +106,11 @@ const schema = {
95
106
 
96
107
  const core = initEmbeddables({
97
108
  projectId: 'proj_…',
98
- forms: [],
109
+ forms: { [schema.id]: schema },
99
110
  experiments: [],
100
111
  })
101
112
 
102
- const { getForm } = initForms({ core, schemas: { signup: schema } })
113
+ const { getForm } = initForms({ core })
103
114
  const form = getForm({ formId: 'signup' })
104
115
 
105
116
  const result = await form.set({ email: 'maria@gmail.com' })
@@ -119,9 +130,25 @@ Each form is one object: `id`, optional `name`, and `fields`. Supported field
119
130
  types: `text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json`.
120
131
 
121
132
  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`.
133
+ `pattern`, `oneOf`, and optional synchronous `validations.custom`.
134
+
135
+ A `select` or `multiselect` field declares its choices in a field-level `options`
136
+ array of `{ value, label?, exclusive? }`, and the field's `type` — not the name of
137
+ a rule — decides whether one or many of them may be selected. An option marked
138
+ `exclusive` on a `multiselect` cannot coexist with any other value: selecting it
139
+ clears the rest, and selecting a regular option afterwards clears it. Those two
140
+ types no longer accept `validations.oneOf`, which stays available on every other
141
+ field type. `em build` migrates it with a warning, carrying across the unique
142
+ non-empty string entries and dropping the rest — an entry that is not a string
143
+ could never have matched a stored value — so a field whose entries are all
144
+ unusable keeps no constraint at all. A schema that reaches `initForms` still
145
+ carrying `oneOf` on either type throws a `SchemaError`.
146
+
147
+ Use string patterns, not `RegExp` objects. Regex flags go in an optional leading
148
+ `(?flags)` prefix inside `pattern` — `(?u)^\p{L}+$` — using any of `dgimsuvy`;
149
+ `g` and `y` are stripped before compilation. For YAML/JSON schemas without inline
150
+ functions, pass validators via `customValidations: [{ formId, customValidations }]`
151
+ on `initForms` / `forms()`.
125
152
 
126
153
  ## Form API
127
154
 
@@ -150,22 +177,43 @@ backend writes are also best-effort: a failing persistence request never rejects
150
177
 
151
178
  | Option | Purpose |
152
179
  | ------ | ------- |
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` |
180
+ | `core` | Initialized `@embeddables/core` instance with form schemas in `forms` (required) |
181
+ | `customValidations` | Optional `{ formId, customValidations }[]` for schemas that need runtime validators |
182
+ | `serverFormData` | Optional `{ formId, serverFormData }[]`; SSR hydration overrides localStorage on overlapping keys |
155
183
  | `analyticsInstance` | Optional analytics client for event tracking only |
156
184
 
157
- **`getForm({ formId, customValidations? })`**
185
+ **`getForm({ formId })`**
158
186
 
159
187
  | Argument | Purpose |
160
188
  | ------ | ------- |
161
- | `formId` | Key of a schema declared on `initForms` (required); an unknown id throws `FormsError` |
162
- | `customValidations` | Per-field validators; overrides inline `validations.custom` |
189
+ | `formId` | Id of a schema registered on Core (required); an unknown id throws `FormsError` |
163
190
 
164
191
  `getForm` returns **one instance per form id**: the first call builds it, every
165
192
  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.
193
+ the same storage entry. The cache lives on the client, so hold one client per app.
194
+
195
+ ## Server (`@embeddables/forms/server`)
196
+
197
+ ```typescript
198
+ import { initEmbeddablesServer } from '@embeddables/core/server'
199
+ import { initFormsServer } from '@embeddables/forms/server'
200
+ import { schema as signupSchema } from './embeddables/_dist/forms/<form-id>/schema.js'
201
+
202
+ const server = initEmbeddablesServer({
203
+ projectId: '…',
204
+ forms: { [signupSchema.id]: signupSchema },
205
+ experiments: [],
206
+ })
207
+ const forms = initFormsServer({ server })
208
+
209
+ await forms.getForm({ formId: signupSchema.id }).set({ email: 'maria@gmail.com' })
210
+ const serverFormData = forms.getServerFormData()
211
+ // Pass serverFormData into client initForms({ serverFormData: [{ formId: signupSchema.id, serverFormData: serverFormData[signupSchema.id] }] })
212
+ ```
213
+
214
+ Server form instances expose `set`, `get`, `getAll`, and
215
+ `getValueByProtocolFieldId` only. Storage is in-memory; nothing is written to
216
+ `localStorage` or durable backends.
169
217
 
170
218
  ## Analytics
171
219