@qaiddev/quests-embed 1.4.0 → 1.5.3

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
@@ -78,8 +78,8 @@ For complex configurations or inline questionnaires, use a separate JSON config
78
78
  2. A response record is created on your server (`POST` to `endpoint`)
79
79
  3. Questions render one at a time with a progress bar and Back/Next navigation
80
80
  4. Answers autosave as the user moves between steps (`PATCH` per question). Text and currency fields debounce by `saveDebounceMs` (default 500ms)
81
- 5. On the final step, all answers are submitted in a single batch (`POST` to `{endpoint}/{id}/submit`)
82
- 6. A "thank you" screen appears when finished
81
+ 5. On the final step, the answers to the questions still visible are submitted in a single batch (`POST` to `{endpoint}/{id}/submit`). An answer to a question that `visibleIf` has since hidden is left out, and cleared on the server with a `PATCH` of `null`
82
+ 6. A "thank you" screen appears once the server accepts the submit. If the create or the submit fails (a network error or a non-2xx reply), the embed shows "Couldn't send your answers" with a **Try again** button instead; the answers are kept, and Try again re-creates the response first if it has to
83
83
 
84
84
  If `container` is set, the form renders inline inside that element. If not, it opens as a centered modal with a backdrop.
85
85
 
@@ -242,7 +242,7 @@ We offer a Free Plan that hosts both the endpoint and a dashboard for managing y
242
242
 
243
243
  | Option | Type | Default | Description |
244
244
  |--------|------|---------|-------------|
245
- | `autoAdvance` | `boolean` | `false` | Advance automatically on selection for single-choice multiple-choice and range questions |
245
+ | `autoAdvance` | `boolean` | `false` | Advance automatically when an option is picked on a single-choice multiple-choice question. No other type auto-advances (multi-select, text, currency, range and date wait for Next / Enter) |
246
246
  | `saveDebounceMs` | `number` | `500` | Debounce in ms for autosave on text/currency/range |
247
247
  | `autoFocus` | `boolean` | `true` | Auto-focus the input on each step. Set `false` in preview/embedded contexts that shouldn't steal focus |
248
248
 
@@ -253,7 +253,7 @@ For programmatic embedding — e.g. launching a quest from another widget — th
253
253
  | Option | Type | Default | Description |
254
254
  |--------|------|---------|-------------|
255
255
  | `metadata` | `Record<string, unknown>` | — | Extra key/value pairs sent verbatim in the create-response `POST` body. The server decides which keys it persists; unknown keys are ignored. Used, for example, by `@qaiddev/thumbs-embed` to pass `{ feedbackId }` so the QAid backend joins the quest answers to the feedback record. |
256
- | `onComplete` | `(answers) => void` | — | Called once when the quest is completed and submitted (just after the thank-you screen renders), with a copy of the collected answers. Fires in both modal and inline modes. |
256
+ | `onComplete` | `(answers) => void` | — | Called once when the quest is completed and the server accepted the submit (just after the thank-you screen renders), with a copy of the submitted answers — answers to questions hidden by `visibleIf` are left out. Not called while the submit is failing. Fires in both modal and inline modes. |
257
257
  | `onClose` | `() => void` | — | Called once when the embed is torn down — visitor close, host `destroy()`, or teardown after completion. Lets a host that launched the quest drop its reference. |
258
258
 
