@businessdash/sdk 0.9.60
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/LICENSE +21 -0
- package/README.md +899 -0
- package/dist/adapters-revalidate.d.ts +44 -0
- package/dist/adapters-revalidate.d.ts.map +1 -0
- package/dist/adapters-revalidate.js +68 -0
- package/dist/adapters-revalidate.js.map +1 -0
- package/dist/alpha.d.ts +21 -0
- package/dist/alpha.d.ts.map +1 -0
- package/dist/alpha.js +21 -0
- package/dist/alpha.js.map +1 -0
- package/dist/analytics-core.d.ts +34 -0
- package/dist/analytics-core.d.ts.map +1 -0
- package/dist/analytics-core.js +138 -0
- package/dist/analytics-core.js.map +1 -0
- package/dist/analytics-server.d.ts +54 -0
- package/dist/analytics-server.d.ts.map +1 -0
- package/dist/analytics-server.js +78 -0
- package/dist/analytics-server.js.map +1 -0
- package/dist/attribution-logo.d.ts +2 -0
- package/dist/attribution-logo.d.ts.map +1 -0
- package/dist/attribution-logo.js +5 -0
- package/dist/attribution-logo.js.map +1 -0
- package/dist/auth-handler.d.ts +92 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +277 -0
- package/dist/auth-handler.js.map +1 -0
- package/dist/biab-forms.css +484 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1824 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +1681 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2845 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +13 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +13 -0
- package/dist/collections.js.map +1 -0
- package/dist/contracts.d.ts +14609 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2428 -0
- package/dist/contracts.js.map +1 -0
- package/dist/data-model-schema.d.ts +962 -0
- package/dist/data-model-schema.d.ts.map +1 -0
- package/dist/data-model-schema.js +2295 -0
- package/dist/data-model-schema.js.map +1 -0
- package/dist/distribution.d.ts +38 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +61 -0
- package/dist/distribution.js.map +1 -0
- package/dist/element.d.ts +121 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +276 -0
- package/dist/element.js.map +1 -0
- package/dist/enums.d.ts +25 -0
- package/dist/enums.d.ts.map +1 -0
- package/dist/enums.js +38 -0
- package/dist/enums.js.map +1 -0
- package/dist/errors.d.ts +195 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +188 -0
- package/dist/errors.js.map +1 -0
- package/dist/forms-core/autocomplete.d.ts +37 -0
- package/dist/forms-core/autocomplete.d.ts.map +1 -0
- package/dist/forms-core/autocomplete.js +82 -0
- package/dist/forms-core/autocomplete.js.map +1 -0
- package/dist/forms-core/business-function.d.ts +61 -0
- package/dist/forms-core/business-function.d.ts.map +1 -0
- package/dist/forms-core/business-function.js +149 -0
- package/dist/forms-core/business-function.js.map +1 -0
- package/dist/forms-core/calculation.d.ts +72 -0
- package/dist/forms-core/calculation.d.ts.map +1 -0
- package/dist/forms-core/calculation.js +103 -0
- package/dist/forms-core/calculation.js.map +1 -0
- package/dist/forms-core/controller.d.ts +249 -0
- package/dist/forms-core/controller.d.ts.map +1 -0
- package/dist/forms-core/controller.js +590 -0
- package/dist/forms-core/controller.js.map +1 -0
- package/dist/forms-core/elements.d.ts +44 -0
- package/dist/forms-core/elements.d.ts.map +1 -0
- package/dist/forms-core/elements.js +68 -0
- package/dist/forms-core/elements.js.map +1 -0
- package/dist/forms-core/email-domains.d.ts +10 -0
- package/dist/forms-core/email-domains.d.ts.map +1 -0
- package/dist/forms-core/email-domains.js +69 -0
- package/dist/forms-core/email-domains.js.map +1 -0
- package/dist/forms-core/file-rules.d.ts +51 -0
- package/dist/forms-core/file-rules.d.ts.map +1 -0
- package/dist/forms-core/file-rules.js +105 -0
- package/dist/forms-core/file-rules.js.map +1 -0
- package/dist/forms-core/formula.d.ts +114 -0
- package/dist/forms-core/formula.d.ts.map +1 -0
- package/dist/forms-core/formula.js +1170 -0
- package/dist/forms-core/formula.js.map +1 -0
- package/dist/forms-core/index.d.ts +24 -0
- package/dist/forms-core/index.d.ts.map +1 -0
- package/dist/forms-core/index.js +28 -0
- package/dist/forms-core/index.js.map +1 -0
- package/dist/forms-core/units.d.ts +49 -0
- package/dist/forms-core/units.d.ts.map +1 -0
- package/dist/forms-core/units.js +255 -0
- package/dist/forms-core/units.js.map +1 -0
- package/dist/forms-core/us-states.d.ts +14 -0
- package/dist/forms-core/us-states.d.ts.map +1 -0
- package/dist/forms-core/us-states.js +66 -0
- package/dist/forms-core/us-states.js.map +1 -0
- package/dist/forms-core/validation.d.ts +266 -0
- package/dist/forms-core/validation.d.ts.map +1 -0
- package/dist/forms-core/validation.js +850 -0
- package/dist/forms-core/validation.js.map +1 -0
- package/dist/forms.d.ts +19 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +29 -0
- package/dist/forms.js.map +1 -0
- package/dist/gallery-types.d.ts +32 -0
- package/dist/gallery-types.d.ts.map +1 -0
- package/dist/gallery-types.js +2 -0
- package/dist/gallery-types.js.map +1 -0
- package/dist/graphql.d.ts +97 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +212 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.cjs +19448 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/marketing-schema.d.ts +216 -0
- package/dist/marketing-schema.d.ts.map +1 -0
- package/dist/marketing-schema.js +200 -0
- package/dist/marketing-schema.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +87 -0
- package/dist/mcp.js.map +1 -0
- package/dist/native.d.ts +45 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +43 -0
- package/dist/native.js.map +1 -0
- package/dist/next-revalidate.d.ts +43 -0
- package/dist/next-revalidate.d.ts.map +1 -0
- package/dist/next-revalidate.js +84 -0
- package/dist/next-revalidate.js.map +1 -0
- package/dist/parallel-pages.d.ts +102 -0
- package/dist/parallel-pages.d.ts.map +1 -0
- package/dist/parallel-pages.js +108 -0
- package/dist/parallel-pages.js.map +1 -0
- package/dist/payment-icons.d.ts +66 -0
- package/dist/payment-icons.d.ts.map +1 -0
- package/dist/payment-icons.js +160 -0
- package/dist/payment-icons.js.map +1 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +97 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react-analytics.d.ts +39 -0
- package/dist/react-analytics.d.ts.map +1 -0
- package/dist/react-analytics.js +59 -0
- package/dist/react-analytics.js.map +1 -0
- package/dist/react-attribution.d.ts +99 -0
- package/dist/react-attribution.d.ts.map +1 -0
- package/dist/react-attribution.js +52 -0
- package/dist/react-attribution.js.map +1 -0
- package/dist/react-marketing.d.ts +181 -0
- package/dist/react-marketing.d.ts.map +1 -0
- package/dist/react-marketing.js +215 -0
- package/dist/react-marketing.js.map +1 -0
- package/dist/react.d.ts +727 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2418 -0
- package/dist/react.js.map +1 -0
- package/dist/reserved-paths.d.ts +24 -0
- package/dist/reserved-paths.d.ts.map +1 -0
- package/dist/reserved-paths.js +60 -0
- package/dist/reserved-paths.js.map +1 -0
- package/dist/revalidate-verify.d.ts +30 -0
- package/dist/revalidate-verify.d.ts.map +1 -0
- package/dist/revalidate-verify.js +74 -0
- package/dist/revalidate-verify.js.map +1 -0
- package/dist/sdk.d.ts +678 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +801 -0
- package/dist/sdk.js.map +1 -0
- package/dist/seo.d.ts +218 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +309 -0
- package/dist/seo.js.map +1 -0
- package/dist/socials.d.ts +47 -0
- package/dist/socials.d.ts.map +1 -0
- package/dist/socials.js +172 -0
- package/dist/socials.js.map +1 -0
- package/dist/static.d.ts +14 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +14 -0
- package/dist/static.js.map +1 -0
- package/dist/url-pattern.d.ts +47 -0
- package/dist/url-pattern.d.ts.map +1 -0
- package/dist/url-pattern.js +124 -0
- package/dist/url-pattern.js.map +1 -0
- package/dist/vanilla.d.ts +77 -0
- package/dist/vanilla.d.ts.map +1 -0
- package/dist/vanilla.js +903 -0
- package/dist/vanilla.js.map +1 -0
- package/openapi.json +13779 -0
- package/package.json +204 -0
- package/src/adapters-revalidate.ts +88 -0
- package/src/alpha.ts +89 -0
- package/src/analytics-core.ts +175 -0
- package/src/analytics-server.ts +99 -0
- package/src/angular/biab-form.component.ts +1443 -0
- package/src/angular/index.ts +26 -0
- package/src/attribution-logo.ts +5 -0
- package/src/auth-handler.ts +352 -0
- package/src/cli.ts +2517 -0
- package/src/client.ts +3578 -0
- package/src/collections.ts +101 -0
- package/src/contracts.ts +3390 -0
- package/src/data-model-schema.ts +4042 -0
- package/src/distribution.ts +73 -0
- package/src/element.ts +321 -0
- package/src/enums.ts +65 -0
- package/src/errors.ts +338 -0
- package/src/forms-core/autocomplete.ts +97 -0
- package/src/forms-core/business-function.ts +209 -0
- package/src/forms-core/calculation.ts +167 -0
- package/src/forms-core/controller.ts +922 -0
- package/src/forms-core/elements.ts +86 -0
- package/src/forms-core/email-domains.ts +68 -0
- package/src/forms-core/file-rules.ts +123 -0
- package/src/forms-core/formula.ts +1339 -0
- package/src/forms-core/index.ts +122 -0
- package/src/forms-core/units.ts +308 -0
- package/src/forms-core/us-states.ts +68 -0
- package/src/forms-core/validation.ts +1037 -0
- package/src/forms.ts +63 -0
- package/src/gallery-types.ts +31 -0
- package/src/graphql.ts +295 -0
- package/src/index.ts +505 -0
- package/src/marketing-schema.ts +395 -0
- package/src/mcp.ts +108 -0
- package/src/native.ts +73 -0
- package/src/next-revalidate.ts +124 -0
- package/src/parallel-pages.ts +192 -0
- package/src/payment-icons.ts +205 -0
- package/src/proxy.ts +173 -0
- package/src/qwik.tsx +1234 -0
- package/src/react-analytics.tsx +76 -0
- package/src/react-attribution.tsx +192 -0
- package/src/react-marketing.tsx +338 -0
- package/src/react.tsx +5495 -0
- package/src/reserved-paths.ts +65 -0
- package/src/revalidate-verify.ts +98 -0
- package/src/sdk.ts +1671 -0
- package/src/seo.ts +488 -0
- package/src/socials.ts +230 -0
- package/src/solid.tsx +1466 -0
- package/src/static.ts +21 -0
- package/src/styles/biab-forms.css +484 -0
- package/src/svelte/BiabForm.svelte +1069 -0
- package/src/svelte/context.ts +57 -0
- package/src/svelte/index.ts +79 -0
- package/src/url-pattern.ts +153 -0
- package/src/vanilla.ts +1290 -0
- package/src/vue/BiabForm.vue +1538 -0
- package/src/vue/index.ts +119 -0
package/src/qwik.tsx
ADDED
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
/** @jsxImportSource @builder.io/qwik */
|
|
2
|
+
/**
|
|
3
|
+
* `@businessdash/sdk/qwik` — the `<BiabForm>` drop-in for Qwik / Qwik City.
|
|
4
|
+
*
|
|
5
|
+
* Ship mode: SOURCE (.tsx). The consumer's Qwik optimizer compiles this file —
|
|
6
|
+
* it is never tsc-built into `dist`. Keep it syntactically valid Qwik JSX and
|
|
7
|
+
* self-consistent against the headless core's PUBLIC contract; do not reach
|
|
8
|
+
* into anything but `@businessdash/sdk/forms`.
|
|
9
|
+
*
|
|
10
|
+
* ─── Architecture ────────────────────────────────────────────────────────────
|
|
11
|
+
* The headless `createFormController` (`@businessdash/sdk/forms`) owns ALL of the
|
|
12
|
+
* form's runtime state and behaviour — values, condition-aware visibility,
|
|
13
|
+
* validation mirrored 1:1 from the BIAB server, multi-step nav + progress,
|
|
14
|
+
* concurrent-reveal gating, the submit lifecycle, and file uploads. This binding
|
|
15
|
+
* stays dumb: it
|
|
16
|
+
* 1. builds the controller (client-side, inside `useVisibleTask$`),
|
|
17
|
+
* 2. mirrors its `snapshot()` into a Qwik `useStore` on every `subscribe`
|
|
18
|
+
* notify (the idiomatic bridge from an external observable into Qwik
|
|
19
|
+
* reactivity), and
|
|
20
|
+
* 3. renders the nested field tree by `type` using the `biab-*` light-DOM
|
|
21
|
+
* class convention — the SAME classes the dashboard `FormPreview` renderer
|
|
22
|
+
* uses, so a consumer's `biab-*` CSS overrides cascade identically and the
|
|
23
|
+
* drop-in reproduces the dashboard preview (animation, multi-step progress
|
|
24
|
+
* bar/labels, concurrent reveal, org icon).
|
|
25
|
+
*
|
|
26
|
+
* ─── Why a client-side controller + a `submit$` QRL ──────────────────────────
|
|
27
|
+
* Qwik can't serialize the controller (a closure-over-state observable) across
|
|
28
|
+
* the resumability boundary, so it is created on the client from a SERIALIZABLE
|
|
29
|
+
* pre-fetched `schema` (the SSR pattern the Qwik starter already uses with
|
|
30
|
+
* `routeLoader$`). The bearer key must never enter the client bundle, so the
|
|
31
|
+
* actual `forms.submit` round-trip is delegated to a `submit$` QRL the consumer
|
|
32
|
+
* supplies (a `server$` RPC). When a `client` is passed directly (e.g. a public
|
|
33
|
+
* key in a fully client-rendered app) the controller's own `client.forms.submit`
|
|
34
|
+
* is used and `submit$` is optional.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
component$,
|
|
39
|
+
type NoSerialize,
|
|
40
|
+
noSerialize,
|
|
41
|
+
type QRL,
|
|
42
|
+
Slot,
|
|
43
|
+
useSignal,
|
|
44
|
+
useStore,
|
|
45
|
+
useVisibleTask$,
|
|
46
|
+
$,
|
|
47
|
+
} from "@builder.io/qwik";
|
|
48
|
+
|
|
49
|
+
import type { BiabClient } from "@businessdash/sdk";
|
|
50
|
+
import {
|
|
51
|
+
type FormCoreField,
|
|
52
|
+
type FormController,
|
|
53
|
+
type FormSchema,
|
|
54
|
+
type FormSnapshot,
|
|
55
|
+
type FormSubmitOptions,
|
|
56
|
+
type FormSubmitResult,
|
|
57
|
+
concurrentRevealCount,
|
|
58
|
+
createFormController,
|
|
59
|
+
evalCondition,
|
|
60
|
+
toSubmissionPayload,
|
|
61
|
+
} from "@businessdash/sdk/forms";
|
|
62
|
+
import {
|
|
63
|
+
type AutocompleteConfig,
|
|
64
|
+
resolveFieldAutocomplete,
|
|
65
|
+
} from "./forms-core/autocomplete.js";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A QRL the binding can `await` and call with `(...args)`, accepting BOTH a
|
|
69
|
+
* plain client `$()` QRL AND a `server$()`-produced `ServerQRL`. `server$`
|
|
70
|
+
* widens its inner function to `((abort: AbortSignal, ...args) => R) | ((...args)
|
|
71
|
+
* => R)`, so we mirror that union here — letting consumers pass a `server$` RPC
|
|
72
|
+
* directly without importing `@builder.io/qwik-city` types into this binding.
|
|
73
|
+
* The leading `AbortSignal` is injected by the Qwik runtime, never by us.
|
|
74
|
+
*/
|
|
75
|
+
type ServerCompatQRL<Args extends unknown[], R> = QRL<
|
|
76
|
+
| ((...args: Args) => Promise<R>)
|
|
77
|
+
| ((abort: AbortSignal, ...args: Args) => Promise<R>)
|
|
78
|
+
// server$ handlers commonly declare ONLY the first argument (the payload).
|
|
79
|
+
// Plain function variance would allow that, but QRL's type parameter is
|
|
80
|
+
// invariant, so the narrower shapes must be named explicitly — without
|
|
81
|
+
// these, `server$((payload) => …)` from qwik-city ^1.20 fails to assign.
|
|
82
|
+
| ((first: Args[0]) => Promise<R>)
|
|
83
|
+
| ((abort: AbortSignal, first: Args[0]) => Promise<R>)
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// Public props — consistent with the other framework bindings.
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export interface BiabFormProps {
|
|
91
|
+
/**
|
|
92
|
+
* Pre-fetched form schema (SSR / `routeLoader$`). PREFERRED on Qwik — it is
|
|
93
|
+
* serializable, keeps the bearer key server-side, and renders without a
|
|
94
|
+
* client-side flicker. Either `schema` or `client` (+`slug`) is required.
|
|
95
|
+
*/
|
|
96
|
+
schema?: FormSchema;
|
|
97
|
+
/**
|
|
98
|
+
* SDK client. Only safe in a fully client-rendered app with a public key —
|
|
99
|
+
* it pulls the schema and (unless `submit$` is given) submits from the
|
|
100
|
+
* browser. For SSR apps, prefer `schema` + `submit$`.
|
|
101
|
+
*/
|
|
102
|
+
client?: BiabClient;
|
|
103
|
+
/** Form slug. Required with `client` (the pre-fetched `schema` carries its own). */
|
|
104
|
+
slug?: string;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Server-side submit delegate (a `server$` QRL). Receives the re-keyed
|
|
108
|
+
* submission payload (output keys) + the merged submit options, and must
|
|
109
|
+
* return the SDK's `FormSubmitResult`. This is how the bearer key stays out
|
|
110
|
+
* of the client bundle. Optional when a `client` is supplied.
|
|
111
|
+
*/
|
|
112
|
+
submit$?: ServerCompatQRL<
|
|
113
|
+
[payload: Record<string, unknown>, options: FormSubmitOptions],
|
|
114
|
+
FormSubmitResult
|
|
115
|
+
>;
|
|
116
|
+
/**
|
|
117
|
+
* Server-side file-upload delegate (a `server$` QRL) for `file_upload`
|
|
118
|
+
* fields. Receives the field id + the picked File and must return the SDK's
|
|
119
|
+
* `FormFileValue` (`{ storageId, url, name, mimeType, size }`). Required for
|
|
120
|
+
* uploads in the SSR (no-client) path. Optional when a `client` is supplied.
|
|
121
|
+
*/
|
|
122
|
+
uploadFile$?: ServerCompatQRL<
|
|
123
|
+
[fieldId: string, file: File],
|
|
124
|
+
{
|
|
125
|
+
storageId: string;
|
|
126
|
+
url: string;
|
|
127
|
+
name: string;
|
|
128
|
+
mimeType: string;
|
|
129
|
+
size: number;
|
|
130
|
+
}
|
|
131
|
+
>;
|
|
132
|
+
|
|
133
|
+
/** Provenance / recipient passthrough merged into every submit (call wins). */
|
|
134
|
+
submitOptions?: FormSubmitOptions;
|
|
135
|
+
/** Convenience: lead-source provenance, folded into `submitOptions.source`. */
|
|
136
|
+
source?: string;
|
|
137
|
+
/** Test mode — validate server-side, store nothing, no side effects. */
|
|
138
|
+
dryRun?: boolean;
|
|
139
|
+
|
|
140
|
+
/** Label for the final submit button. Default "Submit". */
|
|
141
|
+
submitLabel?: string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* HTML autofill global switch. Default infers a sensible `autocomplete` token
|
|
145
|
+
* per field; `false` emits `autocomplete="off"` on every value-bearing input.
|
|
146
|
+
*/
|
|
147
|
+
autoComplete?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Per-field autofill overrides keyed by field id: a literal token, `true`
|
|
150
|
+
* (infer), or `false` (off). Wins over `autoComplete` and any schema token.
|
|
151
|
+
*/
|
|
152
|
+
fieldAutoComplete?: Record<string, string | boolean>;
|
|
153
|
+
|
|
154
|
+
/** Fires with the WHOLE re-keyed payload when local validation passes, before
|
|
155
|
+
* the network round-trip. Read-only hook for analytics / optimistic UI. */
|
|
156
|
+
onSubmit$?: QRL<(payload: Record<string, unknown>) => void>;
|
|
157
|
+
/** Fires after a successful submit (`result.ok === true`). */
|
|
158
|
+
onSuccess$?: QRL<(result: Extract<FormSubmitResult, { ok: true }>) => void>;
|
|
159
|
+
/** Fires after a failed submit (`result.ok === false`). */
|
|
160
|
+
onError$?: QRL<(result: Extract<FormSubmitResult, { ok: false }>) => void>;
|
|
161
|
+
|
|
162
|
+
/** Extra class on the root `.biab-form`. Both `class` and `className` work. */
|
|
163
|
+
class?: string;
|
|
164
|
+
className?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// A serializable mirror of the snapshot. Qwik's `useStore` deep-tracks plain
|
|
169
|
+
// data; the controller's `FormSnapshot` is plain data + nested field trees, so
|
|
170
|
+
// we copy the whole thing in. `result` / `schema` are plain JSON too.
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
type SnapshotState = { snap: FormSnapshot | null };
|
|
174
|
+
|
|
175
|
+
/** Read-shape of the signal holding the (client-only) controller. */
|
|
176
|
+
type ControllerRef = { value: FormController | undefined };
|
|
177
|
+
|
|
178
|
+
/** The resolved server-side upload delegate threaded to `file_upload` fields. */
|
|
179
|
+
type UploadDelegate = BiabFormProps["uploadFile$"];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build the initial snapshot for SSR from a pre-fetched schema so the fields
|
|
183
|
+
* render immediately (no client flicker). `createFormController({ schema })` is
|
|
184
|
+
* a pure synchronous factory — no client / network — and `snapshot()` returns
|
|
185
|
+
* plain serializable data, which is exactly what Qwik needs to resume.
|
|
186
|
+
*/
|
|
187
|
+
function seedSnapshot(schema: FormSchema | undefined): FormSnapshot | null {
|
|
188
|
+
if (!schema) return null;
|
|
189
|
+
const ctrl = createFormController({ schema });
|
|
190
|
+
// `load()` adopts the pre-fetched schema synchronously (no await needed for
|
|
191
|
+
// the snapshot to become `ready`), but the synchronous factory already seeds
|
|
192
|
+
// a valid pre-load snapshot; calling load here keeps status === "ready".
|
|
193
|
+
void ctrl.load();
|
|
194
|
+
const snap = ctrl.snapshot();
|
|
195
|
+
ctrl.destroy();
|
|
196
|
+
return snap;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Field rendering — a faithful port of the dashboard `FormPreview` per-type
|
|
201
|
+
// switch, using `biab-*` classes (light DOM). Qwik JSX uses `class`, not
|
|
202
|
+
// `className`, and `onInput$` / `onClick$` event QRLs.
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/** Static / layout types that never carry a submitted value (skip the label). */
|
|
206
|
+
const NO_LABEL_TYPES = new Set([
|
|
207
|
+
"title",
|
|
208
|
+
"subtitle",
|
|
209
|
+
"separator",
|
|
210
|
+
"richtext",
|
|
211
|
+
"image",
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
/** Stable, override-friendly CSS hook for a field (the `data-biab-field` value). */
|
|
215
|
+
function fieldCssKey(field: FormCoreField): string {
|
|
216
|
+
const explicit =
|
|
217
|
+
typeof field.outputKey === "string" ? field.outputKey.trim() : "";
|
|
218
|
+
if (explicit) return explicit;
|
|
219
|
+
if (typeof field.key === "string" && field.key) return field.key;
|
|
220
|
+
return field.id;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function classOf(...parts: Array<string | false | null | undefined>): string {
|
|
224
|
+
return parts.filter(Boolean).join(" ");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function asString(value: unknown): string {
|
|
228
|
+
return typeof value === "string" ? value : "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The leaf input for one value-bearing field. Reads/writes through `state.snap`
|
|
233
|
+
* via the controller's `setValue` / `uploadFile`. Renders by `type` exactly as
|
|
234
|
+
* the dashboard preview does.
|
|
235
|
+
*/
|
|
236
|
+
const BiabFieldInput = component$<{
|
|
237
|
+
field: FormCoreField;
|
|
238
|
+
controllerSig: ControllerRef;
|
|
239
|
+
state: SnapshotState;
|
|
240
|
+
uploadFn?: UploadDelegate;
|
|
241
|
+
acConfig: AutocompleteConfig;
|
|
242
|
+
}>(({ field, controllerSig, state, uploadFn, acConfig }) => {
|
|
243
|
+
const value = state.snap?.values?.[field.id];
|
|
244
|
+
|
|
245
|
+
const set = $((v: unknown) => {
|
|
246
|
+
controllerSig.value?.setValue(field.id, v);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Resolved HTML autofill token for this field (or `undefined` to omit the
|
|
250
|
+
// attribute). Honors the global `autoComplete` switch + per-field overrides;
|
|
251
|
+
// see forms-core/autocomplete.ts.
|
|
252
|
+
const autoComplete = resolveFieldAutocomplete(field, acConfig);
|
|
253
|
+
|
|
254
|
+
switch (field.type) {
|
|
255
|
+
case "title":
|
|
256
|
+
return <h2 class="biab-field__title">{field.label || "Untitled"}</h2>;
|
|
257
|
+
case "subtitle":
|
|
258
|
+
return <h3 class="biab-field__subtitle">{field.label || ""}</h3>;
|
|
259
|
+
case "separator":
|
|
260
|
+
return <hr class="biab-field__separator" />;
|
|
261
|
+
case "richtext":
|
|
262
|
+
// `content` is server-sanitized HTML on publish.
|
|
263
|
+
return field.content ? (
|
|
264
|
+
<div class="biab-field__richtext" dangerouslySetInnerHTML={field.content} />
|
|
265
|
+
) : null;
|
|
266
|
+
case "image":
|
|
267
|
+
return field.content ? (
|
|
268
|
+
<img class="biab-field__image" src={field.content} alt={field.label || ""} />
|
|
269
|
+
) : null;
|
|
270
|
+
|
|
271
|
+
case "textarea":
|
|
272
|
+
return (
|
|
273
|
+
<textarea
|
|
274
|
+
class="biab-input biab-textarea"
|
|
275
|
+
value={asString(value)}
|
|
276
|
+
placeholder={field.placeholder || ""}
|
|
277
|
+
rows={4}
|
|
278
|
+
autocomplete={autoComplete as AutoFill | undefined}
|
|
279
|
+
onInput$={(_, el) => void set((el as HTMLTextAreaElement).value)}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
case "select":
|
|
284
|
+
return (
|
|
285
|
+
<select
|
|
286
|
+
class="biab-input biab-select"
|
|
287
|
+
value={asString(value)}
|
|
288
|
+
onChange$={(_, el) => void set((el as HTMLSelectElement).value)}
|
|
289
|
+
>
|
|
290
|
+
<option value="">{field.placeholder || "Select..."}</option>
|
|
291
|
+
{(field.options ?? []).map((opt) => (
|
|
292
|
+
<option key={opt} value={opt}>
|
|
293
|
+
{opt}
|
|
294
|
+
</option>
|
|
295
|
+
))}
|
|
296
|
+
</select>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
case "radio":
|
|
300
|
+
return (
|
|
301
|
+
<div class="biab-radio-group">
|
|
302
|
+
{(field.options ?? []).map((opt) => (
|
|
303
|
+
<label class="biab-radio" key={opt}>
|
|
304
|
+
<input
|
|
305
|
+
type="radio"
|
|
306
|
+
name={field.id}
|
|
307
|
+
value={opt}
|
|
308
|
+
checked={asString(value) === opt}
|
|
309
|
+
onChange$={() => void set(opt)}
|
|
310
|
+
/>
|
|
311
|
+
<span>{opt}</span>
|
|
312
|
+
</label>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
case "checkbox": {
|
|
318
|
+
const arr = Array.isArray(value) ? (value as string[]) : [];
|
|
319
|
+
return (
|
|
320
|
+
<div class="biab-checkbox-group">
|
|
321
|
+
{(field.options ?? []).map((opt) => {
|
|
322
|
+
const checked = arr.includes(opt);
|
|
323
|
+
return (
|
|
324
|
+
<label class="biab-checkbox" key={opt}>
|
|
325
|
+
<input
|
|
326
|
+
type="checkbox"
|
|
327
|
+
checked={checked}
|
|
328
|
+
onChange$={(_, el) => {
|
|
329
|
+
const next = (el as HTMLInputElement).checked
|
|
330
|
+
? [...arr, opt]
|
|
331
|
+
: arr.filter((x) => x !== opt);
|
|
332
|
+
void set(next);
|
|
333
|
+
}}
|
|
334
|
+
/>
|
|
335
|
+
<span>{opt}</span>
|
|
336
|
+
</label>
|
|
337
|
+
);
|
|
338
|
+
})}
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case "boolean":
|
|
344
|
+
return (
|
|
345
|
+
<label class="biab-switch">
|
|
346
|
+
<input
|
|
347
|
+
type="checkbox"
|
|
348
|
+
checked={value === true}
|
|
349
|
+
onChange$={(_, el) => void set((el as HTMLInputElement).checked)}
|
|
350
|
+
/>
|
|
351
|
+
<span class="biab-switch__label">{field.placeholder || "Yes / No"}</span>
|
|
352
|
+
</label>
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
case "date":
|
|
356
|
+
return (
|
|
357
|
+
<input
|
|
358
|
+
class="biab-input biab-date"
|
|
359
|
+
type="date"
|
|
360
|
+
value={asString(value)}
|
|
361
|
+
onInput$={(_, el) => void set((el as HTMLInputElement).value)}
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
case "date_range": {
|
|
366
|
+
const v =
|
|
367
|
+
value && typeof value === "object"
|
|
368
|
+
? (value as { start?: string; end?: string })
|
|
369
|
+
: { start: "", end: "" };
|
|
370
|
+
return (
|
|
371
|
+
<div class="biab-date-range">
|
|
372
|
+
<label class="biab-date-range__field">
|
|
373
|
+
<span class="biab-date-range__label">Start</span>
|
|
374
|
+
<input
|
|
375
|
+
class="biab-input"
|
|
376
|
+
type="date"
|
|
377
|
+
value={v.start ?? ""}
|
|
378
|
+
onInput$={(_, el) =>
|
|
379
|
+
void set({ ...v, start: (el as HTMLInputElement).value })
|
|
380
|
+
}
|
|
381
|
+
/>
|
|
382
|
+
</label>
|
|
383
|
+
<label class="biab-date-range__field">
|
|
384
|
+
<span class="biab-date-range__label">End</span>
|
|
385
|
+
<input
|
|
386
|
+
class="biab-input"
|
|
387
|
+
type="date"
|
|
388
|
+
value={v.end ?? ""}
|
|
389
|
+
onInput$={(_, el) =>
|
|
390
|
+
void set({ ...v, end: (el as HTMLInputElement).value })
|
|
391
|
+
}
|
|
392
|
+
/>
|
|
393
|
+
</label>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
case "time_picker": {
|
|
399
|
+
const mode = field.timeConfig?.mode ?? "simple";
|
|
400
|
+
if (mode === "simple") {
|
|
401
|
+
const step = Math.max(field.timeConfig?.stepMinutes ?? 30, 1);
|
|
402
|
+
const format = field.timeConfig?.format ?? "12h";
|
|
403
|
+
const opts: string[] = [];
|
|
404
|
+
for (let h = 0; h < 24; h++) {
|
|
405
|
+
for (let m = 0; m < 60; m += step) {
|
|
406
|
+
opts.push(
|
|
407
|
+
`${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const fmt = (t: string) => {
|
|
412
|
+
if (format === "24h") return t;
|
|
413
|
+
const [hStr, mStr = "00"] = t.split(":");
|
|
414
|
+
const h = Number(hStr);
|
|
415
|
+
const ampm = h < 12 ? "AM" : "PM";
|
|
416
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
417
|
+
return `${h12}:${mStr} ${ampm}`;
|
|
418
|
+
};
|
|
419
|
+
return (
|
|
420
|
+
<select
|
|
421
|
+
class="biab-input biab-select biab-time"
|
|
422
|
+
value={asString(value)}
|
|
423
|
+
onChange$={(_, el) => void set((el as HTMLSelectElement).value)}
|
|
424
|
+
>
|
|
425
|
+
<option value="">{field.placeholder || "Select time"}</option>
|
|
426
|
+
{opts.map((o) => (
|
|
427
|
+
<option key={o} value={o}>
|
|
428
|
+
{fmt(o)}
|
|
429
|
+
</option>
|
|
430
|
+
))}
|
|
431
|
+
</select>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (mode === "time_block") {
|
|
435
|
+
const v =
|
|
436
|
+
value && typeof value === "object"
|
|
437
|
+
? (value as { start?: string; end?: string })
|
|
438
|
+
: { start: "", end: "" };
|
|
439
|
+
return (
|
|
440
|
+
<div class="biab-time-block">
|
|
441
|
+
<label class="biab-time-block__field">
|
|
442
|
+
<span class="biab-time-block__label">Start</span>
|
|
443
|
+
<input
|
|
444
|
+
class="biab-input"
|
|
445
|
+
type="time"
|
|
446
|
+
value={v.start ?? ""}
|
|
447
|
+
onInput$={(_, el) =>
|
|
448
|
+
void set({ ...v, start: (el as HTMLInputElement).value })
|
|
449
|
+
}
|
|
450
|
+
/>
|
|
451
|
+
</label>
|
|
452
|
+
<label class="biab-time-block__field">
|
|
453
|
+
<span class="biab-time-block__label">End</span>
|
|
454
|
+
<input
|
|
455
|
+
class="biab-input"
|
|
456
|
+
type="time"
|
|
457
|
+
value={v.end ?? ""}
|
|
458
|
+
onInput$={(_, el) =>
|
|
459
|
+
void set({ ...v, end: (el as HTMLInputElement).value })
|
|
460
|
+
}
|
|
461
|
+
/>
|
|
462
|
+
</label>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
// appointment
|
|
467
|
+
const v =
|
|
468
|
+
value && typeof value === "object"
|
|
469
|
+
? (value as { date?: string; time?: string })
|
|
470
|
+
: { date: "", time: "" };
|
|
471
|
+
return (
|
|
472
|
+
<div class="biab-appointment">
|
|
473
|
+
<label class="biab-appointment__field">
|
|
474
|
+
<span class="biab-appointment__label">Date</span>
|
|
475
|
+
<input
|
|
476
|
+
class="biab-input"
|
|
477
|
+
type="date"
|
|
478
|
+
value={v.date ?? ""}
|
|
479
|
+
onInput$={(_, el) =>
|
|
480
|
+
void set({ ...v, date: (el as HTMLInputElement).value })
|
|
481
|
+
}
|
|
482
|
+
/>
|
|
483
|
+
</label>
|
|
484
|
+
<label class="biab-appointment__field">
|
|
485
|
+
<span class="biab-appointment__label">Time</span>
|
|
486
|
+
<input
|
|
487
|
+
class="biab-input"
|
|
488
|
+
type="time"
|
|
489
|
+
value={v.time ?? ""}
|
|
490
|
+
onInput$={(_, el) =>
|
|
491
|
+
void set({ ...v, time: (el as HTMLInputElement).value })
|
|
492
|
+
}
|
|
493
|
+
/>
|
|
494
|
+
</label>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
case "slider": {
|
|
500
|
+
const min = field.sliderConfig?.min ?? 0;
|
|
501
|
+
const max = field.sliderConfig?.max ?? 100;
|
|
502
|
+
const step = field.sliderConfig?.step ?? 1;
|
|
503
|
+
const unit = field.sliderConfig?.unit;
|
|
504
|
+
const v = typeof value === "number" ? value : min;
|
|
505
|
+
const fmt = (n: number) =>
|
|
506
|
+
unit === "currency" ? `$${n}` : unit === "percent" ? `${n}%` : `${n}`;
|
|
507
|
+
return (
|
|
508
|
+
<div class="biab-slider">
|
|
509
|
+
<div class="biab-slider__scale">
|
|
510
|
+
<span>{fmt(min)}</span>
|
|
511
|
+
<span class="biab-slider__value">{fmt(v)}</span>
|
|
512
|
+
<span>{fmt(max)}</span>
|
|
513
|
+
</div>
|
|
514
|
+
<input
|
|
515
|
+
class="biab-slider__input"
|
|
516
|
+
type="range"
|
|
517
|
+
min={min}
|
|
518
|
+
max={max}
|
|
519
|
+
step={step}
|
|
520
|
+
value={v}
|
|
521
|
+
onInput$={(_, el) => void set(Number((el as HTMLInputElement).value))}
|
|
522
|
+
/>
|
|
523
|
+
</div>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
case "star_rating": {
|
|
528
|
+
const max = field.starRatingConfig?.maxStars ?? 5;
|
|
529
|
+
const v = typeof value === "number" ? value : 0;
|
|
530
|
+
return (
|
|
531
|
+
<div class="biab-stars">
|
|
532
|
+
{Array.from({ length: max }).map((_, i) => (
|
|
533
|
+
<button
|
|
534
|
+
key={`${field.id}-star-${i}`}
|
|
535
|
+
type="button"
|
|
536
|
+
class={classOf("biab-star", i < v && "biab-star--on")}
|
|
537
|
+
aria-label={`${i + 1} star${i === 0 ? "" : "s"}`}
|
|
538
|
+
onClick$={() => void set(i + 1)}
|
|
539
|
+
>
|
|
540
|
+
★
|
|
541
|
+
</button>
|
|
542
|
+
))}
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
case "happiness_rating": {
|
|
548
|
+
const v = typeof value === "number" ? value : -1;
|
|
549
|
+
const faces = ["😞", "😕", "😐", "🙂", "😄"];
|
|
550
|
+
return (
|
|
551
|
+
<div class="biab-happiness">
|
|
552
|
+
{faces.map((emoji, i) => (
|
|
553
|
+
<button
|
|
554
|
+
key={emoji}
|
|
555
|
+
type="button"
|
|
556
|
+
class={classOf("biab-happiness__face", i === v && "biab-happiness__face--on")}
|
|
557
|
+
onClick$={() => void set(i)}
|
|
558
|
+
>
|
|
559
|
+
{emoji}
|
|
560
|
+
</button>
|
|
561
|
+
))}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "stepper": {
|
|
567
|
+
const v = typeof value === "number" ? value : 0;
|
|
568
|
+
return (
|
|
569
|
+
<div class="biab-stepper">
|
|
570
|
+
<button
|
|
571
|
+
type="button"
|
|
572
|
+
class="biab-stepper__btn biab-stepper__btn--dec"
|
|
573
|
+
onClick$={() => void set(v - 1)}
|
|
574
|
+
>
|
|
575
|
+
−
|
|
576
|
+
</button>
|
|
577
|
+
<span class="biab-stepper__value">{v}</span>
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
class="biab-stepper__btn biab-stepper__btn--inc"
|
|
581
|
+
onClick$={() => void set(v + 1)}
|
|
582
|
+
>
|
|
583
|
+
+
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
case "color_picker": {
|
|
590
|
+
const v = asString(value);
|
|
591
|
+
return (
|
|
592
|
+
<div class="biab-colors">
|
|
593
|
+
{(field.options ?? []).map((color) => (
|
|
594
|
+
<button
|
|
595
|
+
key={color}
|
|
596
|
+
type="button"
|
|
597
|
+
class={classOf("biab-color", v === color && "biab-color--on")}
|
|
598
|
+
style={{ backgroundColor: color }}
|
|
599
|
+
title={color}
|
|
600
|
+
aria-label={color}
|
|
601
|
+
onClick$={() => void set(color)}
|
|
602
|
+
/>
|
|
603
|
+
))}
|
|
604
|
+
</div>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
case "address": {
|
|
609
|
+
const v = (value && typeof value === "object" ? value : {}) as Record<
|
|
610
|
+
string,
|
|
611
|
+
string
|
|
612
|
+
>;
|
|
613
|
+
// Per-subfield autofill tokens (street/line2/city/state/zip), suppressed
|
|
614
|
+
// to "off" when autofill is off for this field (global switch or override).
|
|
615
|
+
const acOff = autoComplete === "off";
|
|
616
|
+
const part = (
|
|
617
|
+
key: string,
|
|
618
|
+
label: string,
|
|
619
|
+
ac: string,
|
|
620
|
+
) => (
|
|
621
|
+
<label class={classOf("biab-address__field", `biab-address__field--${key}`)}>
|
|
622
|
+
<span class="biab-address__label">{label}</span>
|
|
623
|
+
<input
|
|
624
|
+
class="biab-input"
|
|
625
|
+
value={v[key] ?? ""}
|
|
626
|
+
autoComplete={acOff ? "off" : ac}
|
|
627
|
+
onInput$={(_, el) =>
|
|
628
|
+
void set({ ...v, [key]: (el as HTMLInputElement).value })
|
|
629
|
+
}
|
|
630
|
+
/>
|
|
631
|
+
</label>
|
|
632
|
+
);
|
|
633
|
+
return (
|
|
634
|
+
<div class="biab-address">
|
|
635
|
+
{part("line1", "Address Line 1", "street-address")}
|
|
636
|
+
{part("line2", "Address Line 2", "address-line2")}
|
|
637
|
+
{part("city", "City", "address-level2")}
|
|
638
|
+
{part("state", "State", "address-level1")}
|
|
639
|
+
{part("zip", "Zip", "postal-code")}
|
|
640
|
+
</div>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
case "file_upload": {
|
|
645
|
+
const files = Array.isArray(value)
|
|
646
|
+
? (value as Array<{ name: string; size: number; url: string }>)
|
|
647
|
+
: [];
|
|
648
|
+
const maxFiles = field.fileConfig?.maxFiles ?? 1;
|
|
649
|
+
const allowed = field.fileConfig?.allowedTypes;
|
|
650
|
+
const accept = allowed && allowed.length > 0 ? allowed.join(",") : undefined;
|
|
651
|
+
const remaining = Math.max(maxFiles - files.length, 0);
|
|
652
|
+
const uploading = state.snap?.uploading === true;
|
|
653
|
+
return (
|
|
654
|
+
<div class="biab-file">
|
|
655
|
+
<input
|
|
656
|
+
class="biab-file__input"
|
|
657
|
+
type="file"
|
|
658
|
+
multiple={maxFiles > 1}
|
|
659
|
+
accept={accept}
|
|
660
|
+
disabled={uploading || remaining === 0}
|
|
661
|
+
onChange$={async (_, el) => {
|
|
662
|
+
const picked = (el as HTMLInputElement).files;
|
|
663
|
+
const ctrl = controllerSig.value;
|
|
664
|
+
if (!picked || picked.length === 0 || !ctrl) return;
|
|
665
|
+
const list = Array.from(picked).slice(0, remaining);
|
|
666
|
+
for (const file of list) {
|
|
667
|
+
try {
|
|
668
|
+
if (uploadFn) {
|
|
669
|
+
// SSR / no-client path: upload via the consumer's `server$`
|
|
670
|
+
// delegate (bearer key stays server-side), then append the
|
|
671
|
+
// returned FormFileValue to this field's value array.
|
|
672
|
+
const uploaded = await uploadFn(field.id, file);
|
|
673
|
+
const existing = Array.isArray(
|
|
674
|
+
ctrl.snapshot().values[field.id],
|
|
675
|
+
)
|
|
676
|
+
? (ctrl.snapshot().values[field.id] as unknown[])
|
|
677
|
+
: [];
|
|
678
|
+
ctrl.setValue(field.id, [...existing, uploaded]);
|
|
679
|
+
} else {
|
|
680
|
+
// Client path: the controller uploads via its own SDK client.
|
|
681
|
+
await ctrl.uploadFile(field.id, file, {
|
|
682
|
+
filename: file.name,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
} catch {
|
|
686
|
+
/* controller surfaces upload state; swallow per-file failure */
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
(el as HTMLInputElement).value = "";
|
|
690
|
+
}}
|
|
691
|
+
/>
|
|
692
|
+
{uploading ? <span class="biab-file__status">Uploading…</span> : null}
|
|
693
|
+
{files.length > 0 ? (
|
|
694
|
+
<ul class="biab-file__list">
|
|
695
|
+
{files.map((f, idx) => (
|
|
696
|
+
<li class="biab-file__item" key={`${f.url}-${idx}`}>
|
|
697
|
+
<span class="biab-file__name">{f.name}</span>
|
|
698
|
+
<span class="biab-file__size">
|
|
699
|
+
{Math.round(f.size / 1024)} KB
|
|
700
|
+
</span>
|
|
701
|
+
</li>
|
|
702
|
+
))}
|
|
703
|
+
</ul>
|
|
704
|
+
) : null}
|
|
705
|
+
</div>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
case "phone":
|
|
710
|
+
return (
|
|
711
|
+
<input
|
|
712
|
+
class="biab-input biab-phone"
|
|
713
|
+
type="tel"
|
|
714
|
+
value={asString(value)}
|
|
715
|
+
placeholder={
|
|
716
|
+
field.placeholder ||
|
|
717
|
+
(field.phoneConfig?.international
|
|
718
|
+
? "+1 (555) 000-0000"
|
|
719
|
+
: "(555) 000-0000")
|
|
720
|
+
}
|
|
721
|
+
autoComplete={autoComplete}
|
|
722
|
+
onInput$={(_, el) => void set((el as HTMLInputElement).value)}
|
|
723
|
+
/>
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
case "email":
|
|
727
|
+
return (
|
|
728
|
+
<input
|
|
729
|
+
class="biab-input biab-email"
|
|
730
|
+
type="email"
|
|
731
|
+
value={asString(value)}
|
|
732
|
+
placeholder={field.placeholder || "you@example.com"}
|
|
733
|
+
autoComplete={autoComplete}
|
|
734
|
+
onInput$={(_, el) => void set((el as HTMLInputElement).value)}
|
|
735
|
+
/>
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
case "number":
|
|
739
|
+
return (
|
|
740
|
+
<input
|
|
741
|
+
class="biab-input biab-number"
|
|
742
|
+
type="number"
|
|
743
|
+
value={typeof value === "number" ? value : asString(value)}
|
|
744
|
+
placeholder={field.placeholder || ""}
|
|
745
|
+
onInput$={(_, el) => {
|
|
746
|
+
const raw = (el as HTMLInputElement).value;
|
|
747
|
+
void set(raw === "" ? "" : Number(raw));
|
|
748
|
+
}}
|
|
749
|
+
/>
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
default:
|
|
753
|
+
return (
|
|
754
|
+
<input
|
|
755
|
+
class="biab-input biab-text"
|
|
756
|
+
type="text"
|
|
757
|
+
value={asString(value)}
|
|
758
|
+
placeholder={field.placeholder || ""}
|
|
759
|
+
autoComplete={autoComplete}
|
|
760
|
+
onInput$={(_, el) => void set((el as HTMLInputElement).value)}
|
|
761
|
+
/>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* One field "row": the `biab-field` wrapper with stable data-attrs + error /
|
|
768
|
+
* help text, mirroring the dashboard `FieldRow`.
|
|
769
|
+
*/
|
|
770
|
+
const BiabFieldRow = component$<{
|
|
771
|
+
field: FormCoreField;
|
|
772
|
+
controllerSig: ControllerRef;
|
|
773
|
+
state: SnapshotState;
|
|
774
|
+
uploadFn?: UploadDelegate;
|
|
775
|
+
acConfig: AutocompleteConfig;
|
|
776
|
+
}>(({ field, controllerSig, state, uploadFn, acConfig }) => {
|
|
777
|
+
const showLabel = !NO_LABEL_TYPES.has(field.type);
|
|
778
|
+
const error = state.snap?.errors?.[field.id];
|
|
779
|
+
return (
|
|
780
|
+
<div
|
|
781
|
+
class={classOf(
|
|
782
|
+
"biab-field",
|
|
783
|
+
`biab-field--${field.type}`,
|
|
784
|
+
field.required && "biab-field--required",
|
|
785
|
+
error && "biab-field--error",
|
|
786
|
+
)}
|
|
787
|
+
data-biab-field={fieldCssKey(field)}
|
|
788
|
+
data-biab-type={field.type}
|
|
789
|
+
>
|
|
790
|
+
{showLabel ? (
|
|
791
|
+
<label class="biab-field__label">
|
|
792
|
+
{field.label || ""}
|
|
793
|
+
{field.required ? <span class="biab-field__required">*</span> : null}
|
|
794
|
+
</label>
|
|
795
|
+
) : null}
|
|
796
|
+
<BiabFieldInput
|
|
797
|
+
field={field}
|
|
798
|
+
controllerSig={controllerSig}
|
|
799
|
+
state={state}
|
|
800
|
+
uploadFn={uploadFn}
|
|
801
|
+
acConfig={acConfig}
|
|
802
|
+
/>
|
|
803
|
+
{error ? <p class="biab-field__error">{error.message}</p> : null}
|
|
804
|
+
{field.helpText ? (
|
|
805
|
+
<p class="biab-field__help">{field.helpText}</p>
|
|
806
|
+
) : null}
|
|
807
|
+
</div>
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Recursive render of the nested field tree — mirrors `FormPreview.RenderTree`:
|
|
813
|
+
* `input_group` (with concurrent-reveal gating + optional group label),
|
|
814
|
+
* `condition_block` (skipped when its rule is false), `or_condition` (two sides
|
|
815
|
+
* with an OR divider), `flex_row` (inline row), else a `BiabFieldRow`.
|
|
816
|
+
*/
|
|
817
|
+
const BiabRenderTree = component$<{
|
|
818
|
+
fields: FormCoreField[];
|
|
819
|
+
controllerSig: ControllerRef;
|
|
820
|
+
state: SnapshotState;
|
|
821
|
+
uploadFn?: UploadDelegate;
|
|
822
|
+
acConfig: AutocompleteConfig;
|
|
823
|
+
flat?: boolean;
|
|
824
|
+
}>(({ fields, controllerSig, state, uploadFn, acConfig, flat }) => {
|
|
825
|
+
const data = state.snap?.values ?? {};
|
|
826
|
+
return (
|
|
827
|
+
<div class="biab-tree">
|
|
828
|
+
{fields.map((f) => {
|
|
829
|
+
if (f.type === "input_group") {
|
|
830
|
+
const children = (f.children ?? []) as FormCoreField[];
|
|
831
|
+
const concurrent = f.concurrentReveal === true;
|
|
832
|
+
const showGroupLabel = f.showGroupLabel === true;
|
|
833
|
+
const revealCount = concurrent
|
|
834
|
+
? concurrentRevealCount(children, data)
|
|
835
|
+
: children.length;
|
|
836
|
+
const shown = children.slice(0, revealCount);
|
|
837
|
+
return (
|
|
838
|
+
<div
|
|
839
|
+
key={f.id}
|
|
840
|
+
class={classOf("biab-group", flat && "biab-group--flat")}
|
|
841
|
+
data-biab-group={fieldCssKey(f)}
|
|
842
|
+
>
|
|
843
|
+
{f.label && showGroupLabel ? (
|
|
844
|
+
<div class="biab-group__label">{f.label}</div>
|
|
845
|
+
) : null}
|
|
846
|
+
<BiabRenderTree
|
|
847
|
+
fields={shown}
|
|
848
|
+
controllerSig={controllerSig}
|
|
849
|
+
state={state}
|
|
850
|
+
uploadFn={uploadFn}
|
|
851
|
+
acConfig={acConfig}
|
|
852
|
+
/>
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (f.type === "condition_block") {
|
|
858
|
+
if (!evalCondition(f.conditionRule, data)) return null;
|
|
859
|
+
return (
|
|
860
|
+
<BiabRenderTree
|
|
861
|
+
key={f.id}
|
|
862
|
+
fields={(f.children ?? []) as FormCoreField[]}
|
|
863
|
+
controllerSig={controllerSig}
|
|
864
|
+
state={state}
|
|
865
|
+
uploadFn={uploadFn}
|
|
866
|
+
acConfig={acConfig}
|
|
867
|
+
/>
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (f.type === "or_condition") {
|
|
872
|
+
const left = f.children?.[0];
|
|
873
|
+
const right = f.children?.[1];
|
|
874
|
+
const error = state.snap?.errors?.[f.id];
|
|
875
|
+
return (
|
|
876
|
+
<div
|
|
877
|
+
key={f.id}
|
|
878
|
+
class={classOf("biab-or", error && "biab-or--error")}
|
|
879
|
+
data-biab-or={fieldCssKey(f)}
|
|
880
|
+
>
|
|
881
|
+
{f.label ? (
|
|
882
|
+
<div class="biab-or__label">
|
|
883
|
+
{f.label}
|
|
884
|
+
<span class="biab-or__hint">Fill at least one side</span>
|
|
885
|
+
</div>
|
|
886
|
+
) : null}
|
|
887
|
+
<div class="biab-or__grid">
|
|
888
|
+
<div class="biab-or__side biab-or__side--left">
|
|
889
|
+
<div class="biab-or__side-label">
|
|
890
|
+
{f.orConditionConfig?.leftLabel || "Option A"}
|
|
891
|
+
</div>
|
|
892
|
+
{left ? (
|
|
893
|
+
<BiabRenderTree
|
|
894
|
+
fields={[left]}
|
|
895
|
+
controllerSig={controllerSig}
|
|
896
|
+
state={state}
|
|
897
|
+
uploadFn={uploadFn}
|
|
898
|
+
acConfig={acConfig}
|
|
899
|
+
/>
|
|
900
|
+
) : null}
|
|
901
|
+
</div>
|
|
902
|
+
<div class="biab-or__divider">
|
|
903
|
+
<span class="biab-or__badge">OR</span>
|
|
904
|
+
</div>
|
|
905
|
+
<div class="biab-or__side biab-or__side--right">
|
|
906
|
+
<div class="biab-or__side-label">
|
|
907
|
+
{f.orConditionConfig?.rightLabel || "Option B"}
|
|
908
|
+
</div>
|
|
909
|
+
{right ? (
|
|
910
|
+
<BiabRenderTree
|
|
911
|
+
fields={[right]}
|
|
912
|
+
controllerSig={controllerSig}
|
|
913
|
+
state={state}
|
|
914
|
+
uploadFn={uploadFn}
|
|
915
|
+
acConfig={acConfig}
|
|
916
|
+
/>
|
|
917
|
+
) : null}
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
{error ? (
|
|
921
|
+
<p class="biab-or__error">Fill in at least one side to continue.</p>
|
|
922
|
+
) : null}
|
|
923
|
+
</div>
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (f.type === "flex_row") {
|
|
928
|
+
const children = (f.children ?? []) as FormCoreField[];
|
|
929
|
+
return (
|
|
930
|
+
<div key={f.id} class="biab-flex-row">
|
|
931
|
+
{children.map((child) => (
|
|
932
|
+
<div class="biab-flex-row__cell" key={child.id}>
|
|
933
|
+
<BiabFieldRow
|
|
934
|
+
field={child}
|
|
935
|
+
controllerSig={controllerSig}
|
|
936
|
+
state={state}
|
|
937
|
+
uploadFn={uploadFn}
|
|
938
|
+
acConfig={acConfig}
|
|
939
|
+
/>
|
|
940
|
+
</div>
|
|
941
|
+
))}
|
|
942
|
+
</div>
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return (
|
|
947
|
+
<BiabFieldRow
|
|
948
|
+
key={f.id}
|
|
949
|
+
field={f}
|
|
950
|
+
controllerSig={controllerSig}
|
|
951
|
+
state={state}
|
|
952
|
+
uploadFn={uploadFn}
|
|
953
|
+
acConfig={acConfig}
|
|
954
|
+
/>
|
|
955
|
+
);
|
|
956
|
+
})}
|
|
957
|
+
</div>
|
|
958
|
+
);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
962
|
+
// <BiabForm> — the drop-in.
|
|
963
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
964
|
+
|
|
965
|
+
export const BiabForm = component$<BiabFormProps>((props) => {
|
|
966
|
+
// The controller lives only on the client (it's a non-serializable closure).
|
|
967
|
+
// `noSerialize()` tells Qwik to drop it from the resumable state instead of
|
|
968
|
+
// trying (and failing) to serialize the closure; it is re-created in the
|
|
969
|
+
// client-only `useVisibleTask$` below.
|
|
970
|
+
const controllerSig = useSignal<NoSerialize<FormController> | undefined>(
|
|
971
|
+
undefined,
|
|
972
|
+
);
|
|
973
|
+
// Serializable mirror of the controller's snapshot — drives every render.
|
|
974
|
+
const state = useStore<SnapshotState>(
|
|
975
|
+
{
|
|
976
|
+
// Seed from the pre-fetched schema so SSR renders the fields immediately
|
|
977
|
+
// (no flicker). The controller re-adopts this same schema on the client.
|
|
978
|
+
snap: seedSnapshot(props.schema),
|
|
979
|
+
},
|
|
980
|
+
{ deep: true },
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
// eslint-disable-next-line qwik/no-use-visible-task
|
|
984
|
+
useVisibleTask$(({ cleanup }) => {
|
|
985
|
+
// Build the controller client-side. `submit$` / `uploadFile$` keep the
|
|
986
|
+
// bearer key server-side; the controller's own client is the fallback when
|
|
987
|
+
// a `client` was passed directly.
|
|
988
|
+
const submitOptions: FormSubmitOptions = {
|
|
989
|
+
...props.submitOptions,
|
|
990
|
+
...(props.source ? { source: props.source } : {}),
|
|
991
|
+
...(props.dryRun ? { dryRun: true } : {}),
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const controller = createFormController({
|
|
995
|
+
...(props.client ? { client: props.client } : {}),
|
|
996
|
+
...(props.schema ? { schema: props.schema } : {}),
|
|
997
|
+
...(props.slug ? { slug: props.slug } : {}),
|
|
998
|
+
submitOptions,
|
|
999
|
+
onSuccess: (result) => {
|
|
1000
|
+
void props.onSuccess$?.(result);
|
|
1001
|
+
},
|
|
1002
|
+
onError: (result) => {
|
|
1003
|
+
void props.onError$?.(result);
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
controllerSig.value = noSerialize(controller);
|
|
1007
|
+
|
|
1008
|
+
// Mirror the snapshot into the reactive store on every notify.
|
|
1009
|
+
const unsubscribe = controller.subscribe((snap) => {
|
|
1010
|
+
state.snap = snap;
|
|
1011
|
+
});
|
|
1012
|
+
// Load (adopts the pre-fetched schema synchronously, or fetches via client).
|
|
1013
|
+
void controller.load().then(() => {
|
|
1014
|
+
state.snap = controller.snapshot();
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
cleanup(() => {
|
|
1018
|
+
unsubscribe();
|
|
1019
|
+
controller.destroy();
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const rootClass = classOf("biab-form", props.class, props.className);
|
|
1024
|
+
|
|
1025
|
+
// HTML autofill config threaded to every field input. A plain serializable
|
|
1026
|
+
// object (Qwik-friendly): the global switch + per-field overrides. Mirrors the
|
|
1027
|
+
// React adapter; resolution lives in forms-core/autocomplete.ts.
|
|
1028
|
+
const acConfig: AutocompleteConfig = {
|
|
1029
|
+
...(props.autoComplete !== undefined ? { enabled: props.autoComplete } : {}),
|
|
1030
|
+
...(props.fieldAutoComplete ? { overrides: props.fieldAutoComplete } : {}),
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// ── Submit handler — routes through `submit$` (server) when present, else the
|
|
1034
|
+
// controller's own SDK client. Validation + re-keying happen in the core. ──
|
|
1035
|
+
const handleSubmit = $(async () => {
|
|
1036
|
+
const controller = controllerSig.value;
|
|
1037
|
+
if (!controller) return;
|
|
1038
|
+
|
|
1039
|
+
if (props.submit$) {
|
|
1040
|
+
// SSR path: validate + re-key in the core, delegate the round-trip to
|
|
1041
|
+
// the consumer's `server$`. `validate()` publishes inline errors; the
|
|
1042
|
+
// core's `toSubmissionPayload` re-keys id → output key.
|
|
1043
|
+
const missing = controller.validate();
|
|
1044
|
+
if (missing.length > 0) return;
|
|
1045
|
+
const snap = controller.snapshot();
|
|
1046
|
+
// Re-key for the wire via the SAME mapping the core uses on submit
|
|
1047
|
+
// (id → published output key; the server also accepts the legacy id).
|
|
1048
|
+
const payload = toSubmissionPayload(snap.fields, snap.values);
|
|
1049
|
+
void props.onSubmit$?.(payload);
|
|
1050
|
+
const mergedOptions: FormSubmitOptions = {
|
|
1051
|
+
...props.submitOptions,
|
|
1052
|
+
...(props.source ? { source: props.source } : {}),
|
|
1053
|
+
...(props.dryRun ? { dryRun: true } : {}),
|
|
1054
|
+
};
|
|
1055
|
+
const result = await props.submit$(payload, mergedOptions);
|
|
1056
|
+
// Feed the server result back through the store for inline rendering.
|
|
1057
|
+
state.snap = { ...controller.snapshot(), result, submitted: result.ok };
|
|
1058
|
+
if (result.ok) void props.onSuccess$?.(result);
|
|
1059
|
+
else void props.onError$?.(result);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Client path: the controller owns validation + the round-trip via its SDK.
|
|
1064
|
+
await controller.submit();
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const snap = state.snap;
|
|
1068
|
+
if (!snap || snap.status === "loading" || snap.status === "idle") {
|
|
1069
|
+
return (
|
|
1070
|
+
<div class={rootClass} data-biab-status="loading">
|
|
1071
|
+
<Slot name="loading" />
|
|
1072
|
+
</div>
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (snap.status === "error") {
|
|
1076
|
+
return (
|
|
1077
|
+
<div class={rootClass} data-biab-status="error">
|
|
1078
|
+
<Slot name="error" />
|
|
1079
|
+
<p class="biab-form__load-error">
|
|
1080
|
+
{snap.loadError ?? "Could not load the form."}
|
|
1081
|
+
</p>
|
|
1082
|
+
</div>
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const settings = snap.settings;
|
|
1087
|
+
const consecutive = settings.isConsecutive;
|
|
1088
|
+
const showSteps =
|
|
1089
|
+
consecutive &&
|
|
1090
|
+
settings.showProgress &&
|
|
1091
|
+
settings.progressStyle !== "bar" &&
|
|
1092
|
+
snap.stepCount > 1;
|
|
1093
|
+
const showBar =
|
|
1094
|
+
consecutive &&
|
|
1095
|
+
settings.showProgress &&
|
|
1096
|
+
settings.progressStyle === "bar" &&
|
|
1097
|
+
snap.stepCount > 1;
|
|
1098
|
+
|
|
1099
|
+
// Animation entrance class (per dashboard `entranceClass`). "scroll" is left
|
|
1100
|
+
// to the consumer's CSS via the `biab-form--anim-scroll` hook.
|
|
1101
|
+
const animClass =
|
|
1102
|
+
settings.animation === "none" ? "" : `biab-form--anim-${settings.animation}`;
|
|
1103
|
+
const dirClass = snap.forward ? "biab-form--forward" : "biab-form--back";
|
|
1104
|
+
|
|
1105
|
+
if (snap.submitted) {
|
|
1106
|
+
return (
|
|
1107
|
+
<div class={classOf(rootClass, "biab-form--submitted")} data-biab-status="submitted">
|
|
1108
|
+
<Slot name="success" />
|
|
1109
|
+
</div>
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return (
|
|
1114
|
+
<div class={rootClass} data-biab-status="ready" data-biab-animation={settings.animation}>
|
|
1115
|
+
{snap.orgIcon ? (
|
|
1116
|
+
<img class="biab-form__org-icon" src={snap.orgIcon} alt="" />
|
|
1117
|
+
) : null}
|
|
1118
|
+
|
|
1119
|
+
{snap.title ? <h2 class="biab-form__title">{snap.title}</h2> : null}
|
|
1120
|
+
{snap.description ? (
|
|
1121
|
+
<p class="biab-form__description">{snap.description}</p>
|
|
1122
|
+
) : null}
|
|
1123
|
+
|
|
1124
|
+
{showSteps ? (
|
|
1125
|
+
<div class="biab-progress biab-progress--steps">
|
|
1126
|
+
<div class="biab-progress__heading">Form Progress</div>
|
|
1127
|
+
<ol class="biab-progress__steps">
|
|
1128
|
+
{snap.steps.map((s, i) => {
|
|
1129
|
+
const head = s[0];
|
|
1130
|
+
const title = head?.stepTitle || head?.label || `Step ${i + 1}`;
|
|
1131
|
+
const done = i < snap.step;
|
|
1132
|
+
const current = i === snap.step;
|
|
1133
|
+
return (
|
|
1134
|
+
<li
|
|
1135
|
+
key={`biab-step-${i}`}
|
|
1136
|
+
class={classOf(
|
|
1137
|
+
"biab-progress__step",
|
|
1138
|
+
current && "biab-progress__step--current",
|
|
1139
|
+
done && "biab-progress__step--done",
|
|
1140
|
+
)}
|
|
1141
|
+
>
|
|
1142
|
+
<span class="biab-progress__marker">{done ? "✓" : i + 1}</span>
|
|
1143
|
+
<span class="biab-progress__title">{title}</span>
|
|
1144
|
+
</li>
|
|
1145
|
+
);
|
|
1146
|
+
})}
|
|
1147
|
+
</ol>
|
|
1148
|
+
</div>
|
|
1149
|
+
) : null}
|
|
1150
|
+
|
|
1151
|
+
{showBar ? (
|
|
1152
|
+
<div class="biab-progress biab-progress--bar">
|
|
1153
|
+
<span class="biab-progress__count">
|
|
1154
|
+
{snap.step + 1} / {snap.stepCount}
|
|
1155
|
+
</span>
|
|
1156
|
+
<div class="biab-progress__track">
|
|
1157
|
+
<div
|
|
1158
|
+
class="biab-progress__fill"
|
|
1159
|
+
style={{ width: `${snap.progressPercent}%` }}
|
|
1160
|
+
/>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
) : null}
|
|
1164
|
+
|
|
1165
|
+
{snap.errorList.length > 0 ? (
|
|
1166
|
+
<div class="biab-form__errors" role="alert">
|
|
1167
|
+
<div class="biab-form__errors-heading">
|
|
1168
|
+
Please complete the highlighted required field
|
|
1169
|
+
{snap.errorList.length === 1 ? "" : "s"}.
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="biab-form__errors-list">
|
|
1172
|
+
{snap.errorList.map((e) => e.label).join(", ")}
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
) : null}
|
|
1176
|
+
|
|
1177
|
+
<div class={classOf("biab-form__body", animClass, dirClass)} key={`biab-body-${snap.step}`}>
|
|
1178
|
+
<BiabRenderTree
|
|
1179
|
+
fields={snap.visibleFields}
|
|
1180
|
+
controllerSig={controllerSig}
|
|
1181
|
+
state={state}
|
|
1182
|
+
uploadFn={props.uploadFile$}
|
|
1183
|
+
acConfig={acConfig}
|
|
1184
|
+
flat={consecutive}
|
|
1185
|
+
/>
|
|
1186
|
+
</div>
|
|
1187
|
+
|
|
1188
|
+
{consecutive && snap.stepCount > 1 ? (
|
|
1189
|
+
<div class="biab-nav">
|
|
1190
|
+
<button
|
|
1191
|
+
type="button"
|
|
1192
|
+
class="biab-btn biab-btn--back"
|
|
1193
|
+
disabled={snap.isFirstStep}
|
|
1194
|
+
onClick$={() => controllerSig.value?.back()}
|
|
1195
|
+
>
|
|
1196
|
+
Back
|
|
1197
|
+
</button>
|
|
1198
|
+
{snap.isLastStep ? (
|
|
1199
|
+
<button
|
|
1200
|
+
type="button"
|
|
1201
|
+
class="biab-btn biab-btn--submit"
|
|
1202
|
+
disabled={snap.submitting || snap.uploading}
|
|
1203
|
+
onClick$={handleSubmit}
|
|
1204
|
+
>
|
|
1205
|
+
{snap.submitting ? "Sending…" : props.submitLabel || "Submit"}
|
|
1206
|
+
</button>
|
|
1207
|
+
) : (
|
|
1208
|
+
<button
|
|
1209
|
+
type="button"
|
|
1210
|
+
class="biab-btn biab-btn--next"
|
|
1211
|
+
disabled={!snap.canAdvance}
|
|
1212
|
+
onClick$={() => controllerSig.value?.next()}
|
|
1213
|
+
>
|
|
1214
|
+
Next
|
|
1215
|
+
</button>
|
|
1216
|
+
)}
|
|
1217
|
+
</div>
|
|
1218
|
+
) : (
|
|
1219
|
+
<div class="biab-nav">
|
|
1220
|
+
<button
|
|
1221
|
+
type="button"
|
|
1222
|
+
class="biab-btn biab-btn--submit"
|
|
1223
|
+
disabled={snap.submitting || snap.uploading}
|
|
1224
|
+
onClick$={handleSubmit}
|
|
1225
|
+
>
|
|
1226
|
+
{snap.submitting ? "Sending…" : props.submitLabel || "Submit"}
|
|
1227
|
+
</button>
|
|
1228
|
+
</div>
|
|
1229
|
+
)}
|
|
1230
|
+
</div>
|
|
1231
|
+
);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
export default BiabForm;
|