@parag.more.withsurface.com/forms-sdk 0.1.5 → 0.1.7
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 +12 -6
- package/dist/index.cjs +1 -2736
- package/dist/index.mjs +1 -2713
- package/dist/react/index.cjs +1 -43
- package/dist/react/index.mjs +1 -20
- package/dist/surface-forms-sdk.min.js +1 -1
- package/dist/types/bind/bindForm.d.ts +2 -0
- package/dist/types/bind/bookingWidget.d.ts +29 -6
- package/dist/types/bind/scheduler/embedUtils.d.ts +51 -0
- package/dist/types/bind/scheduler/mountScheduler.d.ts +2 -0
- package/dist/types/bind/scheduler/providers/cal.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/calendly.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/chilical.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/clari.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/hubspot.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/index.d.ts +4 -0
- package/dist/types/bind/scheduler/providers/reclaimai.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/revenuehero.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/savvycal.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/surface.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/zoom.d.ts +3 -0
- package/dist/types/bind/scheduler/styles.d.ts +3 -0
- package/dist/types/bind/scheduler/types.d.ts +65 -0
- package/dist/types/core/client/emailValidation.d.ts +3 -0
- package/dist/types/core/client/responses.d.ts +3 -0
- package/dist/types/core/client/runtimeConfig.d.ts +40 -0
- package/dist/types/core/client/scheduler.d.ts +4 -0
- package/dist/types/core/client/transport.d.ts +4 -0
- package/dist/types/core/engine/answerAudit.d.ts +3 -0
- package/dist/types/core/engine/answerStates.d.ts +4 -0
- package/dist/types/core/engine/formEngine.d.ts +18 -0
- package/dist/types/core/engine/redirect.d.ts +1 -0
- package/dist/types/core/engine/responsePayload.d.ts +4 -0
- package/dist/types/core/engine/schedulerEvents.d.ts +40 -0
- package/dist/types/core/engine/urlData.d.ts +2 -0
- package/dist/types/core/engine/urlPrefill.d.ts +1 -0
- package/dist/types/core/events/dropoffDetection.d.ts +1 -0
- package/dist/types/core/events/emitter.d.ts +1 -0
- package/dist/types/core/events/formJourneyTracker.d.ts +1 -0
- package/dist/types/core/events/pushEvents.d.ts +2 -0
- package/dist/types/core/events/startedDetection.d.ts +1 -0
- package/dist/types/core/events/viewDetection.d.ts +1 -0
- package/dist/types/core/identity/browserFingerprint.d.ts +3 -0
- package/dist/types/core/identity/ensureIdentified.d.ts +1 -0
- package/dist/types/core/identity/identify.d.ts +1 -0
- package/dist/types/core/identity/journey.d.ts +3 -0
- package/dist/types/core/identity/leadDataCache.d.ts +1 -0
- package/dist/types/enrichment/attachEnrichment.d.ts +1 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/react/useSurfaceForm.d.ts +1 -0
- package/dist/types/surfaceForms.d.ts +28 -1
- package/dist/types/tracking/attachTracking.d.ts +1 -0
- package/dist/types/tracking/conversionRules.d.ts +3 -0
- package/dist/types/tracking/vendors.d.ts +3 -0
- package/docs/api.md +59 -43
- package/docs/reference/binding.md +86 -13
- package/docs/reference/clients.md +21 -1
- package/docs/reference/engine.md +14 -7
- package/docs/reference/events.md +4 -1
- package/llms.txt +95 -29
- package/package.json +1 -1
- package/skills/surface-forms-sdk/SKILL.md +66 -26
- package/skills/surface-forms-sdk/debugging.md +21 -19
- package/skills/surface-forms-sdk/patterns.md +71 -5
- package/dist/types/bind/bindForm.test.d.ts +0 -1
- package/dist/types/bind/bookingWidget.test.d.ts +0 -1
- package/dist/types/core/client/emailValidation.test.d.ts +0 -1
- package/dist/types/core/client/responses.test.d.ts +0 -1
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts +0 -1
- package/dist/types/core/client/runtimeConfig.test.d.ts +0 -1
- package/dist/types/core/client/scheduler.contract.test.d.ts +0 -1
- package/dist/types/core/client/scheduler.test.d.ts +0 -1
- package/dist/types/core/client/transport.test.d.ts +0 -1
- package/dist/types/core/engine/answerAudit.test.d.ts +0 -1
- package/dist/types/core/engine/answerStates.contract.test.d.ts +0 -1
- package/dist/types/core/engine/formEngine.test.d.ts +0 -1
- package/dist/types/core/engine/redirect.test.d.ts +0 -1
- package/dist/types/core/engine/responsePayload.contract.test.d.ts +0 -1
- package/dist/types/core/engine/responsePayload.test.d.ts +0 -1
- package/dist/types/core/engine/urlData.contract.test.d.ts +0 -1
- package/dist/types/core/engine/urlData.test.d.ts +0 -1
- package/dist/types/core/engine/urlPrefill.test.d.ts +0 -1
- package/dist/types/core/events/dropoffDetection.test.d.ts +0 -1
- package/dist/types/core/events/emitter.test.d.ts +0 -1
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +0 -1
- package/dist/types/core/events/formJourneyTracker.test.d.ts +0 -1
- package/dist/types/core/events/pushEvents.contract.test.d.ts +0 -1
- package/dist/types/core/events/pushEvents.test.d.ts +0 -1
- package/dist/types/core/events/startedDetection.test.d.ts +0 -1
- package/dist/types/core/events/viewDetection.test.d.ts +0 -1
- package/dist/types/core/identity/browserFingerprint.test.d.ts +0 -1
- package/dist/types/core/identity/ensureIdentified.test.d.ts +0 -1
- package/dist/types/core/identity/identify.contract.test.d.ts +0 -1
- package/dist/types/core/identity/identify.test.d.ts +0 -1
- package/dist/types/core/identity/journey.contract.test.d.ts +0 -1
- package/dist/types/core/identity/journey.test.d.ts +0 -1
- package/dist/types/core/identity/journey.testStubs.d.ts +0 -3
- package/dist/types/core/identity/leadDataCache.test.d.ts +0 -1
- package/dist/types/enrichment/attachEnrichment.test.d.ts +0 -1
- package/dist/types/react/ssr.test.d.ts +0 -1
- package/dist/types/react/useSurfaceForm.test.d.ts +0 -1
- package/dist/types/surfaceForms.test.d.ts +0 -1
- package/dist/types/tracking/attachTracking.test.d.ts +0 -1
- package/dist/types/tracking/conversionRules.contract.test.d.ts +0 -1
- package/dist/types/tracking/conversionRules.test.d.ts +0 -1
- package/dist/types/tracking/vendors.test.d.ts +0 -1
- package/dist/types/validation/attachEmailValidation.test.d.ts +0 -1
|
@@ -35,8 +35,30 @@ references alongside this file — read the one that matches the job:
|
|
|
35
35
|
docs may say `@parag.more.withsurface.com/forms-sdk`; the installed name wins.
|
|
36
36
|
- Bundler `target: "esnext"` (top-level await), or wrap `init` in an async fn.
|
|
37
37
|
|
|
38
|
+
## Decisions to confirm with the form owner (before authoring)
|
|
39
|
+
|
|
40
|
+
The record can't express these and the SDK deliberately doesn't decide them —
|
|
41
|
+
ask, don't assume. One question each, with the default stated:
|
|
42
|
+
|
|
43
|
+
- **Invalid email address** — default: annotate only (`emailValidation: true`;
|
|
44
|
+
the visitor continues, the response carries `isEmailValid: false` for the
|
|
45
|
+
owner's workflows). Alternatives: soft-warn inline, block the step with a
|
|
46
|
+
message, or `disqualify()`. Never gate navigation on `validateEmail` unless
|
|
47
|
+
the owner picked block/disqualify.
|
|
48
|
+
- **Required fields** — the binding map's `required` is advisory: the SDK
|
|
49
|
+
never blocks a step, your page does (native forms block). Confirm which
|
|
50
|
+
fields block Continue and what the message says.
|
|
51
|
+
- **Selection limits** — `selectionLimit` on a multi-select is not enforced
|
|
52
|
+
by the SDK; enforce it in markup (disable extra checkboxes) if the owner
|
|
53
|
+
wants parity with the hosted form.
|
|
54
|
+
|
|
38
55
|
## The workflow
|
|
39
56
|
|
|
57
|
+
Binding to an EXISTING published record (the common integration case): skip
|
|
58
|
+
step 2, read the contract with `get_sdk_binding_map` (`source: "live"` when
|
|
59
|
+
you don't own the draft), then author → validate → deploy. Preview (6) needs
|
|
60
|
+
a draft you own; publish (7) only when you changed the record.
|
|
61
|
+
|
|
40
62
|
1. **Plan (optional).** Vague requirements → `plan_form` with `sdk: true`.
|
|
41
63
|
Branching stays prose (`routingIntent`) — your page implements it; never
|
|
42
64
|
encode structured routing for an SDK form.
|
|
@@ -53,7 +75,13 @@ references alongside this file — read the one that matches the job:
|
|
|
53
75
|
5. **Validate.** `validate_form_html` until clean. Scheduler/file questions
|
|
54
76
|
report as notes (no binding needed — code writes their answers). If your
|
|
55
77
|
navigation/submit is JS-driven, declare `data-surface-nav="js"` on any
|
|
56
|
-
element instead of adding fake buttons.
|
|
78
|
+
element instead of adding fake buttons. For a JSX/React page the check is a
|
|
79
|
+
one-shot against transcribed markup — ALSO commit a drift test: extract
|
|
80
|
+
every `data-question-id` / `data-step-id` / choice `value` from the
|
|
81
|
+
component source (regex over the file is fine) and assert them against the
|
|
82
|
+
binding map's ids saved in your contract module. Binding drift is silent
|
|
83
|
+
data loss; only a committed test catches the refactor that renames a
|
|
84
|
+
question wrapper.
|
|
57
85
|
6. **Preview before publishing.** `init({ preview: true, previewToken })`
|
|
58
86
|
serves the DRAFT record with zero network writes; the scheduler returns a
|
|
59
87
|
synthetic booking. Skip `simulate_response` — it evaluates record routing,
|
|
@@ -78,22 +106,39 @@ references alongside this file — read the one that matches the job:
|
|
|
78
106
|
explicit `data-field-name` per control.
|
|
79
107
|
- **Boot flash** — mark every step container after the first `hidden`; the SDK
|
|
80
108
|
reveals the current one after async init.
|
|
109
|
+
- **Continue is instant** — `next()`/`goToStep()` switch the step immediately
|
|
110
|
+
and the partial save lands behind it; never disable the CTA or wait for
|
|
111
|
+
`saved` before showing the next step. Only `submit()`/`disqualify()` await
|
|
112
|
+
their write — disable THAT button until the promise resolves.
|
|
81
113
|
- **Teardown** — the finishing write is awaited (`submit()` resolving means the
|
|
82
|
-
server has it), but partial saves
|
|
83
|
-
programmatic page close or your test harness records a partial.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
server has it), but partial saves run in the background: `await form.flush()`
|
|
115
|
+
before a programmatic page close or your test harness records a partial. In
|
|
116
|
+
a React effect cleanup call `destroy()` alone — a cleanup cannot await, and
|
|
117
|
+
queued saves survive an SPA unmount; `flush()` is for closing the whole
|
|
118
|
+
page/process.
|
|
119
|
+
- **Email validation never blocks by default** — `emailValidation: true`
|
|
120
|
+
annotates the response; an invalid address still continues. Gate with
|
|
121
|
+
`validateEmail` only when the owner chose block/disqualify (Decisions
|
|
122
|
+
above). Verdicts fail open (`{ isValid: true, failed: true }` on vendor
|
|
123
|
+
outage) and a fresh domain may resolve to a definitive verdict later —
|
|
124
|
+
check `failed` to soft-warn; don't write tests expecting stable verdicts.
|
|
88
125
|
- **Enrichment can be silently inert** — check `form.capabilities.enrichment`
|
|
89
126
|
before promising enriched fields; a declared source field without an enabled
|
|
90
127
|
provider does nothing (`debug: true` warns).
|
|
91
128
|
- **Custom values on the response** — `form.mergeMeta({ qualificationScore })`;
|
|
92
129
|
reserved SDK keys are dropped. Don't invent external-state types.
|
|
93
|
-
- **Scheduler** —
|
|
94
|
-
the
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
- **Scheduler** — default to ONE call: `mountScheduler({ form, container })`
|
|
131
|
+
renders the record's scheduler (any provider — Surface widget default-styled,
|
|
132
|
+
or the third-party embed), tracks the lifecycle on `form.on("scheduler")`,
|
|
133
|
+
and persists the booking automatically. Never iframe the hosted booking
|
|
134
|
+
page. Build custom UI only when explicitly asked: `mountBookingWidget` +
|
|
135
|
+
`setAnswer({ ...toSchedulerAnswerState(b), type: "CalendlyScreen" })` in
|
|
136
|
+
`onComplete`. Traps: Clari emits no booking signal (embed-only);
|
|
137
|
+
Calendly/Zoom/Reclaim mounts create the response early (their embed URLs
|
|
138
|
+
carry the responseId); `styled: false` opts out of the injected scoped CSS.
|
|
139
|
+
The Surface widget opens on the first month with availability — a
|
|
140
|
+
persistent "No availability" means the event type really has none (host
|
|
141
|
+
schedule / max advance), not a wrong month.
|
|
97
142
|
- **Business logic is invisible to Surface tooling.** Your qualification/
|
|
98
143
|
branching thresholds live in page code no validator can see — test them in a
|
|
99
144
|
real browser walk (both branches, plus a boundary case).
|
|
@@ -101,14 +146,13 @@ type: "CalendlyScreen" })`. Labels are human-readable with raw values in
|
|
|
101
146
|
## Starter template
|
|
102
147
|
|
|
103
148
|
```html
|
|
104
|
-
<div id="form"
|
|
149
|
+
<div id="form" hidden>
|
|
105
150
|
<section data-step-id="STEP_1_ID">
|
|
106
151
|
<label
|
|
107
152
|
>Work email <input type="email" data-question-id="Q_EMAIL_ID" />
|
|
108
153
|
<!-- EmailForm infers type+field -->
|
|
109
154
|
</label>
|
|
110
|
-
<
|
|
111
|
-
<button type="button" id="to-step-2">Continue</button>
|
|
155
|
+
<button type="button" class="surface-next-button">Continue</button>
|
|
112
156
|
</section>
|
|
113
157
|
|
|
114
158
|
<section data-step-id="STEP_2_ID" hidden>
|
|
@@ -131,23 +175,19 @@ const form = await SurfaceForms.init({
|
|
|
131
175
|
formId: "FORM_ID",
|
|
132
176
|
apiBaseUrl: "API_BASE_URL", // from get_sdk_binding_map — do not omit
|
|
133
177
|
container: document.querySelector("#form"),
|
|
134
|
-
emailValidation: true, //
|
|
178
|
+
emailValidation: true, // default policy: annotate the response, never block
|
|
135
179
|
});
|
|
136
180
|
document.querySelector("#form").hidden = false;
|
|
137
181
|
|
|
138
|
-
// JS-driven forward nav: gate on the email verdict, then branch yourself.
|
|
139
|
-
document.querySelector("#to-step-2").addEventListener("click", async () => {
|
|
140
|
-
const email = document.querySelector("[data-question-id=Q_EMAIL_ID]").value;
|
|
141
|
-
const { isValid, reason } = await form.validateEmail(email);
|
|
142
|
-
if (!isValid) return showError(`That address looks undeliverable (${reason}).`);
|
|
143
|
-
form.next(); // or form.goToStep("…") / form.disqualify() per your rules
|
|
144
|
-
});
|
|
145
|
-
|
|
146
182
|
form.on("saved", ({ responseId, resumeToken }) =>
|
|
147
183
|
localStorage.setItem("surface-resume", JSON.stringify({ responseId, resumeToken }))
|
|
148
184
|
);
|
|
149
185
|
```
|
|
150
186
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
187
|
+
Declarative markers (`surface-next-button`, `data-surface-goto-step`,
|
|
188
|
+
`surface-disqualify-button`, `surface-submit-button`) cover linear and static
|
|
189
|
+
branching. Owner-requested gates (block on an invalid email, required-field
|
|
190
|
+
checks) and dynamic branching are JS: call `form.next()` /
|
|
191
|
+
`form.goToStep(…)` / `form.disqualify()` from your own handler and put
|
|
192
|
+
`data-surface-nav="js"` on any element so `validate_form_html` knows —
|
|
193
|
+
snippets in `patterns.md` §Invalid-email policy.
|
|
@@ -29,25 +29,27 @@ that is the only end-to-end truth.
|
|
|
29
29
|
|
|
30
30
|
## Symptom table
|
|
31
31
|
|
|
32
|
-
| Symptom | Likely cause
|
|
33
|
-
| ------------------------------------------------- |
|
|
34
|
-
| `init` throws "could not fetch runtime config" | Wrong/missing `apiBaseUrl` — the error names the exact URL and host it tried
|
|
35
|
-
| No responses at all | `preview: true` left in the deployed page (blocks ALL writes)
|
|
36
|
-
| No responses at all | Form never published — runtime-config serves the "not published yet" placeholder, so step/question ids don't match
|
|
37
|
-
| Some answers missing | Binding drift: questionId unbound or misspelled
|
|
38
|
-
| Some answers missing | Partial saves off and visitor abandoned mid-form
|
|
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
|
|
40
|
-
| 400 "Fields are missing or incorrectly formatted" | Hand-built `setAnswer` with the wrong shape (choice questions want structured lists, not label strings)
|
|
41
|
-
| Choice answers wrong/empty | Radio/checkbox `value` attrs drifted from the record's option keys
|
|
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
|
|
43
|
-
| Thank-you never shows | Record has no `endStepKind: "thank_you"` step, or the HTML has no container for it
|
|
44
|
-
| Disqualified visitors see the thank-you | No `endStepKind: "disqualified"` step; or page routes them manually to the wrong container
|
|
45
|
-
| Steps don't switch | `data-step-id` doesn't match record stepIds, or goto target invalid
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
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
|
+
| Continue looks dead, then the step switches later | Earlier SDK builds awaited the partial save before switching; the current SDK switches immediately and saves behind (`debug: true` logs `next requested`) | Upgrade; `next()` resolves on the step change, `saved` fires when the write lands |
|
|
47
|
+
| Scheduler says "No availability this month" | The event type has no bookable slots (host schedule, max advance, no connected host) — the widget already opens on the first month the summary reports | `getAvailabilitySummary` → `next_available_date`; the host's schedule in the dashboard; `preview: true` mocks slots |
|
|
48
|
+
| Duplicate responses per visitor | `init` called more than once without `destroy()` (SPA remounts) | One engine per page-life; call `form.destroy()` on teardown |
|
|
49
|
+
| 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` |
|
|
50
|
+
| 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 |
|
|
51
|
+
| 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 |
|
|
52
|
+
| Lead attribution missing | `identify` blocked (preview) or ad-blocked; re-identify after SPA route change | `form.identify()`; network tab `identify` call |
|
|
51
53
|
|
|
52
54
|
## Reading a response back
|
|
53
55
|
|
|
@@ -104,6 +104,44 @@ Dynamic branching instead of static buttons: keep a handle from `init` and call
|
|
|
104
104
|
code-driven. Full event list for your own listeners: `viewed`, `started`, `stepChanged`,
|
|
105
105
|
`stepCompleted`, `completed`, `disqualified`, `saved`, `error`.
|
|
106
106
|
|
|
107
|
+
Timing: `next()`/`goToStep()` switch the step immediately — the partial save lands behind
|
|
108
|
+
them, so the visitor never waits on the network. `submit()`/`disqualify()` await the finishing
|
|
109
|
+
write before the ending shows; disable that button while the promise is pending:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
submitButton.addEventListener("click", async () => {
|
|
113
|
+
submitButton.disabled = true;
|
|
114
|
+
await form.submit();
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Invalid-email policy (ask the owner — default is "annotate only")
|
|
119
|
+
|
|
120
|
+
The SDK never decides what an invalid address means for the visitor. Confirm the owner's
|
|
121
|
+
choice, then pick ONE shape:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// Default — annotate only: verdict meta rides the response; the visitor continues.
|
|
125
|
+
SurfaceForms.init({ ..., emailValidation: true });
|
|
126
|
+
// (declarative surface-next-button markup is enough — no JS gate)
|
|
127
|
+
|
|
128
|
+
// Block — keep the visitor on the step until the address is deliverable.
|
|
129
|
+
continueButton.addEventListener("click", async () => {
|
|
130
|
+
const { isValid, reason, failed } = await form.validateEmail(emailInput.value);
|
|
131
|
+
if (isValid || failed) return form.next(); // fail-open: never lock out on a vendor outage
|
|
132
|
+
errorEl.textContent = `That address looks undeliverable (${reason}).`;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Disqualify — end the form on the "not a fit" ending.
|
|
136
|
+
continueButton.addEventListener("click", async () => {
|
|
137
|
+
const { isValid } = await form.validateEmail(emailInput.value);
|
|
138
|
+
isValid ? form.next() : form.disqualify();
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Block/disqualify pages carry `data-surface-nav="js"`. Both paths share one per-address cache
|
|
143
|
+
with automatic mode, so pairing a gate with `emailValidation: true` bills each address once.
|
|
144
|
+
|
|
107
145
|
## Multi-field component
|
|
108
146
|
|
|
109
147
|
`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):
|
|
@@ -116,14 +154,41 @@ code-driven. Full event list for your own listeners: `viewed`, `started`, `stepC
|
|
|
116
154
|
</div>
|
|
117
155
|
```
|
|
118
156
|
|
|
119
|
-
## Scheduler step
|
|
157
|
+
## Scheduler step
|
|
120
158
|
|
|
121
|
-
|
|
159
|
+
Default: one call. `mountScheduler` renders the record's scheduler question for ANY supported
|
|
160
|
+
provider (Surface's widget default-styled, or the record's third-party embed — Calendly,
|
|
161
|
+
Cal.com, SavvyCal, Chili Piper, HubSpot, RevenueHero, Zoom, Reclaim.ai, Clari), emits the
|
|
162
|
+
lifecycle on `form.on("scheduler")`, and persists the booking into the response by itself.
|
|
163
|
+
Never iframe the hosted booking page, and never bind the question in HTML:
|
|
122
164
|
|
|
123
165
|
```html
|
|
124
166
|
<section data-step-id="step_book" hidden>
|
|
125
167
|
<div id="booking"></div>
|
|
126
168
|
</section>
|
|
169
|
+
<script>
|
|
170
|
+
const teardown = SurfaceFormsSDK.mountScheduler({
|
|
171
|
+
form,
|
|
172
|
+
container: document.querySelector("#booking"),
|
|
173
|
+
onEvent(e) {
|
|
174
|
+
if (e.type === "booking_confirmed") form.submit(); // or next()/goToStep, per your flow
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Note the booking itself needs no wiring — persistence is automatic (`persist: false` opts out);
|
|
181
|
+
`onEvent` here only drives navigation. `styled: false` skips the injected scoped stylesheet
|
|
182
|
+
(Surface provider). Clari is embed-only (no booking signal → drive navigation yourself).
|
|
183
|
+
In preview mode the Surface path books synthetically and nothing is written. The Surface widget
|
|
184
|
+
opens on the current month and, when that month has no slots, jumps to the first bookable date
|
|
185
|
+
the availability summary reports — "No availability this month" after that means the event type
|
|
186
|
+
has none.
|
|
187
|
+
|
|
188
|
+
Custom booking UI (only when asked): mount the unstyled widget directly and write the answer
|
|
189
|
+
yourself —
|
|
190
|
+
|
|
191
|
+
```html
|
|
127
192
|
<script>
|
|
128
193
|
const { createSchedulerClient, createTransport, mountBookingWidget, toSchedulerAnswerState } =
|
|
129
194
|
SurfaceFormsSDK;
|
|
@@ -144,8 +209,7 @@ A `CalendlyScreen` with `calendarType: "Surface"` renders through the SDK's own
|
|
|
144
209
|
The widget emits semantic markup under stable `surface-booking-*` classes (full DOM tree in
|
|
145
210
|
`llms.txt`) and injects no CSS — style from the page. Labels are locale-formatted by default
|
|
146
211
|
with the raw value on each element's `data-value`; pass `formatters: { month?, date?, time? }`
|
|
147
|
-
to override the copy.
|
|
148
|
-
testable offline.
|
|
212
|
+
to override the copy.
|
|
149
213
|
|
|
150
214
|
## React adapter (SPA)
|
|
151
215
|
|
|
@@ -209,7 +273,9 @@ function QualifySteps({ form }: { form: SurfaceForm }) {
|
|
|
209
273
|
}
|
|
210
274
|
```
|
|
211
275
|
|
|
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
|
|
276
|
+
`state` is `useSyncExternalStore`-backed: referentially stable between engine mutations, safe to render from directly. `state.answers` is keyed by stepId then questionId — the example reads the current step (`state.answers[state.currentStepId]`), but an EARLIER step's answer (booking prefill, a summary screen) is read by that step's id: `state.answers["step_role"]?.["q_role"]`. `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.
|
|
277
|
+
|
|
278
|
+
React teardown rule: `destroy()` alone in the effect cleanup is correct — a cleanup cannot await, partial saves are beacons that survive unmount, and the finishing write was already awaited by `submit()` itself before the cleanup can run. Reserve `await form.flush()` for programmatic teardown where the process itself may die with a write still queued (a test harness closing the browser), not for ordinary SPA unmounts.
|
|
213
279
|
|
|
214
280
|
## Tracking setup
|
|
215
281
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|