@parag.more.withsurface.com/forms-sdk 0.1.4
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 +63 -0
- package/dist/index.cjs +2737 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.mjs +2714 -0
- package/dist/index.mjs.map +7 -0
- package/dist/react/index.cjs +44 -0
- package/dist/react/index.cjs.map +7 -0
- package/dist/react/index.mjs +21 -0
- package/dist/react/index.mjs.map +7 -0
- package/dist/surface-forms-sdk.min.js +2 -0
- package/dist/surface-forms-sdk.min.js.map +7 -0
- package/dist/types/bind/bindForm.d.ts +43 -0
- package/dist/types/bind/bindForm.d.ts.map +1 -0
- package/dist/types/bind/bindForm.test.d.ts +2 -0
- package/dist/types/bind/bindForm.test.d.ts.map +1 -0
- package/dist/types/bind/bookingWidget.d.ts +35 -0
- package/dist/types/bind/bookingWidget.d.ts.map +1 -0
- package/dist/types/bind/bookingWidget.test.d.ts +2 -0
- package/dist/types/bind/bookingWidget.test.d.ts.map +1 -0
- package/dist/types/core/client/emailValidation.d.ts +23 -0
- package/dist/types/core/client/emailValidation.d.ts.map +1 -0
- package/dist/types/core/client/emailValidation.test.d.ts +2 -0
- package/dist/types/core/client/emailValidation.test.d.ts.map +1 -0
- package/dist/types/core/client/responses.d.ts +35 -0
- package/dist/types/core/client/responses.d.ts.map +1 -0
- package/dist/types/core/client/responses.test.d.ts +2 -0
- package/dist/types/core/client/responses.test.d.ts.map +1 -0
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts +2 -0
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts.map +1 -0
- package/dist/types/core/client/runtimeConfig.d.ts +72 -0
- package/dist/types/core/client/runtimeConfig.d.ts.map +1 -0
- package/dist/types/core/client/runtimeConfig.test.d.ts +2 -0
- package/dist/types/core/client/runtimeConfig.test.d.ts.map +1 -0
- package/dist/types/core/client/scheduler.contract.test.d.ts +2 -0
- package/dist/types/core/client/scheduler.contract.test.d.ts.map +1 -0
- package/dist/types/core/client/scheduler.d.ts +121 -0
- package/dist/types/core/client/scheduler.d.ts.map +1 -0
- package/dist/types/core/client/scheduler.test.d.ts +2 -0
- package/dist/types/core/client/scheduler.test.d.ts.map +1 -0
- package/dist/types/core/client/transport.d.ts +56 -0
- package/dist/types/core/client/transport.d.ts.map +1 -0
- package/dist/types/core/client/transport.test.d.ts +2 -0
- package/dist/types/core/client/transport.test.d.ts.map +1 -0
- package/dist/types/core/engine/answerAudit.d.ts +21 -0
- package/dist/types/core/engine/answerAudit.d.ts.map +1 -0
- package/dist/types/core/engine/answerAudit.test.d.ts +2 -0
- package/dist/types/core/engine/answerAudit.test.d.ts.map +1 -0
- package/dist/types/core/engine/answerStates.contract.test.d.ts +2 -0
- package/dist/types/core/engine/answerStates.contract.test.d.ts.map +1 -0
- package/dist/types/core/engine/answerStates.d.ts +92 -0
- package/dist/types/core/engine/answerStates.d.ts.map +1 -0
- package/dist/types/core/engine/formEngine.d.ts +145 -0
- package/dist/types/core/engine/formEngine.d.ts.map +1 -0
- package/dist/types/core/engine/formEngine.test.d.ts +2 -0
- package/dist/types/core/engine/formEngine.test.d.ts.map +1 -0
- package/dist/types/core/engine/redirect.d.ts +36 -0
- package/dist/types/core/engine/redirect.d.ts.map +1 -0
- package/dist/types/core/engine/redirect.test.d.ts +2 -0
- package/dist/types/core/engine/redirect.test.d.ts.map +1 -0
- package/dist/types/core/engine/responsePayload.contract.test.d.ts +2 -0
- package/dist/types/core/engine/responsePayload.contract.test.d.ts.map +1 -0
- package/dist/types/core/engine/responsePayload.d.ts +74 -0
- package/dist/types/core/engine/responsePayload.d.ts.map +1 -0
- package/dist/types/core/engine/responsePayload.test.d.ts +2 -0
- package/dist/types/core/engine/responsePayload.test.d.ts.map +1 -0
- package/dist/types/core/engine/urlData.contract.test.d.ts +2 -0
- package/dist/types/core/engine/urlData.contract.test.d.ts.map +1 -0
- package/dist/types/core/engine/urlData.d.ts +30 -0
- package/dist/types/core/engine/urlData.d.ts.map +1 -0
- package/dist/types/core/engine/urlData.test.d.ts +2 -0
- package/dist/types/core/engine/urlData.test.d.ts.map +1 -0
- package/dist/types/core/engine/urlPrefill.d.ts +21 -0
- package/dist/types/core/engine/urlPrefill.d.ts.map +1 -0
- package/dist/types/core/engine/urlPrefill.test.d.ts +2 -0
- package/dist/types/core/engine/urlPrefill.test.d.ts.map +1 -0
- package/dist/types/core/events/dropoffDetection.d.ts +21 -0
- package/dist/types/core/events/dropoffDetection.d.ts.map +1 -0
- package/dist/types/core/events/dropoffDetection.test.d.ts +2 -0
- package/dist/types/core/events/dropoffDetection.test.d.ts.map +1 -0
- package/dist/types/core/events/emitter.d.ts +23 -0
- package/dist/types/core/events/emitter.d.ts.map +1 -0
- package/dist/types/core/events/emitter.test.d.ts +2 -0
- package/dist/types/core/events/emitter.test.d.ts.map +1 -0
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +2 -0
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts.map +1 -0
- package/dist/types/core/events/formJourneyTracker.d.ts +69 -0
- package/dist/types/core/events/formJourneyTracker.d.ts.map +1 -0
- package/dist/types/core/events/formJourneyTracker.test.d.ts +2 -0
- package/dist/types/core/events/formJourneyTracker.test.d.ts.map +1 -0
- package/dist/types/core/events/pushEvents.contract.test.d.ts +2 -0
- package/dist/types/core/events/pushEvents.contract.test.d.ts.map +1 -0
- package/dist/types/core/events/pushEvents.d.ts +62 -0
- package/dist/types/core/events/pushEvents.d.ts.map +1 -0
- package/dist/types/core/events/pushEvents.test.d.ts +2 -0
- package/dist/types/core/events/pushEvents.test.d.ts.map +1 -0
- package/dist/types/core/events/startedDetection.d.ts +11 -0
- package/dist/types/core/events/startedDetection.d.ts.map +1 -0
- package/dist/types/core/events/startedDetection.test.d.ts +2 -0
- package/dist/types/core/events/startedDetection.test.d.ts.map +1 -0
- package/dist/types/core/events/viewDetection.d.ts +15 -0
- package/dist/types/core/events/viewDetection.d.ts.map +1 -0
- package/dist/types/core/events/viewDetection.test.d.ts +2 -0
- package/dist/types/core/events/viewDetection.test.d.ts.map +1 -0
- package/dist/types/core/identity/browserFingerprint.d.ts +40 -0
- package/dist/types/core/identity/browserFingerprint.d.ts.map +1 -0
- package/dist/types/core/identity/browserFingerprint.test.d.ts +2 -0
- package/dist/types/core/identity/browserFingerprint.test.d.ts.map +1 -0
- package/dist/types/core/identity/ensureIdentified.d.ts +27 -0
- package/dist/types/core/identity/ensureIdentified.d.ts.map +1 -0
- package/dist/types/core/identity/ensureIdentified.test.d.ts +2 -0
- package/dist/types/core/identity/ensureIdentified.test.d.ts.map +1 -0
- package/dist/types/core/identity/identify.contract.test.d.ts +2 -0
- package/dist/types/core/identity/identify.contract.test.d.ts.map +1 -0
- package/dist/types/core/identity/identify.d.ts +48 -0
- package/dist/types/core/identity/identify.d.ts.map +1 -0
- package/dist/types/core/identity/identify.test.d.ts +2 -0
- package/dist/types/core/identity/identify.test.d.ts.map +1 -0
- package/dist/types/core/identity/journey.contract.test.d.ts +2 -0
- package/dist/types/core/identity/journey.contract.test.d.ts.map +1 -0
- package/dist/types/core/identity/journey.d.ts +28 -0
- package/dist/types/core/identity/journey.d.ts.map +1 -0
- package/dist/types/core/identity/journey.test.d.ts +2 -0
- package/dist/types/core/identity/journey.test.d.ts.map +1 -0
- package/dist/types/core/identity/journey.testStubs.d.ts +5 -0
- package/dist/types/core/identity/journey.testStubs.d.ts.map +1 -0
- package/dist/types/core/identity/leadDataCache.d.ts +27 -0
- package/dist/types/core/identity/leadDataCache.d.ts.map +1 -0
- package/dist/types/core/identity/leadDataCache.test.d.ts +2 -0
- package/dist/types/core/identity/leadDataCache.test.d.ts.map +1 -0
- package/dist/types/enrichment/attachEnrichment.d.ts +35 -0
- package/dist/types/enrichment/attachEnrichment.d.ts.map +1 -0
- package/dist/types/enrichment/attachEnrichment.test.d.ts +2 -0
- package/dist/types/enrichment/attachEnrichment.test.d.ts.map +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/react/index.d.ts +2 -0
- package/dist/types/react/index.d.ts.map +1 -0
- package/dist/types/react/ssr.test.d.ts +2 -0
- package/dist/types/react/ssr.test.d.ts.map +1 -0
- package/dist/types/react/useSurfaceForm.d.ts +25 -0
- package/dist/types/react/useSurfaceForm.d.ts.map +1 -0
- package/dist/types/react/useSurfaceForm.test.d.ts +2 -0
- package/dist/types/react/useSurfaceForm.test.d.ts.map +1 -0
- package/dist/types/surfaceForms.d.ts +120 -0
- package/dist/types/surfaceForms.d.ts.map +1 -0
- package/dist/types/surfaceForms.test.d.ts +2 -0
- package/dist/types/surfaceForms.test.d.ts.map +1 -0
- package/dist/types/tracking/attachTracking.d.ts +35 -0
- package/dist/types/tracking/attachTracking.d.ts.map +1 -0
- package/dist/types/tracking/attachTracking.test.d.ts +2 -0
- package/dist/types/tracking/attachTracking.test.d.ts.map +1 -0
- package/dist/types/tracking/conversionRules.contract.test.d.ts +2 -0
- package/dist/types/tracking/conversionRules.contract.test.d.ts.map +1 -0
- package/dist/types/tracking/conversionRules.d.ts +87 -0
- package/dist/types/tracking/conversionRules.d.ts.map +1 -0
- package/dist/types/tracking/conversionRules.test.d.ts +2 -0
- package/dist/types/tracking/conversionRules.test.d.ts.map +1 -0
- package/dist/types/tracking/vendors.d.ts +46 -0
- package/dist/types/tracking/vendors.d.ts.map +1 -0
- package/dist/types/tracking/vendors.test.d.ts +2 -0
- package/dist/types/tracking/vendors.test.d.ts.map +1 -0
- package/dist/types/validation/attachEmailValidation.d.ts +8 -0
- package/dist/types/validation/attachEmailValidation.d.ts.map +1 -0
- package/dist/types/validation/attachEmailValidation.test.d.ts +2 -0
- package/dist/types/validation/attachEmailValidation.test.d.ts.map +1 -0
- package/docs/api.md +310 -0
- package/docs/reference/binding.md +116 -0
- package/docs/reference/clients.md +212 -0
- package/docs/reference/engine.md +193 -0
- package/docs/reference/events.md +93 -0
- package/docs/reference/identity.md +116 -0
- package/llms.txt +317 -0
- package/package.json +25 -0
- package/skills/surface-forms-sdk/SKILL.md +153 -0
- package/skills/surface-forms-sdk/debugging.md +64 -0
- package/skills/surface-forms-sdk/editing.md +67 -0
- package/skills/surface-forms-sdk/patterns.md +222 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Debugging an SDK form
|
|
2
|
+
|
|
3
|
+
Symptom → cause → check, distilled from real field runs. Surface never sees the customer's HTML,
|
|
4
|
+
so debugging always triangulates three views: the **browser console** (the SDK warns at runtime),
|
|
5
|
+
**`validate_form_html`** (the static gate), and **response read-back**
|
|
6
|
+
(`list_responses`/`get_response` — what actually landed). When in doubt, read a response back;
|
|
7
|
+
that is the only end-to-end truth.
|
|
8
|
+
|
|
9
|
+
## First moves, always
|
|
10
|
+
|
|
11
|
+
1. `form.state()` in the browser console — `currentStepId`, `answers`, `responseId`, `finished`
|
|
12
|
+
tell you most of the story. `form.capabilities` tells you what's actually wired
|
|
13
|
+
(enrichment/emailValidation/tracking) in this environment.
|
|
14
|
+
2. Attach a firehose while reproducing:
|
|
15
|
+
```js
|
|
16
|
+
[
|
|
17
|
+
"viewed",
|
|
18
|
+
"started",
|
|
19
|
+
"stepChanged",
|
|
20
|
+
"stepCompleted",
|
|
21
|
+
"completed",
|
|
22
|
+
"disqualified",
|
|
23
|
+
"saved",
|
|
24
|
+
"error",
|
|
25
|
+
].forEach((e) => form.on(e, (p) => console.log("[surface]", e, p)));
|
|
26
|
+
```
|
|
27
|
+
3. Re-run `validate_form_html` against the deployed HTML — drift since the last validation is the
|
|
28
|
+
most common root cause after any record edit.
|
|
29
|
+
|
|
30
|
+
## Symptom table
|
|
31
|
+
|
|
32
|
+
| Symptom | Likely cause | Check |
|
|
33
|
+
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `init` throws "could not fetch runtime config" | Wrong/missing `apiBaseUrl` — the error names the exact URL and host it tried | Use the `apiBaseUrl` from `get_sdk_binding_map`; curl the URL from the error |
|
|
35
|
+
| No responses at all | `preview: true` left in the deployed page (blocks ALL writes) | Grep the page for `preview`; `saved` event never fires in preview |
|
|
36
|
+
| No responses at all | Form never published — runtime-config serves the "not published yet" placeholder, so step/question ids don't match | Console shows unbound-questionId warnings; `publish_form` |
|
|
37
|
+
| Some answers missing | Binding drift: questionId unbound or misspelled | Console warning "record declares questionIds with no bound element"; `validate_form_html` |
|
|
38
|
+
| Some answers missing | Partial saves off and visitor abandoned mid-form | `trackPartialResponses` in settings; expected behavior |
|
|
39
|
+
| Finished response stored as a partial | SDK < 0.1.3 sent the terminal write fire-and-forget; current SDK awaits it — remaining cause is tearing the page down mid-flight | Upgrade; `await form.submit()` then `await form.flush()` before any programmatic close |
|
|
40
|
+
| 400 "Fields are missing or incorrectly formatted" | Hand-built `setAnswer` with the wrong shape (choice questions want structured lists, not label strings) | Compare the payload with the binding map's `answerShape`; prefer `data-*` binding, which builds choice answers itself |
|
|
41
|
+
| Choice answers wrong/empty | Radio/checkbox `value` attrs drifted from the record's option keys | `validate_form_html` warns option-key drift; diff against `choiceKeys` |
|
|
42
|
+
| Same email accepted one day, rejected the next | Fail-open verdict (`isValid: true, failed: true`) later resolving to a definitive one — expected waterfall behavior, not a bug | Check `failed` on the verdict; don't write tests expecting stable verdicts for fresh domains |
|
|
43
|
+
| Thank-you never shows | Record has no `endStepKind: "thank_you"` step, or the HTML has no container for it | Binding map: does a step carry `endStepKind`? Submit reveals it automatically when it exists |
|
|
44
|
+
| Disqualified visitors see the thank-you | No `endStepKind: "disqualified"` step; or page routes them manually to the wrong container | Same check; `disqualify()` only reveals a `disqualified` ending |
|
|
45
|
+
| Steps don't switch | `data-step-id` doesn't match record stepIds, or goto target invalid | Console warns on invalid `goToStep`; `validate_form_html` blocks unknown step ids |
|
|
46
|
+
| Duplicate responses per visitor | `init` called more than once without `destroy()` (SPA remounts) | One engine per page-life; call `form.destroy()` on teardown |
|
|
47
|
+
| GTM/GA4/Meta events not firing | Preview mode (tracking is fully blocked in preview), or settings not set | Deployed page without preview; `window.dataLayer` after a step submit; settings via `update_form_settings` |
|
|
48
|
+
| Conversions (ads rules) not firing | Rules gate on the trigger + URL conditions; terminal rules only fire on `completed`, never on disqualify; everything queues until the first `responseId` | Check rule trigger/conditions in the dashboard; watch for the `saved` event before expecting queued fires |
|
|
49
|
+
| Enrichment fields empty | The environment has no ENABLED enrichment provider — a record-declared `enrichmentSourceField` is then silently inert | `form.capabilities.enrichment` (false = inert); `debug: true` warns at boot; enable a provider in the dashboard |
|
|
50
|
+
| Lead attribution missing | `identify` blocked (preview) or ad-blocked; re-identify after SPA route change | `form.identify()`; network tab `identify` call |
|
|
51
|
+
|
|
52
|
+
## Reading a response back
|
|
53
|
+
|
|
54
|
+
`list_responses` (newest first) → `get_response` with the id. Confirm: every expected questionId
|
|
55
|
+
present, choice answers carry the structured list, `surfaceTagStatus: "sdk"` in meta, and — if a
|
|
56
|
+
booking step exists — the scheduler answer has `eventScheduled: true` plus the meeting fields.
|
|
57
|
+
A response that looks right here ends the client-side investigation.
|
|
58
|
+
|
|
59
|
+
## When the record and HTML disagree about reality
|
|
60
|
+
|
|
61
|
+
`get_sdk_binding_map` (draft AND `source: "live"`) is the arbiter: the live map is what the
|
|
62
|
+
deployed SDK actually fetched; the draft map is what the next publish will make true. An HTML
|
|
63
|
+
page validated against the draft but running against an older live record explains most
|
|
64
|
+
"validator says fine, runtime disagrees" reports — publish, or validate against `source: "live"`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Editing an existing SDK form
|
|
2
|
+
|
|
3
|
+
The record and the HTML live in different places (Surface vs the user's project) and only stay
|
|
4
|
+
consistent if every record edit is followed through to the HTML. This is the highest-drift-risk
|
|
5
|
+
journey in the SDK: a record edit that ships without its HTML counterpart silently degrades data
|
|
6
|
+
capture, and `validate_form_html` is the only gate that sees both sides.
|
|
7
|
+
|
|
8
|
+
## The loop
|
|
9
|
+
|
|
10
|
+
1. **Read the current record** — `get_form_structure` for the machine-address view (stepIds,
|
|
11
|
+
row/column addresses, questionIds), `get_sdk_binding_map` for what the HTML must bind. Note the
|
|
12
|
+
`revision` — pass it as `ifRevision` on every write so a concurrent edit fails loudly instead of
|
|
13
|
+
interleaving.
|
|
14
|
+
2. **Edit the record** — `update_component` / `add_component` (with `placement`) /
|
|
15
|
+
`remove_component` / `update_step` / `batch_edit` (all-or-nothing, one `ifRevision` for the
|
|
16
|
+
whole batch — prefer it for multi-part restructures). `move_component` refuses on SDK forms;
|
|
17
|
+
position is presentation and lives in the HTML.
|
|
18
|
+
3. **Re-fetch `get_sdk_binding_map` and diff against the HTML.** Every difference is work:
|
|
19
|
+
- **New question** → add its binding (`data-question-id`, `data-question-type`, field names per
|
|
20
|
+
`answerShape`). Until bound, it's a `validate_form_html` blocker (scheduler/file questions
|
|
21
|
+
excepted — their answers are written in code).
|
|
22
|
+
- **Removed question** → delete its binding; a stale `data-question-id` is a blocker (answers
|
|
23
|
+
would be dropped server-side).
|
|
24
|
+
- **Changed choice options** (`choiceKeys`) → update every radio/checkbox `value` to the new
|
|
25
|
+
keys. The SDK builds answers from control values, so stale values submit options that no
|
|
26
|
+
longer exist — `validate_form_html` warns on exactly this drift.
|
|
27
|
+
- **Changed IdentityInfo fields** (`fieldNames`) → update the `data-field-name` attributes.
|
|
28
|
+
- **New/removed steps** → add/remove the `data-step-id` container and adjust navigation markers
|
|
29
|
+
(`data-surface-goto-step` targets pointing at a removed step are blockers).
|
|
30
|
+
4. **`validate_form_html`** with the updated HTML against `source: "draft"` (the default). Fix
|
|
31
|
+
every blocker; re-run until clean.
|
|
32
|
+
5. **`validate_form`** if the edit touched more than presentation-adjacent content.
|
|
33
|
+
(`simulate_response` does not apply to SDK forms — it evaluates record routing.)
|
|
34
|
+
6. **`publish_form` with `confirm: true`.** Draft edits are invisible to the SDK until this —
|
|
35
|
+
and once a form has been published, `preview: true` serves the LIVE config, not the draft
|
|
36
|
+
(the draft path needs a never-published form plus the `previewToken` from
|
|
37
|
+
`get_sdk_binding_map`). So for edits to a published form, `validate_form_html` is the
|
|
38
|
+
pre-publish gate; there is no draft preview.
|
|
39
|
+
7. **Deploy the HTML change and publish the record together.** If the HTML deploy and
|
|
40
|
+
`publish_form` can't be simultaneous, sequence by change type — every ordering has a
|
|
41
|
+
divergence window, so pick the one whose window degrades least:
|
|
42
|
+
- **Additions → publish the record first.** During the window the old HTML doesn't show the
|
|
43
|
+
new question, so responses arrive without it — nothing errors, but a newly-required
|
|
44
|
+
question goes unanswered until the deploy lands (SDK ingest does not enforce `required`
|
|
45
|
+
server-side). If that gap is unacceptable, hold the publish until the deploy is ready to
|
|
46
|
+
go out with it. The reverse order is strictly worse: deployed bindings for a questionId
|
|
47
|
+
that isn't on the live record yet let visitors answer a visible question whose answer is
|
|
48
|
+
dropped server-side.
|
|
49
|
+
- **Removals or choice-key changes → deploy the HTML first.** The stale record briefly
|
|
50
|
+
declares a question nobody is asked (harmless); the reverse leaves visitors answering
|
|
51
|
+
removed questions or submitting stale option keys.
|
|
52
|
+
Never leave the two sides diverged longer than a deploy cycle.
|
|
53
|
+
8. **Read back one response** (`list_responses` / `get_response`) after the first post-edit
|
|
54
|
+
submission and confirm the changed answers landed with the expected shape.
|
|
55
|
+
|
|
56
|
+
## Record-side effects worth knowing
|
|
57
|
+
|
|
58
|
+
- Changing a question's text also updates its label (`questionName`) if the label was never
|
|
59
|
+
customized — the responses table follows automatically.
|
|
60
|
+
- Replacing an IdentityInfo's `fields[]` re-validates `enrichmentSourceField` against the new
|
|
61
|
+
keys; pointing it at a removed field is rejected in the same call (update or clear it together
|
|
62
|
+
with `fields`).
|
|
63
|
+
- Content edits rebuild content-derived state (choice lists) but preserve `questionId`s — existing
|
|
64
|
+
HTML bindings and analytics survive a re-word.
|
|
65
|
+
- `update_form_settings` changes (tracking ids, language, partial-response gates) propagate to the
|
|
66
|
+
edge on confirm and do NOT need a `publish_form` — but allow a few minutes for the CDN response
|
|
67
|
+
cache to roll over before verifying.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# SDK form HTML patterns
|
|
2
|
+
|
|
3
|
+
Concrete shapes for the authoring step. Substitute the real `environmentId`/`formId`/
|
|
4
|
+
`questionId`/`stepId`/`apiBaseUrl` values from `get_sdk_binding_map` — never invent ids.
|
|
5
|
+
Class names and layout are examples; keep the user's own markup style.
|
|
6
|
+
|
|
7
|
+
## Boot — script tag (plain HTML page)
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<form id="contact-form">
|
|
11
|
+
<!-- bindings here -->
|
|
12
|
+
</form>
|
|
13
|
+
<script src="/vendor/surface-forms-sdk.min.js"></script>
|
|
14
|
+
<script>
|
|
15
|
+
SurfaceFormsSDK.SurfaceForms.init({
|
|
16
|
+
environmentId: "env_xxx",
|
|
17
|
+
formId: "form_xxx",
|
|
18
|
+
apiBaseUrl: "https://…", // from get_sdk_binding_map — required off the default host
|
|
19
|
+
container: document.querySelector("#contact-form"),
|
|
20
|
+
preview: true, // remove before deploying
|
|
21
|
+
previewToken: "…", // with preview: true, serves a never-published form's DRAFT config
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
ESM projects: `import { SurfaceForms } from "@parag.more.withsurface.com/forms-sdk"` (substitute the
|
|
27
|
+
installed package name) and pass the same options.
|
|
28
|
+
|
|
29
|
+
## Single-step form
|
|
30
|
+
|
|
31
|
+
No `data-step-id` needed. The `<form>` container's native submit finishes, so a plain submit button works; `surface-submit-button` is for non-form containers or extra buttons.
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<form id="contact-form">
|
|
35
|
+
<label
|
|
36
|
+
>Work email
|
|
37
|
+
<input type="email" data-question-id="q_email" data-question-type="ShortInput" data-field-name="input" />
|
|
38
|
+
</label>
|
|
39
|
+
<label
|
|
40
|
+
>Company size
|
|
41
|
+
<select data-question-id="q_size" data-question-type="Dropdown">
|
|
42
|
+
<option>1-10</option>
|
|
43
|
+
<option>11-50</option>
|
|
44
|
+
</select>
|
|
45
|
+
</label>
|
|
46
|
+
<button type="submit">Send</button>
|
|
47
|
+
</form>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Note the email input: when the record's component is `ShortInput`, the explicit attributes
|
|
51
|
+
override the SDK's `type=email → EmailForm` inference. The Dropdown's `<select>` infers its
|
|
52
|
+
state key (`selected`); its option values must equal the record's option keys.
|
|
53
|
+
|
|
54
|
+
## Choice question (radios/checkboxes)
|
|
55
|
+
|
|
56
|
+
The SDK builds the structured answer from the whole group — `data-question-type` on the container is REQUIRED, each control's `value` must equal the record option key exactly, and no `data-field-name` is set:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<fieldset data-question-id="q_role" data-question-type="MultipleOptionsQuestion">
|
|
60
|
+
<label><input type="radio" name="role" value="Revenue / Sales Ops" /> Revenue / Sales Ops</label>
|
|
61
|
+
<label><input type="radio" name="role" value="Sales leadership" /> Sales leadership</label>
|
|
62
|
+
<label><input type="radio" name="role" value="Other" /> Other</label>
|
|
63
|
+
</fieldset>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Multi-select records use checkboxes with the same markup. `validate_form_html` cross-checks the control values against the record's options.
|
|
67
|
+
|
|
68
|
+
## Multi-step with a branch, disqualify, and a thank-you ending
|
|
69
|
+
|
|
70
|
+
One `data-step-id` container per record step — the SDK shows one at a time (mark the
|
|
71
|
+
containers after the first `hidden` to avoid a boot flash). The branch is the page's own
|
|
72
|
+
buttons; the record has no routing. The submit button lives on the LAST QUESTION step: after
|
|
73
|
+
the terminal write the SDK reveals the record's `endStepKind: "thank_you"` step by itself
|
|
74
|
+
(and the `"disqualified"` one on disqualify).
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<form id="qualify-form">
|
|
78
|
+
<section data-step-id="step_role">
|
|
79
|
+
<label
|
|
80
|
+
>Your role
|
|
81
|
+
<input data-question-id="q_role" />
|
|
82
|
+
</label>
|
|
83
|
+
<button type="button" data-surface-goto-step="step_company">I'm a buyer</button>
|
|
84
|
+
<button type="button" class="surface-disqualify-button">Just browsing</button>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section data-step-id="step_company" hidden>
|
|
88
|
+
<label
|
|
89
|
+
>Company
|
|
90
|
+
<input data-question-id="q_company" />
|
|
91
|
+
</label>
|
|
92
|
+
<button type="submit" class="surface-submit-button">Finish</button>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
<section data-step-id="step_done" hidden>
|
|
96
|
+
<p>Thanks — we'll be in touch.</p>
|
|
97
|
+
</section>
|
|
98
|
+
</form>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Dynamic branching instead of static buttons: keep a handle from `init` and call
|
|
102
|
+
`form.goToStep(stepId)` / `form.disqualify()` from your own logic, and declare
|
|
103
|
+
`data-surface-nav="js"` on any element so `validate_form_html` knows navigation/submit is
|
|
104
|
+
code-driven. Full event list for your own listeners: `viewed`, `started`, `stepChanged`,
|
|
105
|
+
`stepCompleted`, `completed`, `disqualified`, `saved`, `error`.
|
|
106
|
+
|
|
107
|
+
## Multi-field component
|
|
108
|
+
|
|
109
|
+
`data-field-name` resolves per control — set it on each input, not (only) the wrapper. `IdentityInfo` is the standard contact-details component; bind only the fields the record declares (its `fieldNames` in the binding map):
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<div data-question-id="q_contact" data-question-type="IdentityInfo">
|
|
113
|
+
<input data-field-name="firstName" placeholder="First name" />
|
|
114
|
+
<input data-field-name="lastName" placeholder="Last name" />
|
|
115
|
+
<input type="email" data-field-name="workEmailAddress" placeholder="Work email" />
|
|
116
|
+
</div>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Scheduler step (Surface scheduler)
|
|
120
|
+
|
|
121
|
+
A `CalendlyScreen` with `calendarType: "Surface"` renders through the SDK's own unstyled widget — never iframe the hosted booking page, and never bind the question in HTML (the `onComplete` writes the answer):
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<section data-step-id="step_book" hidden>
|
|
125
|
+
<div id="booking"></div>
|
|
126
|
+
</section>
|
|
127
|
+
<script>
|
|
128
|
+
const { createSchedulerClient, createTransport, mountBookingWidget, toSchedulerAnswerState } =
|
|
129
|
+
SurfaceFormsSDK;
|
|
130
|
+
const scheduler = createSchedulerClient({ transport: createTransport({ apiBaseUrl }), preview: PREVIEW });
|
|
131
|
+
mountBookingWidget({
|
|
132
|
+
container: document.querySelector("#booking"),
|
|
133
|
+
scheduler,
|
|
134
|
+
eventTypeId: "evt_xxx", // the record's CalendlyScreen content.eventTypeId
|
|
135
|
+
identity: { responseId: form.state().responseId ?? undefined },
|
|
136
|
+
onComplete(booking) {
|
|
137
|
+
form.setAnswer("q_booking", { ...toSchedulerAnswerState(booking), type: "CalendlyScreen" });
|
|
138
|
+
form.submit();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
</script>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The widget emits semantic markup under stable `surface-booking-*` classes (full DOM tree in
|
|
145
|
+
`llms.txt`) and injects no CSS — style from the page. Labels are locale-formatted by default
|
|
146
|
+
with the raw value on each element's `data-value`; pass `formatters: { month?, date?, time? }`
|
|
147
|
+
to override the copy. In preview mode the client returns a synthetic booking, so the flow is
|
|
148
|
+
testable offline.
|
|
149
|
+
|
|
150
|
+
## React adapter (SPA)
|
|
151
|
+
|
|
152
|
+
Headless mode: `init` without `container` (no `data-*` binding), one engine per mounted form, `destroy()` on unmount — remounting without destroy creates duplicate responses. Answers are set imperatively with the exact `answerShape` from the binding map (choice questions included — there is no group auto-build without the bind layer).
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { SurfaceForms, type SurfaceForm } from "@parag.more.withsurface.com/forms-sdk";
|
|
156
|
+
import { useSurfaceForm } from "@parag.more.withsurface.com/forms-sdk/react";
|
|
157
|
+
import { useEffect, useState } from "react";
|
|
158
|
+
|
|
159
|
+
export function QualifyForm() {
|
|
160
|
+
const [form, setForm] = useState<SurfaceForm | null>(null);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
let handle: SurfaceForm | null = null;
|
|
164
|
+
let disposed = false;
|
|
165
|
+
void SurfaceForms.init({
|
|
166
|
+
environmentId: "env_xxx",
|
|
167
|
+
formId: "form_xxx",
|
|
168
|
+
apiBaseUrl: "https://…", // from get_sdk_binding_map
|
|
169
|
+
}).then((created) => {
|
|
170
|
+
if (disposed) created.destroy();
|
|
171
|
+
else {
|
|
172
|
+
handle = created;
|
|
173
|
+
setForm(created);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return () => {
|
|
177
|
+
disposed = true;
|
|
178
|
+
handle?.destroy();
|
|
179
|
+
};
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
if (!form) return null;
|
|
183
|
+
return <QualifySteps form={form} />;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function QualifySteps({ form }: { form: SurfaceForm }) {
|
|
187
|
+
const { state, setAnswer, goToStep, disqualify, submit } = useSurfaceForm(form);
|
|
188
|
+
const role = (state.answers[state.currentStepId]?.q_role as { input?: string })?.input ?? "";
|
|
189
|
+
|
|
190
|
+
if (state.currentStepId === "step_role") {
|
|
191
|
+
return (
|
|
192
|
+
<section>
|
|
193
|
+
<input
|
|
194
|
+
value={role}
|
|
195
|
+
onChange={(e) => setAnswer("q_role", { type: "ShortInput", input: e.target.value })}
|
|
196
|
+
/>
|
|
197
|
+
<button onClick={() => goToStep("step_company")}>I'm a buyer</button>
|
|
198
|
+
<button onClick={() => disqualify()}>Just browsing</button>
|
|
199
|
+
</section>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (state.currentStepId === "step_done") return <p>Thanks — we'll be in touch.</p>;
|
|
203
|
+
return (
|
|
204
|
+
<section>
|
|
205
|
+
{/* … company inputs … */}
|
|
206
|
+
<button onClick={() => submit()}>Finish</button>
|
|
207
|
+
</section>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`state` is `useSyncExternalStore`-backed: referentially stable between engine mutations, safe to render from directly. `submit()`/`disqualify()` move `state.currentStepId` to the record's matching ending step, so the same step-switch renders the confirmation. On SPA route changes call `form.identify()` to re-attribute the visitor; on programmatic teardown after a submit flow, `await form.flush()` first.
|
|
213
|
+
|
|
214
|
+
## Tracking setup
|
|
215
|
+
|
|
216
|
+
Tracking is settings-driven and needs no page code — the SDK bootstraps the vendors and fires the same events as native forms (`SurfaceFormStarted`, `SurfaceFormStepSubmitted`, `SurfaceFormSubmitEvent` on GTM's dataLayer; Meta `trackCustom`; GA4 events).
|
|
217
|
+
|
|
218
|
+
- **Set via MCP**: `update_form_settings` accepts `gtmId`, `gaMeasurementId`, `metaPixelId`, `trackPartialResponses`, `trackCookie` (two-phase: preview → `confirm: true`). Ad-conversion rules (`adsConversions`) and HubSpot are **not settable via MCP** — the user configures those in the dashboard (Form settings → tracking).
|
|
219
|
+
- **Or pass at init**: `SurfaceForms.init({ settings: { gtmId, gaMeasurementId, metaPixelId, adsConversions } })` — caller values win over the record's, which lets a customer wire conversions entirely client-side.
|
|
220
|
+
- **Preview blocks tracking entirely** (no vendor scripts load, nothing fires) — always verify on a deployed non-preview page.
|
|
221
|
+
- **Verify**: complete a step, then inspect `window.dataLayer` for `SurfaceFormStepSubmitted` (its `stepId` is the real step id and the submit event carries `formId`). Conversion rules fire once per rule per response, queue until the first save produces a `responseId`, and terminal rules fire on completion only — never on disqualify.
|
|
222
|
+
- `form.capabilities.tracking` tells you whether any tracking config is active for this init.
|