259
259
  ```typescript
@@ -348,8 +348,52 @@ const quests = new QaidQuests(config: QuestsConfig);
348
348
  | Method | Description |
349
349
  |--------|-------------|
350
350
  | `getAnswers()` | Read-only snapshot of the answers collected so far, keyed by question id |
351
+ | `getCurrentQuestionId()` | Id of the question on screen, or `null` if the embed isn't on one (loading, finished, or unmounted) |
352
+ | `goToStep(questionId)` | Jump to a visible question. Returns `false` if it's unknown or hidden by an unmet `visibleIf`. Latched if the embed is still initializing |
353
+ | `update(questionnaire)` | Swap in a new questionnaire without re-mounting. Returns whether it was applied — see below |
351
354
  | `destroy()` | Remove all DOM elements, event listeners, and injected styles. Safe to call multiple times |
352
355
 
356
+ #### `update(questionnaire)`
357
+
358
+ For hosts that render a questionnaire while it is being authored — a live
359
+ preview in an editor, say. Rebuilding the embed on every edit re-mounts the
360
+ shadow root, repaints the loading state, and re-runs both the theme fetch and
361
+ the create call; because that path awaits the network it always paints a blank
362
+ frame first, which is what makes a live preview flicker as the author types.
363
+
364
+ `update()` re-renders the header and the current step, synchronously. The
365
+ shadow root, the resolved theme, the response id and the answers so far all
366
+ survive, and focus is left wherever the host put it — it does not pull focus
367
+ into the form the way stepping through it does.
368
+
369
+ Answers are kept for questions that still exist and dropped for ones that
370
+ don't, so `getAnswers()` never reports an id the questionnaire has no question
371
+ for. The reader's place is kept the same way: if the question on screen is
372
+ still present and visible, the embed stays on it; otherwise the step index is
373
+ clamped into range.
374
+
375
+ It returns `false`, leaving the embed exactly as it was, when:
376
+
377
+ - the questionnaire has no questions,
378
+ - every question is gated off behind an unmet `visibleIf`, so there would be
379
+ nothing to render,
380
+ - or the reader has already completed the form — resuming a submitted response
381
+ isn't something the embed can decide for you, so rebuild if you want the new
382
+ form there.
383
+
384
+ Called before the embed has finished initializing, the update is latched and
385
+ applied as soon as it is ready (and reported as applied), the same way
386
+ `goToStep` is.
387
+
388
+ ```js
389
+ const quests = new QaidQuests({ endpoint, questionnaire, container: "#preview" });
390
+
391
+ // later, as the author edits — no teardown, no refetch, no flicker
392
+ if (!quests.update(nextQuestionnaire)) {
393
+ // refused: rebuild if you need the new questionnaire on screen
394
+ }
395
+ ```
396
+
353
397
  ### Server Protocol
354
398
 
355
399
  The embed talks to your endpoint in three steps:
@@ -378,8 +422,14 @@ Your endpoint should return `{ "id": "..." }` (string or number). The form is us
378
422
  ```
379
423
 
380
424
  `value` is `string | number | string[] | null` depending on the question type.
381
-
382
- **3. Submit** — `POST {endpoint}/{id}/submit`:
425
+ Just before submitting, the embed sends `"value": null` for every answered
426
+ question that `visibleIf` has since hidden, so a server that stores answers
427
+ from these PATCHes drops them too.
428
+
429
+ **3. Submit** — `POST {endpoint}/{id}/submit`. `answers` holds only the
430
+ questions visible at submit time. Reply with a 2xx; anything else (or a
431
+ network error) shows the visitor a retry screen instead of the thank-you
432
+ screen:
383
433
 
