@picked/client 0.0.71-next.1 → 0.0.71-next.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/dist/.tsbuildinfo +1 -1
- package/dist/{api-CH2FNa6S.cjs → api-CnjBi6JG.cjs} +1 -1
- package/dist/{api-CgqRvuKL.js → api-vBVGLtvU.js} +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
- package/dist/cjs/server/index.js +1 -1
- package/dist/docs/explanation/conditions-and-the-logic-engine.md +94 -0
- package/dist/docs/explanation/feature-flag-allocation.md +176 -0
- package/dist/docs/explanation/how-picked-works.md +102 -0
- package/dist/docs/explanation/how-pickedql-works.md +76 -0
- package/dist/docs/explanation/pages-and-flows.md +64 -0
- package/dist/docs/explanation/the-content-model.md +77 -0
- package/dist/docs/explanation/the-two-apis.md +68 -0
- package/dist/docs/explanation/why-server-side-rendering.md +76 -0
- package/dist/docs/explanation/workspaces-and-versions.md +71 -0
- package/dist/docs/how-to/add-a-step-field-or-action.md +43 -0
- package/dist/docs/{pickedql/examples.md → how-to/analytics/index.md} +19 -6
- package/dist/docs/how-to/archive-or-delete-a-workspace.md +46 -0
- package/dist/docs/how-to/authenticate-with-a-magic-link.md +40 -0
- package/dist/docs/how-to/authenticate-with-a-password.md +51 -0
- package/dist/docs/how-to/authenticate-with-an-api-key.md +51 -0
- package/dist/docs/how-to/authenticate-with-oauth.md +40 -0
- package/dist/docs/{flow-builder/building-flows.md → how-to/build-a-flow.md} +13 -9
- package/dist/docs/how-to/connect-the-mcp-server.md +38 -0
- package/dist/docs/how-to/create-a-project.md +29 -0
- package/dist/docs/how-to/create-an-experience.md +42 -0
- package/dist/docs/how-to/create-and-revoke-an-api-key.md +37 -0
- package/dist/docs/how-to/debug-with-the-chrome-extension.md +57 -0
- package/dist/docs/how-to/define-a-page-layout.md +39 -0
- package/dist/docs/how-to/fill-and-publish-a-page.md +50 -0
- package/dist/docs/{gtm-template.md → how-to/install-the-gtm-template.md} +9 -3
- package/dist/docs/how-to/install-the-sdk.md +68 -0
- package/dist/docs/how-to/install-the-vite-plugin.md +56 -0
- package/dist/docs/how-to/invite-and-remove-members.md +34 -0
- package/dist/docs/how-to/preview-a-workspace.md +28 -0
- package/dist/docs/how-to/publish-a-workspace.md +30 -0
- package/dist/docs/how-to/read-a-feature-flag.md +101 -0
- package/dist/docs/how-to/register-a-component.md +55 -0
- package/dist/docs/how-to/register-an-action-handler.md +67 -0
- package/dist/docs/how-to/render-a-step-form.md +132 -0
- package/dist/docs/how-to/render-an-unpublished-preview.md +41 -0
- package/dist/docs/how-to/reuse-a-section.md +37 -0
- package/dist/docs/how-to/reuse-a-step-field-or-action.md +28 -0
- package/dist/docs/how-to/roll-back-a-published-version.md +26 -0
- package/dist/docs/how-to/run-an-ab-test.md +49 -0
- package/dist/docs/how-to/run-code-on-a-form-submission.md +125 -0
- package/dist/docs/how-to/server-render-a-page.md +81 -0
- package/dist/docs/how-to/set-a-content-security-policy.md +32 -0
- package/dist/docs/how-to/set-up-mfa.md +69 -0
- package/dist/docs/how-to/upload-and-use-an-asset.mdx +73 -0
- package/dist/docs/how-to/validate-a-field.md +52 -0
- package/dist/docs/how-to/write-a-condition.md +45 -0
- package/dist/docs/how-to/write-a-predicate.md +60 -0
- package/dist/docs/index.md +41 -70
- package/dist/docs/reference/dashboard/actions.mdx +53 -0
- package/dist/docs/{api-keys.mdx → reference/dashboard/api-keys.mdx} +7 -15
- package/dist/docs/reference/dashboard/conditions.mdx +42 -0
- package/dist/docs/reference/dashboard/content/asset-library.mdx +63 -0
- package/dist/docs/{content → reference/dashboard/content}/content-entries.mdx +11 -3
- package/dist/docs/{content → reference/dashboard/content}/layouts.mdx +11 -35
- package/dist/docs/reference/dashboard/content/pages.mdx +35 -0
- package/dist/docs/reference/dashboard/content/sections.mdx +28 -0
- package/dist/docs/{experiences.mdx → reference/dashboard/experiences.mdx} +9 -16
- package/dist/docs/{features.mdx → reference/dashboard/features.mdx} +26 -41
- package/dist/docs/{fields.mdx → reference/dashboard/fields.mdx} +26 -20
- package/dist/docs/{flow-builder/overview.mdx → reference/dashboard/flow-builder/canvas.mdx} +8 -13
- package/dist/docs/{flow-builder → reference/dashboard/flow-builder}/condition-builder.md +5 -13
- package/dist/docs/{flow-builder → reference/dashboard/flow-builder}/keyboard-shortcuts.md +7 -1
- package/dist/docs/{flow-builder → reference/dashboard/flow-builder}/nodes.md +10 -3
- package/dist/docs/reference/dashboard/projects.mdx +44 -0
- package/dist/docs/reference/dashboard/reports.mdx +43 -0
- package/dist/docs/reference/dashboard/steps.mdx +37 -0
- package/dist/docs/{team-management.md → reference/dashboard/team-management.md} +5 -22
- package/dist/docs/{workspace-versioning.mdx → reference/dashboard/workspace-versioning.mdx} +6 -10
- package/dist/docs/reference/dashboard/workspaces.mdx +76 -0
- package/dist/docs/{delivery-api → reference/delivery-api}/experience-content.md +11 -4
- package/dist/docs/reference/delivery-api/index.md +45 -0
- package/dist/docs/{delivery-api → reference/delivery-api}/preview-caching-crawling.md +12 -5
- package/dist/docs/{delivery-api → reference/delivery-api}/project-config.md +11 -4
- package/dist/docs/reference/glossary.md +173 -0
- package/dist/docs/reference/management-api/authentication.md +74 -0
- package/dist/docs/{api → reference/management-api}/authorization-and-scopes.md +10 -3
- package/dist/docs/{api → reference/management-api}/error-handling.md +9 -3
- package/dist/docs/{api → reference/management-api}/pagination.md +6 -0
- package/dist/docs/{pickedql → reference/pickedql}/data-sources.md +16 -9
- package/dist/docs/{pickedql → reference/pickedql}/dimensions.md +7 -0
- package/dist/docs/{pickedql → reference/pickedql}/filtering.md +7 -0
- package/dist/docs/reference/pickedql/index.md +18 -0
- package/dist/docs/{pickedql → reference/pickedql}/schema.md +13 -6
- package/dist/docs/{pickedql → reference/pickedql}/virtual-functions.md +7 -0
- package/dist/docs/reference/sdk/actions.md +30 -0
- package/dist/docs/{sdk → reference/sdk}/conditions.md +8 -1
- package/dist/docs/reference/sdk/content-security-policy.md +21 -0
- package/dist/docs/reference/sdk/experiences.md +51 -0
- package/dist/docs/{sdk → reference/sdk}/features.md +30 -117
- package/dist/docs/{sdk → reference/sdk}/fields.md +10 -88
- package/dist/docs/reference/sdk/index.md +68 -0
- package/dist/docs/{sdk → reference/sdk}/plugins/create-experience.md +10 -22
- package/dist/docs/reference/sdk/plugins/dynamic-experiences.md +26 -0
- package/dist/docs/reference/sdk/plugins/index.md +29 -0
- package/dist/docs/{sdk → reference/sdk}/plugins/window-object.md +6 -0
- package/dist/docs/reference/sdk/predicates.md +43 -0
- package/dist/docs/reference/sdk/ssr/index.md +33 -0
- package/dist/docs/{sdk → reference/sdk}/ssr/rendering-and-hydration.md +4 -63
- package/dist/docs/{sdk → reference/sdk}/ssr/serialize.md +6 -33
- package/dist/docs/{sdk → reference/sdk}/ssr/slots-and-placements.md +7 -1
- package/dist/docs/reference/sdk/steps.md +87 -0
- package/dist/docs/reference/tools/chrome-extension.md +16 -0
- package/dist/docs/{image-optimisation-service.md → reference/tools/image-optimisation-service.md} +8 -0
- package/dist/docs/{mcp.md → reference/tools/mcp.md} +5 -45
- package/dist/docs/{vite-plugin.md → reference/tools/vite-plugin.md} +8 -38
- package/dist/docs/{api/index.md → tutorials/first-api-call.md} +27 -15
- package/dist/docs/tutorials/first-experience-in-the-dashboard.mdx +143 -0
- package/dist/docs/tutorials/first-experience.mdx +255 -0
- package/dist/docs/tutorials/first-server-rendered-page.mdx +186 -0
- package/dist/esm/index.js +468 -426
- package/dist/esm/plugins/dynamic-experiences/index.js +2 -2
- package/dist/esm/server/index.js +1 -1
- package/dist/{plugin-DCW-tnjO.cjs → plugin-CSmD1gPl.cjs} +1 -1
- package/dist/{plugin-D2UqFikQ.js → plugin-GOTtoDlC.js} +1 -1
- package/dist/types/entities/feature/feature.d.ts +25 -2
- package/dist/types/entities/feature/feature.d.ts.map +1 -1
- package/dist/types/picked.d.ts +2 -1
- package/dist/types/picked.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/docs/actions.mdx +0 -98
- package/dist/docs/api/authentication.md +0 -403
- package/dist/docs/chrome-extension.md +0 -68
- package/dist/docs/conditions.mdx +0 -72
- package/dist/docs/content/asset-library.mdx +0 -124
- package/dist/docs/content/pages.mdx +0 -82
- package/dist/docs/content/sections.mdx +0 -44
- package/dist/docs/delivery-api/index.md +0 -53
- package/dist/docs/guides/build-a-personalized-page.mdx +0 -161
- package/dist/docs/pickedql/index.md +0 -54
- package/dist/docs/projects.mdx +0 -44
- package/dist/docs/reports.mdx +0 -20
- package/dist/docs/sdk/actions.md +0 -82
- package/dist/docs/sdk/content-security-policy.md +0 -33
- package/dist/docs/sdk/experiences.md +0 -64
- package/dist/docs/sdk/feature-allocation.md +0 -161
- package/dist/docs/sdk/index.md +0 -98
- package/dist/docs/sdk/plugins/dynamic-experiences.md +0 -84
- package/dist/docs/sdk/plugins/index.md +0 -9
- package/dist/docs/sdk/predicates.md +0 -128
- package/dist/docs/sdk/ssr/index.md +0 -79
- package/dist/docs/sdk/steps.md +0 -479
- package/dist/docs/steps.mdx +0 -53
- package/dist/docs/workspaces.mdx +0 -133
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
layout: docs
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Predicates
|
|
6
|
-
|
|
7
|
-
A predicate holds reusable condition logic that reads the current state of your
|
|
8
|
-
application. Each predicate is a pure function. It takes a state snapshot, and that
|
|
9
|
-
snapshot holds the responses, the fields, the browser context, and optional
|
|
10
|
-
arguments.
|
|
11
|
-
|
|
12
|
-
## Define a predicate
|
|
13
|
-
|
|
14
|
-
Define your predicates when you create the Picked client. Pass a `predicates` object.
|
|
15
|
-
Each predicate needs a unique identifier and a function that takes a state snapshot:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { createClient } from '@picked/client'
|
|
19
|
-
|
|
20
|
-
const client = createClient({
|
|
21
|
-
projectId: 'your-project-id',
|
|
22
|
-
predicates: {
|
|
23
|
-
'is-example-page': ({ location }) => {
|
|
24
|
-
return location?.pathname === '/example'
|
|
25
|
-
},
|
|
26
|
-
'get-account-tier': async () => {
|
|
27
|
-
const data = await fetch('/api/get-account-tier').then((r) => r.json())
|
|
28
|
-
return data.result // Returns "trial".
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
})
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## How predicates work
|
|
35
|
-
|
|
36
|
-
After you register a predicate and a condition uses it, the SDK follows this flow:
|
|
37
|
-
|
|
38
|
-
1. **Registration**: you register each predicate on the Picked client at startup.
|
|
39
|
-
2. **State snapshot**: the SDK gives the predicate a frozen state snapshot. The
|
|
40
|
-
snapshot holds the current responses, the fields, and the browser context.
|
|
41
|
-
3. **Condition evaluation**: an experience condition invokes the predicate with a
|
|
42
|
-
fresh snapshot on each call.
|
|
43
|
-
4. **Return types**: a predicate can return a boolean, a string, an array of
|
|
44
|
-
strings, or `undefined`. It can also return a promise that resolves to one of
|
|
45
|
-
those.
|
|
46
|
-
|
|
47
|
-
## Use a predicate in a condition
|
|
48
|
-
|
|
49
|
-
Most predicates are used inside condition logic, through the `invoke` operator:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
const myExperience = createExperience({
|
|
53
|
-
// ... other config
|
|
54
|
-
condition: {
|
|
55
|
-
and: [
|
|
56
|
-
{
|
|
57
|
-
'==': [
|
|
58
|
-
{
|
|
59
|
-
invoke: {
|
|
60
|
-
var: 'predicates.is-example-page', // Points at a predicate that calls your endpoint.
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
true, // Check that the return value is true.
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
'==': [
|
|
68
|
-
{
|
|
69
|
-
invoke: {
|
|
70
|
-
var: 'predicates.account-tier',
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
'/trial', // The condition is true when the string matches.
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
})
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
:::tip
|
|
82
|
-
You can also use a predicate in `validation` logic.
|
|
83
|
-
:::
|
|
84
|
-
|
|
85
|
-
## Use the state snapshot
|
|
86
|
-
|
|
87
|
-
Each predicate takes a `StateSnapshot` object with these properties:
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
interface StateSnapshot {
|
|
91
|
-
client: Picked // The Picked client.
|
|
92
|
-
responses?: Array<{
|
|
93
|
-
// Every response from the user.
|
|
94
|
-
fieldId: string
|
|
95
|
-
value: FieldValue | undefined
|
|
96
|
-
}>
|
|
97
|
-
args?: unknown[]
|
|
98
|
-
fields?: Field[] // Every registered field.
|
|
99
|
-
location?: Location // The browser location: URL, pathname, search, and more.
|
|
100
|
-
document?: Document // The browser document object.
|
|
101
|
-
sessionStorage?: Storage // The browser sessionStorage API.
|
|
102
|
-
localStorage?: Storage // The browser localStorage API.
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
:::warning
|
|
107
|
-
In server-side rendering, and in any environment that is not a browser, `location`,
|
|
108
|
-
`document`, `sessionStorage`, and `localStorage` are `undefined`.
|
|
109
|
-
:::
|
|
110
|
-
|
|
111
|
-
## Best practices
|
|
112
|
-
|
|
113
|
-
- **Keep each predicate pure**: a predicate must be deterministic and free of side
|
|
114
|
-
effects. The same snapshot must always give the same result.
|
|
115
|
-
- **Handle missing data**: check that a response or a field is there before you read
|
|
116
|
-
its value. Return `undefined` when the data is not available. Do not throw.
|
|
117
|
-
- **Do not do heavy work**: the SDK can invoke a predicate many times. Keep the logic
|
|
118
|
-
small and fast.
|
|
119
|
-
- **Use an async predicate only when you must**: the SDK supports one, but it can
|
|
120
|
-
make the experience slow to appear. Use synchronous logic where you can.
|
|
121
|
-
- **Return one type**: do not return a boolean from a predicate one time and a string
|
|
122
|
-
the next.
|
|
123
|
-
- **Use a meaningful name**: a predicate ID must say what the predicate evaluates,
|
|
124
|
-
such as `is-premium-user` or `has-items-in-cart`.
|
|
125
|
-
- **Keep the result of expensive work**: when a predicate must do expensive work, put
|
|
126
|
-
the result in `localStorage` or `sessionStorage` inside the predicate.
|
|
127
|
-
- **Do not change the snapshot**: the state snapshot is frozen. A change to it throws
|
|
128
|
-
an error in strict mode.
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
layout: docs
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Server-Side Rendering
|
|
6
|
-
|
|
7
|
-
Render a Picked Page on your server so that search engines can read it. Then hydrate
|
|
8
|
-
the page on the client. This page shows the whole shape. The pages after it cover
|
|
9
|
-
each part.
|
|
10
|
-
|
|
11
|
-
## Why server-side rendering
|
|
12
|
-
|
|
13
|
-
The search rank of a Page matters, so the HTML that your server returns must hold the
|
|
14
|
-
content of that Page. A crawler and a social preview both read that HTML directly.
|
|
15
|
-
Neither waits for the JavaScript in the browser. A Page that the server renders
|
|
16
|
-
therefore has its content in the first response, and the client takes over after
|
|
17
|
-
that.
|
|
18
|
-
|
|
19
|
-
## The flow
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
projectConfig (Delivery API)
|
|
23
|
-
-> serialize(client, params) on the server -> PickedSnapshot
|
|
24
|
-
-> embed snapshot in HTML
|
|
25
|
-
-> client seeds from snapshot -> renders the same markup -> hydrates
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
1. Fetch your published `projectConfig` from the [Delivery API](/delivery-api).
|
|
29
|
-
2. Call `serialize()` on the server. It finds which experiences match the URL, reads
|
|
30
|
-
their content, and returns a `PickedSnapshot` as plain JSON. See
|
|
31
|
-
[Server Snapshot](/sdk/ssr/serialize).
|
|
32
|
-
3. Embed the snapshot in your HTML payload.
|
|
33
|
-
4. On the client, seed a Picked client from the snapshot. The first paint then
|
|
34
|
-
matches the markup from the server. Hydrate after that. See
|
|
35
|
-
[Rendering and Hydration](/sdk/ssr/rendering-and-hydration).
|
|
36
|
-
|
|
37
|
-
The snapshot carries the exact options that the server chose with, and the condition
|
|
38
|
-
results that it kept. The first render on the client therefore reuses the decisions
|
|
39
|
-
of the server. It does not evaluate them again.
|
|
40
|
-
|
|
41
|
-
## React and Svelte side by side
|
|
42
|
-
|
|
43
|
-
:::code-group
|
|
44
|
-
|
|
45
|
-
```tsx [React]
|
|
46
|
-
import { PickedProvider, ExperienceSlot } from '@picked/react'
|
|
47
|
-
|
|
48
|
-
<PickedProvider options={options} snapshot={snapshot}>
|
|
49
|
-
<ExperienceSlot name="page" />
|
|
50
|
-
</PickedProvider>
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
```svelte [Svelte]
|
|
54
|
-
<script lang="ts">
|
|
55
|
-
import { ExperienceSlot, setPickedContext } from '@picked/svelte'
|
|
56
|
-
|
|
57
|
-
setPickedContext({ options, snapshot })
|
|
58
|
-
</script>
|
|
59
|
-
|
|
60
|
-
<ExperienceSlot name="page" />
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
:::
|
|
64
|
-
|
|
65
|
-
## The server entry point
|
|
66
|
-
|
|
67
|
-
The server helpers live in a dedicated entry point, so a browser bundle never holds
|
|
68
|
-
them:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { serialize, registerProjectConfig } from '@picked/client/server'
|
|
72
|
-
import type { PickedSnapshot } from '@picked/client/server'
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Go to [Server Snapshot](/sdk/ssr/serialize) for the `serialize()` reference. Go to
|
|
76
|
-
[Rendering and Hydration](/sdk/ssr/rendering-and-hydration) to connect React and
|
|
77
|
-
Svelte. Go to [Slots and Placements](/sdk/ssr/slots-and-placements) for how a slot
|
|
78
|
-
chooses an experience. If the SDK is new to you, read
|
|
79
|
-
[Installation](/sdk) first.
|
package/dist/docs/sdk/steps.md
DELETED
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
layout: docs
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Steps
|
|
6
|
-
|
|
7
|
-
Render your steps with `<StepRouter>` and `<Step>` from `@picked/react` or
|
|
8
|
-
`@picked/svelte`. Reach the active step through `useStep()` or `getStep()`.
|
|
9
|
-
|
|
10
|
-
## How steps work
|
|
11
|
-
|
|
12
|
-
When an experience starts, its steps form a directed graph of nodes. The SDK walks
|
|
13
|
-
this graph to find which step to show:
|
|
14
|
-
|
|
15
|
-
1. **Initialization**: when the experience opens, the SDK starts at the origin node.
|
|
16
|
-
It walks the graph to find the first step.
|
|
17
|
-
2. **Step entry**: the SDK sets the active step. It sends a `step_enter` event on the
|
|
18
|
-
event bus.
|
|
19
|
-
3. **Field evaluation**: the SDK evaluates the fields of the step. It shows or hides
|
|
20
|
-
a field with a condition, and the current state decides which.
|
|
21
|
-
4. **User input**: the user fills in the fields of the step. Validation runs in the
|
|
22
|
-
browser on blur and on submit.
|
|
23
|
-
5. **Step submission**: when the user submits, a `step_submit` event fires. The SDK
|
|
24
|
-
commits the field responses to the client state.
|
|
25
|
-
6. **Navigation**: the SDK follows the edge from the current step node to the next
|
|
26
|
-
one. It walks through each condition and action on the way.
|
|
27
|
-
7. **Completion**: when no step remains, the experience flow is complete.
|
|
28
|
-
|
|
29
|
-
## Step routing
|
|
30
|
-
|
|
31
|
-
`StepRouter` watches the current step state of the experience. It activates the
|
|
32
|
-
`Step` whose `id` matches. Each `Step` registers itself on mount and unregisters on
|
|
33
|
-
unmount, so a tab or a drawer that renders lazily works with no extra code.
|
|
34
|
-
|
|
35
|
-
:::code-group
|
|
36
|
-
|
|
37
|
-
```svelte [Svelte]
|
|
38
|
-
<script lang="ts">
|
|
39
|
-
import { StepRouter, Step } from '@picked/svelte/router'
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
<StepRouter>
|
|
43
|
-
<Step id="welcome">
|
|
44
|
-
<WelcomeScreen />
|
|
45
|
-
</Step>
|
|
46
|
-
|
|
47
|
-
<Step id="contact">
|
|
48
|
-
<ContactForm />
|
|
49
|
-
</Step>
|
|
50
|
-
|
|
51
|
-
<Step id="confirmation">
|
|
52
|
-
<ConfirmationScreen />
|
|
53
|
-
</Step>
|
|
54
|
-
</StepRouter>
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
```tsx [React]
|
|
58
|
-
import { StepRouter, Step } from '@picked/react/router'
|
|
59
|
-
|
|
60
|
-
function MyExperience() {
|
|
61
|
-
return (
|
|
62
|
-
<StepRouter>
|
|
63
|
-
<Step id="welcome">
|
|
64
|
-
<WelcomeScreen />
|
|
65
|
-
</Step>
|
|
66
|
-
|
|
67
|
-
<Step id="contact">
|
|
68
|
-
<ContactForm />
|
|
69
|
-
</Step>
|
|
70
|
-
|
|
71
|
-
<Step id="confirmation">
|
|
72
|
-
<ConfirmationScreen />
|
|
73
|
-
</Step>
|
|
74
|
-
</StepRouter>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
:::
|
|
80
|
-
|
|
81
|
-
:::note
|
|
82
|
-
A router must hold every Step, even when there is only one Step.
|
|
83
|
-
:::
|
|
84
|
-
|
|
85
|
-
### Lazy-loaded steps
|
|
86
|
-
|
|
87
|
-
For a large experience, load a step lazily with a dynamic import:
|
|
88
|
-
|
|
89
|
-
:::code-group
|
|
90
|
-
|
|
91
|
-
```svelte [Svelte]
|
|
92
|
-
<StepRouter>
|
|
93
|
-
<Step id="welcome" component={import('./WelcomeScreen.svelte')} />
|
|
94
|
-
</StepRouter>
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
```tsx [React]
|
|
98
|
-
import { lazy } from 'react'
|
|
99
|
-
|
|
100
|
-
const WelcomeScreen = lazy(() => import('./WelcomeScreen'))
|
|
101
|
-
|
|
102
|
-
<StepRouter>
|
|
103
|
-
<Step id="welcome">
|
|
104
|
-
<WelcomeScreen />
|
|
105
|
-
</Step>
|
|
106
|
-
</StepRouter>
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
:::
|
|
110
|
-
|
|
111
|
-
The `component` prop takes a `Promise<Component>`. The SDK does not load the content
|
|
112
|
-
of the step until that step becomes active.
|
|
113
|
-
|
|
114
|
-
## Reach the step context
|
|
115
|
-
|
|
116
|
-
Inside a `Step` component, or inside any component that a `Step` renders, call
|
|
117
|
-
`getStep()` or `useStep()` to reach the reactive context of the step:
|
|
118
|
-
|
|
119
|
-
:::code-group
|
|
120
|
-
|
|
121
|
-
```svelte [Svelte]
|
|
122
|
-
<script lang="ts">
|
|
123
|
-
import { getStep } from '@picked/svelte/hooks'
|
|
124
|
-
|
|
125
|
-
const { step, fields } = getStep()
|
|
126
|
-
</script>
|
|
127
|
-
|
|
128
|
-
<h1>{$step.label}</h1>
|
|
129
|
-
|
|
130
|
-
{#each $fields as field}
|
|
131
|
-
<label>{field.label}</label>
|
|
132
|
-
<input
|
|
133
|
-
name={field.id}
|
|
134
|
-
value={field.value ?? ''}
|
|
135
|
-
onchange={field.onChange}
|
|
136
|
-
onblur={field.onBlur}
|
|
137
|
-
/>
|
|
138
|
-
{#if field.error && field.touched}
|
|
139
|
-
<p class="error">{field.error.userMessage}</p>
|
|
140
|
-
{/if}
|
|
141
|
-
{/each}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
```tsx [React]
|
|
145
|
-
import { useStep } from '@picked/react/hooks'
|
|
146
|
-
import { useUnit } from 'effector-react'
|
|
147
|
-
|
|
148
|
-
function ContactForm() {
|
|
149
|
-
const { step, fields } = useStep()
|
|
150
|
-
const currentStep = useUnit(step)
|
|
151
|
-
const currentFields = useUnit(fields)
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<>
|
|
155
|
-
<h1>{currentStep.label}</h1>
|
|
156
|
-
|
|
157
|
-
{currentFields.map((field) => (
|
|
158
|
-
<div key={field.id}>
|
|
159
|
-
<label>{field.label}</label>
|
|
160
|
-
<input
|
|
161
|
-
name={field.id}
|
|
162
|
-
value={field.value ?? ''}
|
|
163
|
-
onChange={field.onChange}
|
|
164
|
-
onBlur={field.onBlur}
|
|
165
|
-
/>
|
|
166
|
-
{field.error && field.touched && <p className="error">{field.error.userMessage}</p>}
|
|
167
|
-
</div>
|
|
168
|
-
))}
|
|
169
|
-
</>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
:::
|
|
175
|
-
|
|
176
|
-
`getStep()` in Svelte, and `useStep()` in React, return a `StepContext` with these
|
|
177
|
-
properties:
|
|
178
|
-
|
|
179
|
-
| Property | Type | Description |
|
|
180
|
-
| ---------- | --------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
181
|
-
| `step` | `Store<ExperienceStep>` | A reactive store that holds the `id` and the `label` of the step |
|
|
182
|
-
| `fields` | `Store<FormField[]>` | A reactive store of the fields, with their current values, errors, and handlers |
|
|
183
|
-
| `isActive` | `Store<boolean>` (Svelte) / `boolean` (React) | Whether the router holds this step as the active one |
|
|
184
|
-
| `form` | Object | The handlers for a form submission. See [Forms](#forms). |
|
|
185
|
-
|
|
186
|
-
## Forms
|
|
187
|
-
|
|
188
|
-
When a step has fields, `getStep()` and `useStep()` give you a `form` object. Use it
|
|
189
|
-
for the submission, the validation, and the move to the next step.
|
|
190
|
-
|
|
191
|
-
Both take an optional object of callbacks, as in `getStep({ onSuccess })` or
|
|
192
|
-
`useStep({ onSuccess })`. This is the recommended way to act on a submission. The
|
|
193
|
-
slots are `onBeforeSubmit`, `onSuccess`, and `onError`. This page covers each one
|
|
194
|
-
below.
|
|
195
|
-
|
|
196
|
-
### Handle a submission
|
|
197
|
-
|
|
198
|
-
Bind `form.props` to the `onsubmit` handler of your `<form>` element:
|
|
199
|
-
|
|
200
|
-
:::code-group
|
|
201
|
-
|
|
202
|
-
```svelte [Svelte]
|
|
203
|
-
<script lang="ts">
|
|
204
|
-
import { getStep } from '@picked/svelte/hooks'
|
|
205
|
-
|
|
206
|
-
const { fields, form } = getStep()
|
|
207
|
-
const isSubmitting = form.submitting
|
|
208
|
-
</script>
|
|
209
|
-
|
|
210
|
-
<form {...form.props}>
|
|
211
|
-
{#each $fields as field}
|
|
212
|
-
<input
|
|
213
|
-
name={field.id}
|
|
214
|
-
value={field.value ?? ''}
|
|
215
|
-
onchange={field.onChange}
|
|
216
|
-
onblur={field.onBlur}
|
|
217
|
-
/>
|
|
218
|
-
{#if field.error && field.touched}
|
|
219
|
-
<p class="error">{field.error.userMessage}</p>
|
|
220
|
-
{/if}
|
|
221
|
-
{/each}
|
|
222
|
-
|
|
223
|
-
<button type="submit" disabled={$isSubmitting}>
|
|
224
|
-
{$isSubmitting ? 'Submitting…' : 'Continue'}
|
|
225
|
-
</button>
|
|
226
|
-
</form>
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
```tsx [React]
|
|
230
|
-
import { useStep } from '@picked/react/hooks'
|
|
231
|
-
import { useUnit } from 'effector-react'
|
|
232
|
-
|
|
233
|
-
function StepForm() {
|
|
234
|
-
const { fields, form } = useStep()
|
|
235
|
-
const currentFields = useUnit(fields)
|
|
236
|
-
const isSubmitting = useUnit(form.submitting)
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<form {...form.props}>
|
|
240
|
-
{currentFields.map((field) => (
|
|
241
|
-
<div key={field.id}>
|
|
242
|
-
<input
|
|
243
|
-
name={field.id}
|
|
244
|
-
value={field.value ?? ''}
|
|
245
|
-
onChange={field.onChange}
|
|
246
|
-
onBlur={field.onBlur}
|
|
247
|
-
/>
|
|
248
|
-
{field.error && field.touched && <p className="error">{field.error.userMessage}</p>}
|
|
249
|
-
</div>
|
|
250
|
-
))}
|
|
251
|
-
|
|
252
|
-
<button type="submit" disabled={isSubmitting}>
|
|
253
|
-
{isSubmitting ? 'Submitting…' : 'Continue'}
|
|
254
|
-
</button>
|
|
255
|
-
</form>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
:::
|
|
261
|
-
|
|
262
|
-
When you submit the form, the SDK does this:
|
|
263
|
-
|
|
264
|
-
1. It collects the field values from the HTML form.
|
|
265
|
-
2. It runs each `onBeforeSubmit` callback, one after another. A throw here aborts the
|
|
266
|
-
submit and commits nothing.
|
|
267
|
-
3. It checks each required field and each validation rule.
|
|
268
|
-
4. It commits the responses to the experience state.
|
|
269
|
-
5. It fires `form_submit_success` or `form_submit_error` on the event bus.
|
|
270
|
-
6. It runs your `onSuccess` or `onError` callback.
|
|
271
|
-
7. It moves to the next step. A call to `stopNavigation` stops this. A callback that
|
|
272
|
-
failed in step 6 does not stop it.
|
|
273
|
-
|
|
274
|
-
### Transform data before a submission
|
|
275
|
-
|
|
276
|
-
Use `onBeforeSubmit` to intercept and transform the field data before the SDK stores it. The callbacks run one after another. Each one takes the output of the one before
|
|
277
|
-
it:
|
|
278
|
-
|
|
279
|
-
:::code-group
|
|
280
|
-
|
|
281
|
-
```svelte [Svelte]
|
|
282
|
-
<script lang="ts">
|
|
283
|
-
import { getStep } from '@picked/svelte/hooks'
|
|
284
|
-
|
|
285
|
-
const { form } = getStep({
|
|
286
|
-
onBeforeSubmit: async (data) => {
|
|
287
|
-
// Normalize the email address to lowercase first.
|
|
288
|
-
return {
|
|
289
|
-
...data,
|
|
290
|
-
'email-address': String(data['email-address']).toLowerCase(),
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
})
|
|
294
|
-
</script>
|
|
295
|
-
|
|
296
|
-
<form {...form.props}>{/* ... */}</form>
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
```tsx [React]
|
|
300
|
-
import { useStep } from '@picked/react/hooks'
|
|
301
|
-
|
|
302
|
-
function ContactStep() {
|
|
303
|
-
const { form } = useStep({
|
|
304
|
-
onBeforeSubmit: async (data) => {
|
|
305
|
-
// Normalize the email address to lowercase first.
|
|
306
|
-
return {
|
|
307
|
-
...data,
|
|
308
|
-
'email-address': String(data['email-address']).toLowerCase(),
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
return <form {...form.props}>{/* ... */}</form>
|
|
314
|
-
}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
:::
|
|
318
|
-
|
|
319
|
-
:::warning
|
|
320
|
-
When an `onBeforeSubmit` callback throws, the SDK aborts the submit before it commits
|
|
321
|
-
anything, and the step does not advance. This is deliberate. A payload that is only
|
|
322
|
-
part transformed would store data that looks correct but is wrong. Use
|
|
323
|
-
`onBeforeSubmit` for a transform and for a validation check. Put a side effect, such as an
|
|
324
|
-
analytics call or a backend call, in `onSuccess`.
|
|
325
|
-
:::
|
|
326
|
-
|
|
327
|
-
### Success and error callbacks
|
|
328
|
-
|
|
329
|
-
Pass `onSuccess` and `onError` to act on the outcome of a submission:
|
|
330
|
-
|
|
331
|
-
:::code-group
|
|
332
|
-
|
|
333
|
-
```svelte [Svelte]
|
|
334
|
-
<script lang="ts">
|
|
335
|
-
import { getStep } from '@picked/svelte/hooks'
|
|
336
|
-
|
|
337
|
-
const { form } = getStep({
|
|
338
|
-
onSuccess: ({ data }) => {
|
|
339
|
-
analytics.track('lead_captured', data)
|
|
340
|
-
},
|
|
341
|
-
onError: (errors) => {
|
|
342
|
-
console.error('Validation errors:', errors)
|
|
343
|
-
},
|
|
344
|
-
})
|
|
345
|
-
</script>
|
|
346
|
-
|
|
347
|
-
<form {...form.props}>{/* ... */}</form>
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
```tsx [React]
|
|
351
|
-
import { useStep } from '@picked/react/hooks'
|
|
352
|
-
|
|
353
|
-
function ContactStep() {
|
|
354
|
-
const { form } = useStep({
|
|
355
|
-
onSuccess: ({ data }) => {
|
|
356
|
-
analytics.track('lead_captured', data)
|
|
357
|
-
},
|
|
358
|
-
onError: (errors) => {
|
|
359
|
-
console.error('Validation errors:', errors)
|
|
360
|
-
},
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
return <form {...form.props}>{/* ... */}</form>
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
:::
|
|
368
|
-
|
|
369
|
-
A callback belongs to the component that passes it. The SDK releases the callback
|
|
370
|
-
when that component unmounts, so you write no teardown code. A new inline function on
|
|
371
|
-
a re-render replaces the earlier one and does not add a second one. One submit
|
|
372
|
-
therefore runs each callback exactly one time.
|
|
373
|
-
|
|
374
|
-
An `onSuccess` or `onError` callback that throws or rejects cannot stop the flow. The
|
|
375
|
-
step still advances, and the SDK reports the failure on the client event bus as
|
|
376
|
-
`form_submit_callback_error`, so the failure stays observable.
|
|
377
|
-
|
|
378
|
-
#### Register a callback in code
|
|
379
|
-
|
|
380
|
-
`form.onSuccess`, `form.onError`, and `form.onBeforeSubmit` are an escape hatch, for
|
|
381
|
-
a callback that you must attach only when a condition holds. They return nothing. The
|
|
382
|
-
SDK releases them for you, as above.
|
|
383
|
-
|
|
384
|
-
```tsx [React]
|
|
385
|
-
function ContactStep({ trackConversions }: { trackConversions: boolean }) {
|
|
386
|
-
const { form } = useStep()
|
|
387
|
-
|
|
388
|
-
if (trackConversions) {
|
|
389
|
-
form.onSuccess(({ data }) => analytics.track('lead_captured', data))
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return <form {...form.props}>{/* ... */}</form>
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
:::warning
|
|
397
|
-
Register from the body of the component, and not from an event handler. A
|
|
398
|
-
registration inside a click handler is added each time the handler runs, and the SDK
|
|
399
|
-
releases it only when the component unmounts. The registrations therefore accumulate.
|
|
400
|
-
Use the options object above where you can.
|
|
401
|
-
:::
|
|
402
|
-
|
|
403
|
-
### Stop the move to the next step
|
|
404
|
-
|
|
405
|
-
By default the experience advances to the next step after a successful submission.
|
|
406
|
-
Call `stopNavigation()` inside `onSuccess` to stop this. Do it when you must choose
|
|
407
|
-
the next step yourself:
|
|
408
|
-
|
|
409
|
-
:::code-group
|
|
410
|
-
|
|
411
|
-
```svelte [Svelte]
|
|
412
|
-
<script lang="ts">
|
|
413
|
-
import { getStep } from '@picked/svelte/hooks'
|
|
414
|
-
import { getExperience } from '@picked/svelte/hooks'
|
|
415
|
-
|
|
416
|
-
const experience = getExperience()
|
|
417
|
-
|
|
418
|
-
const { form } = getStep({
|
|
419
|
-
onSuccess: ({ data, stopNavigation }) => {
|
|
420
|
-
stopNavigation()
|
|
421
|
-
|
|
422
|
-
// Choose the next step from the data that the user sent.
|
|
423
|
-
if (data['plan-type'] === 'enterprise') {
|
|
424
|
-
experience.goToStepById('enterprise-contact')
|
|
425
|
-
} else {
|
|
426
|
-
experience.goToNextStep()
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
})
|
|
430
|
-
</script>
|
|
431
|
-
|
|
432
|
-
<form {...form.props}>{/* ... */}</form>
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
```tsx [React]
|
|
436
|
-
import { useStep } from '@picked/react/hooks'
|
|
437
|
-
import { useExperience } from '@picked/react/hooks'
|
|
438
|
-
|
|
439
|
-
function PlanStep() {
|
|
440
|
-
const experience = useExperience()
|
|
441
|
-
|
|
442
|
-
const { form } = useStep({
|
|
443
|
-
onSuccess: ({ data, stopNavigation }) => {
|
|
444
|
-
stopNavigation()
|
|
445
|
-
|
|
446
|
-
// Choose the next step from the data that the user sent.
|
|
447
|
-
if (data['plan-type'] === 'enterprise') {
|
|
448
|
-
experience.goToStepById('enterprise-contact')
|
|
449
|
-
} else {
|
|
450
|
-
experience.goToNextStep()
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
return <form {...form.props}>{/* ... */}</form>
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
:::
|
|
460
|
-
|
|
461
|
-
Move by step `id` with `goToStepById('<step-id>')`, or by the `key` of the step with
|
|
462
|
-
`goToStepByKey('<step-key>')`. Both jump directly to the step node that matches. When
|
|
463
|
-
no step matches, the call does nothing.
|
|
464
|
-
|
|
465
|
-
## Best practices
|
|
466
|
-
|
|
467
|
-
- **Give each step a meaningful label**: analytics events and the dashboard both show
|
|
468
|
-
the `label`. Use a name that says what the step does, such as `"Contact Details"`
|
|
469
|
-
and not `"Step 2"`.
|
|
470
|
-
- **Keep each step small**: each step must collect one small set of related
|
|
471
|
-
information. More steps with fewer fields each reduce the number of users who leave.
|
|
472
|
-
- **Use a condition to take a branch**: do not make a second copy of a step. Write the
|
|
473
|
-
branch with `nextConditional()`, so that you can reuse a shared step.
|
|
474
|
-
- **Use `stopNavigation` only when you must**: an automatic move after a submit is
|
|
475
|
-
what a user expects. Stop it only when you must choose the next step yourself.
|
|
476
|
-
- **Check a field on blur, not on change**: the SDK does this for you. A field shows
|
|
477
|
-
an error after the user leaves it, and not while the user types.
|
|
478
|
-
- **Load a large step component lazily**: use the `component` prop on `<Step>` with a
|
|
479
|
-
dynamic import, for content that only some users see.
|