384
434
  ```json
385
435
  {
package/dist/embed.d.ts CHANGED
@@ -1,4 +1,14 @@
1
- import type { Answers, QuestsConfig } from "./types";
1
+ import type { Answers, Questionnaire, QuestsConfig } from "./types";
2
+ /**
3
+ * A random v4 UUID.
4
+ *
5
+ * `crypto.randomUUID` exists only in secure contexts (HTTPS and localhost). On
6
+ * a plain http:// page it is undefined, and calling it threw while mounting, so
7
+ * the quest never appeared. `crypto.getRandomValues` has no such restriction;
8
+ * `Math.random` covers a runtime with no `crypto` at all. The id only groups
9
+ * one browser's responses, so it needs to be unique, not secret.
10
+ */
11
+ export declare function randomId(): string;
2
12
  export declare class QaidQuests {
3
13
  private config;
4
14
  private questionnaire;
@@ -8,10 +18,16 @@ export declare class QaidQuests {
8
18
  private stepIndex;
9
19
  private visibleQuestions;
10
20
  private pendingGoToStep;
21
+ private pendingUpdate;
22
+ private suppressStepFocus;
11
23
  private hasRenderedStep;
12
24
  private answers;
13
25
  private responseId;
14
26
  private visitorId;
27
+ private createPromise;
28
+ private createPending;
29
+ private droppedSaves;
30
+ private submitting;
15
31
  private pendingSaveTimer;
16
32
  private pendingSaveQuestionId;
17
33
  private pendingSaveValue;
@@ -70,6 +86,13 @@ export declare class QaidQuests {
70
86
  private renderHeader;
71
87
  private renderStep;
72
88
  private renderDone;
89
+ /**
90
+ * Shown instead of the thank-you screen when the response could not be
91
+ * created or the submit was refused. The answers stay in memory, so
92
+ * "Try again" re-runs the whole submit (re-creating the response first
93
+ * if needed) without the visitor re-entering anything.
94
+ */
95
+ private renderSubmitError;
73
96
  private advance;
74
97
  private back;
75
98
  private recomputeVisible;
@@ -77,8 +100,11 @@ export declare class QaidQuests {
77
100
  private flushPendingSave;
78
101
  private saveAnswer;
79
102
  private doSave;
103
+ private patchAnswer;
104
+ private startCreate;
80
105
  private createResponse;
81
106
  private submit;
107
+ private trySubmit;
82
108
  private jsonHeaders;
83
109
  private showSaving;
84
110
  private handleKeyDown;
@@ -101,6 +127,33 @@ export declare class QaidQuests {
101
127
  * user's place after a forced re-mount.
102
128
  */
103
129
  getCurrentQuestionId(): string | null;
130
+ /**
131
+ * Swap in a new questionnaire without tearing the embed down.
132
+ *
133
+ * Built for editor previews, where the alternative — `destroy()` plus a
134
+ * fresh construction on every edit — re-mounts the shadow root, repaints
135
+ * the loading state, and re-runs both the theme fetch and the create
136
+ * call. Because that path awaits the network it always paints a blank
137
+ * frame first, which is what makes a live preview strobe while the author
138
+ * types. This re-renders the header and the current step and nothing
139
+ * else, synchronously: the shadow root, the resolved theme, the response
140
+ * id and the answers so far all survive.
141
+ *
142
+ * Answers are kept for questions that still exist and dropped for ones
143
+ * that don't, so `getAnswers()` never reports an id the questionnaire has
144
+ * no question for. The reader's place is kept the same way: if the
145
+ * question on screen is still present and visible, the embed stays on it,
146
+ * otherwise the step index is clamped into range.
147
+ *
148
+ * Returns whether the update was applied. It is refused, leaving the
149
+ * embed exactly as it was, when the questionnaire has no questions, or
150
+ * once the reader has completed the form — resuming a submitted response
151
+ * is not something this can decide on the host's behalf, so a host that
152
+ * wants the new form there should rebuild. Called before the embed has
153
+ * finished initializing, the update is latched and applied as soon as it
154
+ * is ready (and reported as applied), the same way `goToStep` is.
155
+ */
156
+ update(questionnaire: Questionnaire): boolean;
104
157
  /**
105
158
  * Jump to the visible question with the given id. Returns true if
106
159
  * the question is currently visible (and the embed navigated to
package/dist/index.d.ts CHANGED
@@ -32,5 +32,5 @@
32
32
  */
33
33
  import { QaidQuests } from "./embed";
34
34
  export { QaidQuests };
35
- export { getVisibleQuestions, evaluateRule } from "./visibility";
35
+ export { getVisibleQuestions, getVisibleAnswers, evaluateRule, } from "./visibility";
36
36
  export type { QuestsConfig, ResolvedQuestsConfig, Questionnaire, Question, TextQuestion, CurrencyQuestion, RangeQuestion, DateQuestion, MultipleChoiceQuestion, MultipleChoiceOption, Answers, AnswerValue, VisibilityRule, CreateResponsePayload, CreateResponseResult, UpdateAnswerPayload, SubmitPayload, } from "./types";