@wix/agent-skills 1.20.0 → 1.20.1
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/package.json +1 -1
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -8,16 +8,24 @@ every form has a different field set and no "contact form" component could ship
|
|
|
8
8
|
|
|
9
9
|
## The file map (deployed into `src/`)
|
|
10
10
|
|
|
11
|
-
**
|
|
12
|
-
|
|
11
|
+
**On Astro and React the shipped files are tested and work as they are** — this table and the
|
|
12
|
+
contracts below are everything you need to use them, so don't spend the run reading their source;
|
|
13
|
+
wire them and build your surfaces. Reading them is the right move when something is off (a runtime
|
|
14
|
+
error, a field kind this playbook doesn't cover) or when the brief wants a behaviour they don't
|
|
15
|
+
offer — then read the file that owns it and change or extend it. On `lib`, `static`, and a port
|
|
16
|
+
the hook doesn't deploy; the store behind it does, and the wiring sections below say what to
|
|
17
|
+
read first. Files you edit: `SiteLayout.astro` and `styles/global.css`. Files you **create**
|
|
18
|
+
(skeletons below): the form page and its island, plus your home page.
|
|
13
19
|
|
|
14
20
|
| file | what it is |
|
|
15
21
|
|---|---|
|
|
16
22
|
| `wix/config.ts` · `wix/sdk.ts` · `wix/media.ts` · `wix/money.ts` | shared auth seam + helpers (deploy configures; nothing to set) |
|
|
17
|
-
| `wix/forms/types.ts` | the DTOs (`FormDto`, `FormFieldDto`, `FormValues`, `FormErrors`) — contracts below |
|
|
18
|
-
| `wix/forms/forms.ts` | `getForm`, `listForms` — the raw schema
|
|
19
|
-
| `wix/forms/submissions.ts` | `uploadFiles`, `
|
|
20
|
-
| `
|
|
23
|
+
| `wix/forms/types.ts` | the DTOs (`FormDto`, `FormFieldDto`, `FormValues`, `FormErrors`, `SubmissionDto`) — contracts below |
|
|
24
|
+
| `wix/forms/forms.ts` | `getForm`, `listForms` — the transport; the flattening of the raw schema into `FormFieldDto[]` is in `forms-core.ts` beside it (shared with the REST layer) |
|
|
25
|
+
| `wix/forms/submissions.ts` | `uploadFiles`, `createSubmission` — the transport; `toSubmissionValues`, `submissionErrors`, `normalizePhone`, `SUBMITTED_OK` and the upload PUT are in `submissions-core.ts` beside it (shared with the REST layer) |
|
|
26
|
+
| `wix/forms/form-store.ts` | the form state machine, framework-free (`createFormStore({ formId, initialForm? })` — `getState`/`subscribe` + `setValue`/`setValues`/`validate`/`submit`, one instance per form on the page); `validateValue`, `FORM_ERROR`; the hook below binds it to React, every other stack uses it directly |
|
|
27
|
+
| `hooks/forms/useWixForm.ts` | React binding of `form-store.ts` — the whole form as state plus `bind` — contract below |
|
|
28
|
+
| `rest/forms.ts` · `rest/submissions.ts` (skill-side) | the REST twin of the two data files: same exports, same DTOs, over `fetch` with the visitor token — what `--stack static` deploys, and the spec a port reads |
|
|
21
29
|
| `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
|
|
22
30
|
|
|
23
31
|
There are **no shipped components and no shipped pages** — every control is yours.
|
|
@@ -36,7 +44,25 @@ Read the seed plan first: its forms and their fields are what the page renders.
|
|
|
36
44
|
|
|
37
45
|
Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
|
|
38
46
|
|
|
39
|
-
###
|
|
47
|
+
### What a complete form page shows (recommended defaults)
|
|
48
|
+
|
|
49
|
+
Defaults for a brief that says nothing about them; the prompt wins when it asks otherwise.
|
|
50
|
+
|
|
51
|
+
- Every field's `label` visible above or beside its control; `required` marked the same way on
|
|
52
|
+
every field; `description` as help text under the control; `placeholder` only where the owner
|
|
53
|
+
wrote one.
|
|
54
|
+
- An error under its own control (`errors[target]`, `id="err-<target>"` so `aria-describedby`
|
|
55
|
+
resolves), the form-level `errors[FORM_ERROR]` above the button; nothing turns red before the
|
|
56
|
+
visitor touched the field or pressed submit.
|
|
57
|
+
- The submit button reads `submitText` when set, your wording else; disabled while `loading`.
|
|
58
|
+
- `form === null` renders a skeleton of the section, not an empty `<form>`; `errors[FORM_ERROR]`
|
|
59
|
+
with no form renders the message and nothing else.
|
|
60
|
+
- The thank-you replaces the form in place (same section, same width) — the visitor stays where
|
|
61
|
+
they were.
|
|
62
|
+
- Copy is the owner's: labels and choices from the schema, no invented privacy promises or
|
|
63
|
+
response-time claims.
|
|
64
|
+
|
|
65
|
+
### The contracts your components consume (tested and work as they are; read the source when something is off or the brief wants more)
|
|
40
66
|
|
|
41
67
|
```ts
|
|
42
68
|
// FormDto — one form, ready to render
|
|
@@ -69,6 +95,13 @@ Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`,
|
|
|
69
95
|
// validate, // validate(target) | validate("addr/city") | validate() for the whole form
|
|
70
96
|
// errors, // target (or target/sub) → visitor-facing message; errors[FORM_ERROR] is form-level
|
|
71
97
|
// loading } // loading the schema, or submitting
|
|
98
|
+
|
|
99
|
+
// createFormStore({ formId, initialForm? }) — the same machine without React (lib, static, a port):
|
|
100
|
+
// getState() → { form, values, errors, loading }; subscribe(fn); start() once mounted (loads the
|
|
101
|
+
// schema unless initialForm was given); stop() on unmount; setValue(target, value);
|
|
102
|
+
// setValues(next | fn); validate(target?) → boolean; submit(event?) → Promise<boolean>.
|
|
103
|
+
// submit(event) reads event.currentTarget to focus the first invalid control — hand it the
|
|
104
|
+
// native submit event. FORM_ERROR and validateValue are exported next to it.
|
|
72
105
|
```
|
|
73
106
|
|
|
74
107
|
`bind` covers input / textarea / select. A **checkbox or radio group** carries `checked`
|
|
@@ -88,26 +121,169 @@ keeping the same `name`, `onBlur: () => validate(target)` and `aria-describedby`
|
|
|
88
121
|
| `checkbox` | a single `<input type="checkbox">`; its value is a **boolean** |
|
|
89
122
|
| `checkboxGroup` `tags` | one checkbox per choice; the value is an **array** of chosen values |
|
|
90
123
|
| `address` | one control per `addressParts` entry; the value is an **object** keyed by `sub`, and its error keys are `target/sub` |
|
|
91
|
-
| `file` `signature` | `<input type="file">` (uncontrolled) — put the `File` objects in `values[target]`; the
|
|
124
|
+
| `file` `signature` | `<input type="file">` (uncontrolled) — put the `File` objects in `values[target]`; the store uploads them on submit |
|
|
92
125
|
| `payment` `appointment` `unknown` | out of scope for a plain form — a payment field needs the payment flow, an appointment field needs the `bookings` vertical. Render a disabled note rather than an input that submits the wrong thing |
|
|
93
126
|
|
|
94
|
-
###
|
|
127
|
+
### The page and island you create — skeletons
|
|
95
128
|
|
|
96
|
-
|
|
129
|
+
The class names here are the Astro/React spelling of rules that hold on every stack; on a
|
|
130
|
+
stack where the hook doesn't deploy, keep the rule and write it in your own CSS. Hooks first,
|
|
131
|
+
branches after (an early return above a hook changes hook order between renders and React
|
|
132
|
+
throws). The island renders on the server too (`client:load` SSRs) — render every state
|
|
133
|
+
totally; nothing in a render path may throw.
|
|
97
134
|
|
|
98
135
|
```astro
|
|
99
136
|
---
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
// src/pages/contact.astro — YOU create it. The schema is fetched SERVER-SIDE so the first paint
|
|
138
|
+
// has the fields, and handed to the island as a serialized DTO prop.
|
|
139
|
+
import SiteLayout from "../layouts/SiteLayout.astro";
|
|
140
|
+
import ContactForm from "../components/forms/ContactForm";
|
|
141
|
+
import { getForm } from "../wix/forms/forms";
|
|
142
|
+
import type { FormDto } from "../wix/forms/types";
|
|
143
|
+
|
|
144
|
+
const FORM_ID = "…"; // from the seed result (seed-result.json → forms[].formId)
|
|
145
|
+
let form: FormDto | null = null;
|
|
146
|
+
try {
|
|
147
|
+
form = await getForm(FORM_ID);
|
|
148
|
+
} catch {
|
|
149
|
+
// Guarded: an unhandled SSR throw truncates the response mid-stream; the island loads the
|
|
150
|
+
// schema itself and shows the real error if it fails again.
|
|
151
|
+
}
|
|
102
152
|
---
|
|
103
|
-
<
|
|
153
|
+
<SiteLayout title="Contact">
|
|
154
|
+
<!-- your heading / intro, then: -->
|
|
155
|
+
<ContactForm client:load formId={FORM_ID} initialForm={form ?? undefined} />
|
|
156
|
+
</SiteLayout>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// src/components/forms/ContactForm.tsx — YOU build it; contact.astro mounts it.
|
|
161
|
+
import { useState } from "react";
|
|
162
|
+
import { useWixForm, FORM_ERROR } from "../../hooks/forms/useWixForm";
|
|
163
|
+
import type { FormDto } from "../../wix/forms/types";
|
|
164
|
+
|
|
165
|
+
export default function ContactForm({ formId, initialForm }: { formId: string; initialForm?: FormDto }) {
|
|
166
|
+
const f = useWixForm(formId, { initialForm }); // no second fetch when initialForm is passed
|
|
167
|
+
const [sent, setSent] = useState(false);
|
|
168
|
+
// …you implement the render:
|
|
169
|
+
// • sent → your thank-you, in place of the form (the resolved submit() IS the confirmation)
|
|
170
|
+
// • f.errors[FORM_ERROR] && !f.form → the message alone (a form that can't load is a setup problem)
|
|
171
|
+
// • !f.form → a skeleton of the section
|
|
172
|
+
// • else <form noValidate onSubmit={async (e) => { if (await f.submit(e)) setSent(true); }}>
|
|
173
|
+
// mapping f.form.fields to controls per the table above — text-ish: <input type=… {...f.bind(field.target)} />;
|
|
174
|
+
// groups/checkbox/file by hand with the same name / onBlur / aria contract; the error for a
|
|
175
|
+
// control in <p id={`err-${field.target}`}>{f.errors[field.target]}</p>; address subfield
|
|
176
|
+
// errors under f.errors[`${field.target}/${sub}`]; f.errors[FORM_ERROR] above the button;
|
|
177
|
+
// the button labelled f.form.submitText || your wording, disabled while f.loading.
|
|
178
|
+
// noValidate: the store validates in your wording and focuses the first invalid control —
|
|
179
|
+
// the browser's bubbles would race it.
|
|
180
|
+
}
|
|
104
181
|
```
|
|
105
182
|
|
|
106
|
-
The
|
|
183
|
+
### The reference files for stacks where the components don't deploy
|
|
184
|
+
|
|
185
|
+
On `lib`, `static`, and a port, nothing under `hooks/` arrives — and this vertical ships nothing
|
|
186
|
+
under `components/` on any stack. The state machine behind the hook does arrive —
|
|
187
|
+
`wix/forms/form-store.ts` — so you never rewrite it: `createFormStore` per form, `subscribe`,
|
|
188
|
+
render from `getState()`, call its actions. Its `FormState` interface is the render contract;
|
|
189
|
+
read that. What you write is the rendering, and one shipped file is worth reading first:
|
|
190
|
+
|
|
191
|
+
1. `hooks/forms/useWixForm.ts` — `bind`: the exact props a text-ish control gets (`name`,
|
|
192
|
+
`value`, change → `setValue`, blur → `validate(target)`, `aria-describedby="err-<target>"`,
|
|
193
|
+
`aria-invalid`), and how a change of `formId` gets a fresh store. Your framework's controls
|
|
194
|
+
follow the same contract.
|
|
195
|
+
|
|
196
|
+
The control table above is the spec for everything else. Under `references/forms/app/`.
|
|
197
|
+
|
|
198
|
+
### Wiring — Astro (default)
|
|
199
|
+
|
|
200
|
+
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass).
|
|
201
|
+
2. Create the form page and its island per the skeletons above — frontmatter machinery exact,
|
|
202
|
+
presentation yours. Fetch in frontmatter so the first paint has the schema; the island calls
|
|
203
|
+
`useWixForm(formId, { initialForm })` — no second fetch, no loading flash.
|
|
204
|
+
3. Write `pages/index.astro` (home) on `SiteLayout`.
|
|
205
|
+
|
|
206
|
+
### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
|
|
207
|
+
|
|
208
|
+
Read the reference file listed above before writing the surface.
|
|
209
|
+
|
|
210
|
+
`deploy.mjs forms --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts` (the
|
|
211
|
+
visitor client, configured with the public client id), `media.ts`, `money.ts`, and `wix/forms/` —
|
|
212
|
+
`forms.ts`, `submissions.ts`, `types.ts`, the two `*-core.ts` rule files, and `form-store.ts`.
|
|
213
|
+
None of it is React. The hook doesn't ship on this stack; the store replaces it, and you write
|
|
214
|
+
the controls in your framework to the contracts on this page:
|
|
215
|
+
|
|
216
|
+
- bind the store with your framework's external-store primitive (Vue: `shallowRef` updated in
|
|
217
|
+
`subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
|
|
218
|
+
Solid: a signal set in `subscribe`). `createFormStore({ formId })` per form (`start()` when
|
|
219
|
+
mounted, `stop()` when unmounted). State in, actions out — exactly the hook's contract above;
|
|
220
|
+
- a control's change handler calls `setValue(target, value)` (a checkbox group: the array of
|
|
221
|
+
checked values; a file input: `[...input.files]`), blur calls `validate(target)`, the
|
|
222
|
+
`<form>`'s submit handler calls `submit(event)` and shows the thank-you on `true`.
|
|
223
|
+
|
|
224
|
+
Route `/contact` (or wherever the form lives); dev server on 4321; a static build goes through
|
|
225
|
+
`npx @wix/cli@latest release` with `site.outputDirectory` pointing at the build folder, an SSR
|
|
226
|
+
build is hosted by you.
|
|
227
|
+
|
|
228
|
+
### Wiring — static site (`--stack static`, no bundler)
|
|
229
|
+
|
|
230
|
+
Read the reference file listed above before writing the surface.
|
|
231
|
+
|
|
232
|
+
`deploy.mjs forms --stack static --out site` put the REST layer in `site/js/wix/` (browser ESM,
|
|
233
|
+
the `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` —
|
|
234
|
+
pages, styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project
|
|
235
|
+
root (config, plan, seed output) is never the upload. Same function names and DTOs as the table
|
|
236
|
+
above, so the contracts on this page hold unchanged: `getForm`, `listForms` from
|
|
237
|
+
`./js/wix/forms.js`; `uploadFiles`, `createSubmission`, `toSubmissionValues`, `submissionErrors`
|
|
238
|
+
from `./js/wix/submissions.js`. The state machine ships too: `createFormStore` from
|
|
239
|
+
`./js/wix/form-store.js` (`start()` once the page is up; `setValue`, `validate`, `submit`;
|
|
240
|
+
`FORM_ERROR` beside it). No hook, no components — you write the rendering in plain JS: build the
|
|
241
|
+
controls ONCE when `getState().form` arrives (one element per `form.fields` entry, `name` =
|
|
242
|
+
`target`, `input` → `setValue`, `blur` → `validate(target)`), and on later notifications update
|
|
243
|
+
only the error text, `aria-invalid`, and the button's disabled state — rebuilding the inputs on
|
|
244
|
+
every keystroke drops focus. `<form novalidate>` with `onsubmit = (e) => store.submit(e).then(ok
|
|
245
|
+
=> ok && showThankYou())`; `submit` reads the native event's `currentTarget` to focus the first
|
|
246
|
+
invalid control. Wix static hosting serves files, not directories: the page is `contact.html`,
|
|
247
|
+
linked as such. The visitor token persists in `localStorage` on its own; never mint per page.
|
|
248
|
+
The page's title and meta description are yours (a form is not an entity page; there is no
|
|
249
|
+
`seoData`). `npx @wix/cli@latest release` uploads `site/`.
|
|
250
|
+
|
|
251
|
+
### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
|
|
252
|
+
|
|
253
|
+
Read the reference file listed above before writing the surface.
|
|
254
|
+
|
|
255
|
+
Run `deploy.mjs forms --stack static` in the project folder anyway: `js/wix/` is both the
|
|
256
|
+
browser-side code and the readable spec. Then split by where the call runs. **The schema on the
|
|
257
|
+
server:** port `js/wix/forms.ts` and `forms-core.ts` to your language — `getForm` is one GET with
|
|
258
|
+
the visitor token, `toForm` is the flattening — and render the fields in your template (labels,
|
|
259
|
+
required marks, choices, the same `name` = `target`), so the form is in the HTML; one anonymous
|
|
260
|
+
visitor token per process for this public read (mint and refresh per `client.ts`). **Values,
|
|
261
|
+
validation and the submit in the browser:** load `./js/wix/form-store.js` in the template and
|
|
262
|
+
drive the rendered controls through `createFormStore({ formId })` exactly as the static wiring
|
|
263
|
+
above — the browser owns the visitor's token, so the server never handles per-visitor tokens or
|
|
264
|
+
submissions. If the submit must run server-side anyway (no JS), port `submissions.ts` +
|
|
265
|
+
`submissions-core.ts` and `client.ts`'s header applies: one token set per visitor in the visitor's
|
|
266
|
+
session, never one process-wide token. Add your public https origin to the OAuth app's allowed
|
|
267
|
+
domains.
|
|
268
|
+
|
|
269
|
+
**Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for
|
|
270
|
+
the schema read, run at build time with one anonymous token; the generator emits the form page
|
|
271
|
+
with the fields in the HTML. Run `deploy.mjs forms --stack static --out <build dir>` so `js/wix/`
|
|
272
|
+
is inside the output the page imports from, point `site.outputDirectory` at that folder, `wix
|
|
273
|
+
release`. Pages sit at different depths: give the templates one base path to `js/wix/` (a
|
|
274
|
+
template variable, or root-relative `/js/wix/…`), never a relative `./js/wix/` — it breaks one
|
|
275
|
+
level down. The rendered fields are the first paint; values, validation and the submit still run
|
|
276
|
+
client-side through `createFormStore()` from `./js/wix/form-store.js`, exactly as on a static
|
|
277
|
+
site. Close with the rebuild + release command and one line for the owner: a field renamed or
|
|
278
|
+
added in the dashboard reaches the rendered HTML when that command runs; the submit is live
|
|
279
|
+
regardless.
|
|
107
280
|
|
|
108
281
|
### Wiring — React SPA (Vite etc.)
|
|
109
282
|
|
|
110
|
-
`
|
|
283
|
+
Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite config
|
|
284
|
+
plugins — deploy already added the dep). `useWixForm(FORM_ID)` with no `initialForm`; render a
|
|
285
|
+
skeleton while `form` is null. Deploy wrote the public client id into `wix/config.ts`; nothing
|
|
286
|
+
else to configure.
|
|
111
287
|
|
|
112
288
|
## Hard rules
|
|
113
289
|
|
|
@@ -117,6 +293,10 @@ The island calls `useWixForm(form.id, { initialForm })` — no second fetch, no
|
|
|
117
293
|
- **Never build a form without reading the schema.** A hand-built `<form>` posting to Wix Data
|
|
118
294
|
or an email service silently drops real enquiries and loses the dashboard form builder,
|
|
119
295
|
spam protection, notifications and CRM contact mapping.
|
|
296
|
+
- **Values, validation and the submit go through `useWixForm` / the store** — never call the
|
|
297
|
+
submission API by hand, never re-derive a request shape, never validate by a field's name.
|
|
298
|
+
Extend by adding a function in `wix/forms/` for what they don't cover (API contracts: the
|
|
299
|
+
`wix-docs` skill).
|
|
120
300
|
- **The visitor token is enough.** Both the schema read and the submission return 200 on an
|
|
121
301
|
anonymous visitor, even though the spec lists them under owner scopes. Never add a backend,
|
|
122
302
|
a connector token, or `auth.elevate` to make a form work.
|
|
@@ -127,24 +307,32 @@ The island calls `useWixForm(form.id, { initialForm })` — no second fetch, no
|
|
|
127
307
|
owner gets duplicates for an entry they already have.
|
|
128
308
|
- **Never mock, fail loudly.** A form that cannot load is a setup problem — surface it; never
|
|
129
309
|
fall back to a hand-built form.
|
|
310
|
+
- Don't wrap shipped calls in your own API routes — they run client-side by design.
|
|
311
|
+
- Where the shipped code deploys (Astro, React): theme via the `@theme` tokens, and your markup
|
|
312
|
+
uses Tailwind utilities on the same tokens — one design system. Where it doesn't (`lib`,
|
|
313
|
+
`static`, a port): style with whatever your stack does well, on one token set of your own.
|
|
314
|
+
- **Call every hook before any conditional return.** An island that returns early for the
|
|
315
|
+
skeleton or the thank-you above `useWixForm`/`useState` changes hook order between renders and
|
|
316
|
+
React throws. Hooks first, branches after.
|
|
130
317
|
|
|
131
318
|
## Point the user to their dashboard
|
|
132
319
|
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
|
|
320
|
+
Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON prints it as
|
|
321
|
+
`dashboardUrl`); `{formId}` is the id the seed result returned.
|
|
322
|
+
|
|
323
|
+
| page | `https://manage.wix.com/dashboard/{siteId}/` + |
|
|
324
|
+
|---|---|
|
|
325
|
+
| Forms list | `wix-forms` |
|
|
326
|
+
| Edit the form (fields, rules, submit settings, notifications) | `wix-forms/form/{formId}` |
|
|
327
|
+
| Its submissions | `wix-forms/form/{formId}/submissions` |
|
|
328
|
+
|
|
329
|
+
Say so when you hand the site over — the whole value of this vertical is that their edits land on the
|
|
330
|
+
site with no code change. Submissions are per form; there is no site-wide submissions page.
|
|
136
331
|
|
|
137
332
|
## Seeding
|
|
138
333
|
|
|
139
334
|
`seed/SEED.md` is the contract: a plain-data plan in, created forms out. Read it when drafting
|
|
140
335
|
the plan; the seed writes the form ids your pages import.
|
|
141
336
|
|
|
142
|
-
|
|
337
|
+
**Build the UI only after the seed has run** — the form id comes from it, and the field set
|
|
143
338
|
you are rendering is the one it created.
|
|
144
|
-
|
|
145
|
-
## Verify (before declaring done)
|
|
146
|
-
|
|
147
|
-
- The rendered controls match the seeded fields — same labels, same order, same required marks.
|
|
148
|
-
- A submit with an empty required field shows YOUR inline message, and focus moves to it.
|
|
149
|
-
- A valid submit resolves and shows the thank-you; the entry appears in the owner's dashboard.
|
|
150
|
-
- Renaming a field in the dashboard changes the page on reload, with no code change.
|
|
@@ -1,145 +1,20 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// React binding of the form store (wix/forms/form-store.ts) — schema in, validated submission
|
|
2
|
+
// out, minus the markup; the state machine lives there, framework-free. This hook subscribes to
|
|
3
|
+
// one store per mounted form and exposes its state and actions under one name, plus `bind` (the
|
|
4
|
+
// props a text-ish control spreads). Astro islands and React SPAs use this; a static page, Vue,
|
|
5
|
+
// or Svelte uses the store directly.
|
|
6
|
+
//
|
|
7
|
+
// This vertical ships no components: a form is schema-driven (the owner picks the fields), so
|
|
8
|
+
// every form has a different field set and no "contact form" component could ship for it. You
|
|
9
|
+
// render `form.fields`; see references/forms/INSTRUCTIONS.md.
|
|
5
10
|
//
|
|
6
11
|
// SSR-friendly: pass a server-fetched FormDto as `initialForm` (Astro frontmatter) and no
|
|
7
12
|
// client fetch happens; a SPA passes nothing and the hook loads it.
|
|
8
|
-
import { useCallback, useEffect,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
createSubmission,
|
|
12
|
-
normalizePhone,
|
|
13
|
-
submissionErrors,
|
|
14
|
-
toSubmissionValues,
|
|
15
|
-
uploadFiles,
|
|
16
|
-
} from "../../wix/forms/submissions";
|
|
17
|
-
import type { FormDto, FormErrors, FormFieldDto, FormValues } from "../../wix/forms/types";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The key in `errors` for a message that belongs to the FORM rather than one field — a schema
|
|
21
|
-
* that failed to load, or a rejection with no per-field violations. `@` cannot appear in a
|
|
22
|
-
* form `target`, so this never collides with a field's own error.
|
|
23
|
-
*/
|
|
24
|
-
export const FORM_ERROR = "@form";
|
|
25
|
-
|
|
26
|
-
/** The empty form: every field at a value of the shape its control binds to. */
|
|
27
|
-
function defaultValues(fields: FormFieldDto[]): FormValues {
|
|
28
|
-
const values: FormValues = {};
|
|
29
|
-
for (const f of fields) values[f.target] = Array.isArray(f.defaultValue) ? [...f.defaultValue] : f.defaultValue;
|
|
30
|
-
return values;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check one field's value against its own schema. Exported because it is a plain function —
|
|
35
|
-
* usable outside React, and the place to look when a message needs rewording.
|
|
36
|
-
*
|
|
37
|
-
* Every rule comes from the schema, never from a field's NAME. (The classic mistake is keying
|
|
38
|
-
* the email check on `target === "email"`; deriving it from `format` means an owner-added
|
|
39
|
-
* PHONE/URL/length rule is honored with no code change.)
|
|
40
|
-
*
|
|
41
|
-
* ⚠️ A client check LAXER than the server's is worse than none — the visitor then learns about
|
|
42
|
-
* the problem only after a round trip, in the server's wording rather than yours.
|
|
43
|
-
*/
|
|
44
|
-
export function validateValue(field: FormFieldDto, value: unknown): string {
|
|
45
|
-
const v = String(value ?? "").trim();
|
|
46
|
-
const rules = field.validation;
|
|
47
|
-
|
|
48
|
-
if (field.required && !v) return `${field.label} is required.`;
|
|
49
|
-
if (!v) return ""; // optional and empty → fine
|
|
50
|
-
|
|
51
|
-
if (field.control === "number" || field.control === "rating") {
|
|
52
|
-
// The control hands back a string, so parse before comparing: "9" > 10 is false but
|
|
53
|
-
// "9" > "10" is true.
|
|
54
|
-
const n = Number(v);
|
|
55
|
-
if (!Number.isFinite(n)) return `${field.label} must be a number.`;
|
|
56
|
-
if (rules.minimum != null && n < rules.minimum) return `${field.label} must be ${rules.minimum} or more.`;
|
|
57
|
-
if (rules.maximum != null && n > rules.maximum) return `${field.label} must be ${rules.maximum} or less.`;
|
|
58
|
-
return "";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (rules.minLength && v.length < rules.minLength)
|
|
62
|
-
return `${field.label} must be at least ${rules.minLength} characters.`;
|
|
63
|
-
if (rules.maxLength && v.length > rules.maxLength)
|
|
64
|
-
return `${field.label} must be at most ${rules.maxLength} characters.`;
|
|
65
|
-
if (rules.format === "EMAIL" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))
|
|
66
|
-
return "Please enter a valid email address.";
|
|
67
|
-
if (rules.format === "URL" && !/^https?:\/\/.+/.test(v)) return "Please enter a valid URL.";
|
|
68
|
-
// PHONE is E.164 server-side: leading +, country code, digits. Strip formatting first —
|
|
69
|
-
// visitors add spaces, dashes and parens, and rejecting those is a UX bug, not validation.
|
|
70
|
-
if (rules.format === "PHONE" && !/^\+[1-9]\d{6,14}$/.test(normalizePhone(v)))
|
|
71
|
-
return "Use international format, starting with + and the country code.";
|
|
72
|
-
if (rules.pattern && !new RegExp(rules.pattern).test(v))
|
|
73
|
-
return `${field.label} is not in the expected format.`;
|
|
74
|
-
return "";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* One field's error entries, keyed the way the controls are named. A plain field yields at most
|
|
79
|
-
* one (`target`); an ADDRESS yields one per failing subfield (`target/sub`).
|
|
80
|
-
*
|
|
81
|
-
* An address subfield gets the `required` check only — `country` and `subdivision` are
|
|
82
|
-
* country-dependent enums the schema does not enumerate, so their content is the server's call.
|
|
83
|
-
*/
|
|
84
|
-
function errorsForField(field: FormFieldDto, values: FormValues): FormErrors {
|
|
85
|
-
const errors: FormErrors = {};
|
|
86
|
-
|
|
87
|
-
if (field.control === "address") {
|
|
88
|
-
const parts = (values[field.target] ?? {}) as Record<string, unknown>;
|
|
89
|
-
for (const { sub, label, required } of field.addressParts) {
|
|
90
|
-
if (required && !String(parts[sub] ?? "").trim()) errors[`${field.target}/${sub}`] = `${label} is required.`;
|
|
91
|
-
}
|
|
92
|
-
return errors;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (field.control === "file" || field.control === "signature") {
|
|
96
|
-
// Files are File objects, which no string rule can judge — check the count instead.
|
|
97
|
-
const picked = ([] as unknown[]).concat(values[field.target] ?? []).filter(Boolean);
|
|
98
|
-
const limit = field.validation.fileLimit;
|
|
99
|
-
if (field.required && !picked.length) errors[field.target] = `${field.label} is required.`;
|
|
100
|
-
else if (limit && picked.length > limit)
|
|
101
|
-
errors[field.target] = `Attach at most ${limit} file${limit === 1 ? "" : "s"}.`;
|
|
102
|
-
return errors;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (field.inputType === "ARRAY") {
|
|
106
|
-
const picked = (Array.isArray(values[field.target]) ? (values[field.target] as unknown[]) : []).filter(Boolean);
|
|
107
|
-
const { minItems, maxItems } = field.validation;
|
|
108
|
-
if (field.required && !picked.length) errors[field.target] = `${field.label} is required.`;
|
|
109
|
-
else if (minItems && picked.length < minItems) errors[field.target] = `Choose at least ${minItems}.`;
|
|
110
|
-
else if (maxItems && picked.length > maxItems) errors[field.target] = `Choose at most ${maxItems}.`;
|
|
111
|
-
return errors;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (field.control === "checkbox") {
|
|
115
|
-
if (field.required && values[field.target] !== true) errors[field.target] = `${field.label} is required.`;
|
|
116
|
-
return errors;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const message = validateValue(field, values[field.target]);
|
|
120
|
-
if (message) errors[field.target] = message;
|
|
121
|
-
return errors;
|
|
122
|
-
}
|
|
13
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
14
|
+
import { createFormStore, type FormStore, type FormSubmitEvent } from "../../wix/forms/form-store";
|
|
15
|
+
import type { FormDto, FormErrors, FormValues } from "../../wix/forms/types";
|
|
123
16
|
|
|
124
|
-
|
|
125
|
-
const errors: FormErrors = {};
|
|
126
|
-
for (const field of fields) Object.assign(errors, errorsForField(field, values));
|
|
127
|
-
return errors;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Move focus to a control by input name. ⚠️ `namedItem` returns a RadioNodeList for a radio or
|
|
132
|
-
* checkbox group and an element for everything else — a guard checking only for an element
|
|
133
|
-
* silently skips every choice group.
|
|
134
|
-
*/
|
|
135
|
-
function focusControl(formEl: HTMLFormElement | null, name: string): void {
|
|
136
|
-
const control = formEl?.elements?.namedItem?.(name) as unknown;
|
|
137
|
-
const node =
|
|
138
|
-
typeof RadioNodeList !== "undefined" && control instanceof RadioNodeList
|
|
139
|
-
? (control[0] as HTMLElement | undefined)
|
|
140
|
-
: (control as HTMLElement | undefined);
|
|
141
|
-
node?.focus?.();
|
|
142
|
-
}
|
|
17
|
+
export { FORM_ERROR, validateValue } from "../../wix/forms/form-store";
|
|
143
18
|
|
|
144
19
|
export interface UseWixFormOptions {
|
|
145
20
|
/** Server-fetched form (Astro frontmatter) — skips the client fetch entirely. */
|
|
@@ -162,7 +37,7 @@ export interface UseWixForm {
|
|
|
162
37
|
"aria-invalid": true | undefined;
|
|
163
38
|
};
|
|
164
39
|
/** `onSubmit`. Resolves TRUE when the submission was created — that IS the success signal. */
|
|
165
|
-
submit: (event?:
|
|
40
|
+
submit: (event?: FormSubmitEvent) => Promise<boolean>;
|
|
166
41
|
/** One field, one address subfield, or the whole form when called with nothing. */
|
|
167
42
|
validate: (target?: string) => boolean;
|
|
168
43
|
errors: FormErrors;
|
|
@@ -172,84 +47,15 @@ export interface UseWixForm {
|
|
|
172
47
|
|
|
173
48
|
export function useWixForm(formId: string, options: UseWixFormOptions = {}): UseWixForm {
|
|
174
49
|
const { initialForm } = options;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const [loading, setLoading] = useState(!initialForm);
|
|
179
|
-
// The empty form to reset to after a successful submit — the schema's own defaults.
|
|
180
|
-
const emptyRef = useRef<FormValues>({});
|
|
181
|
-
|
|
182
|
-
const fields = useMemo(() => form?.fields ?? [], [form]);
|
|
183
|
-
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
if (initialForm) return; // the SSR pass already answered this
|
|
186
|
-
let alive = true;
|
|
187
|
-
if (!formId) {
|
|
188
|
-
setLoading(false);
|
|
189
|
-
setErrors({ [FORM_ERROR]: "No formId — pass one from the seed's forms map." });
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
setLoading(true);
|
|
193
|
-
getForm(formId)
|
|
194
|
-
.then((loaded) => {
|
|
195
|
-
if (!alive) return;
|
|
196
|
-
setForm(loaded);
|
|
197
|
-
setErrors({});
|
|
198
|
-
setLoading(false);
|
|
199
|
-
})
|
|
200
|
-
.catch((e: unknown) => {
|
|
201
|
-
if (!alive) return;
|
|
202
|
-
// Fail loudly. A form that cannot load is a setup problem — never fall back to a
|
|
203
|
-
// hand-built form, which would drop real enquiries silently.
|
|
204
|
-
setErrors({ [FORM_ERROR]: e instanceof Error ? e.message : "Could not load the form." });
|
|
205
|
-
setLoading(false);
|
|
206
|
-
});
|
|
207
|
-
return () => {
|
|
208
|
-
alive = false;
|
|
209
|
-
};
|
|
210
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
211
|
-
}, [formId]);
|
|
212
|
-
|
|
213
|
-
// Seed the controls once the schema is in: every control is controlled from the first render,
|
|
214
|
-
// so each target must hold a value of the right shape before any of them mount.
|
|
50
|
+
const ref = useRef<{ formId: string; store: FormStore } | null>(null);
|
|
51
|
+
if (!ref.current || ref.current.formId !== formId) ref.current = { formId, store: createFormStore({ formId, initialForm }) };
|
|
52
|
+
const store = ref.current.store;
|
|
215
53
|
useEffect(() => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const validate = useCallback(
|
|
222
|
-
(target?: string) => {
|
|
223
|
-
if (!target) {
|
|
224
|
-
const all = errorsForForm(fields, values);
|
|
225
|
-
setErrors(all);
|
|
226
|
-
return Object.keys(all).length === 0;
|
|
227
|
-
}
|
|
228
|
-
const key = String(target);
|
|
229
|
-
const field = fields.find((f) => f.target === key.split("/")[0]);
|
|
230
|
-
if (!field) return true;
|
|
231
|
-
|
|
232
|
-
// The keys this call owns, so a re-check CLEARS what it fixed as well as flagging what it
|
|
233
|
-
// did not: one key for a plain field, or every subfield when an address itself is named.
|
|
234
|
-
const owned = key.includes("/")
|
|
235
|
-
? [key]
|
|
236
|
-
: field.control === "address"
|
|
237
|
-
? field.addressParts.map(({ sub }) => `${field.target}/${sub}`)
|
|
238
|
-
: [field.target];
|
|
239
|
-
|
|
240
|
-
const found = errorsForField(field, values);
|
|
241
|
-
setErrors((prev) => {
|
|
242
|
-
const next = { ...prev };
|
|
243
|
-
for (const k of owned) {
|
|
244
|
-
delete next[k];
|
|
245
|
-
if (found[k]) next[k] = found[k];
|
|
246
|
-
}
|
|
247
|
-
return next;
|
|
248
|
-
});
|
|
249
|
-
return owned.every((k) => !found[k]);
|
|
250
|
-
},
|
|
251
|
-
[fields, values],
|
|
252
|
-
);
|
|
54
|
+
store.start();
|
|
55
|
+
return () => store.stop();
|
|
56
|
+
}, [store]);
|
|
57
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
58
|
+
const { values, errors } = state;
|
|
253
59
|
|
|
254
60
|
// Covers input / textarea / select. A checkbox or radio group carries `checked` instead of
|
|
255
61
|
// `value`, and a file input cannot be controlled at all — wire those by hand, keeping the
|
|
@@ -258,60 +64,22 @@ export function useWixForm(formId: string, options: UseWixFormOptions = {}): Use
|
|
|
258
64
|
(target: string) => ({
|
|
259
65
|
name: target,
|
|
260
66
|
value: String(values[target] ?? ""),
|
|
261
|
-
onChange: (e: { target: { value: string } }) =>
|
|
262
|
-
|
|
263
|
-
onBlur: () => validate(target),
|
|
67
|
+
onChange: (e: { target: { value: string } }) => store.setValue(target, e.target.value),
|
|
68
|
+
onBlur: () => store.validate(target),
|
|
264
69
|
"aria-describedby": `err-${target}`,
|
|
265
70
|
"aria-invalid": errors[target] ? (true as const) : undefined,
|
|
266
71
|
}),
|
|
267
|
-
[values, errors,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const submit = useCallback(
|
|
271
|
-
async (event?: { preventDefault?: () => void; currentTarget?: unknown }) => {
|
|
272
|
-
event?.preventDefault?.();
|
|
273
|
-
// Capture the <form> NOW: React clears currentTarget once the handler returns, so reading
|
|
274
|
-
// it after the await below (to focus a server-rejected control) comes back null.
|
|
275
|
-
const formEl = (event?.currentTarget ?? null) as HTMLFormElement | null;
|
|
276
|
-
if (!form) return false;
|
|
277
|
-
|
|
278
|
-
// Client pass first, so the visitor gets inline feedback in OUR wording before a round trip.
|
|
279
|
-
const clientErrors = errorsForForm(fields, values);
|
|
280
|
-
if (Object.keys(clientErrors).length) {
|
|
281
|
-
setErrors(clientErrors);
|
|
282
|
-
// FOCUS the first invalid control — scrollIntoView moves the viewport and nothing else,
|
|
283
|
-
// leaving a keyboard or screen-reader user where they were.
|
|
284
|
-
focusControl(formEl, Object.keys(clientErrors)[0]);
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
setLoading(true);
|
|
289
|
-
try {
|
|
290
|
-
// Attachments go up FIRST — a File is not something the submission API takes, and its
|
|
291
|
-
// value is the upload URL this hands back. No file fields → nothing happens here.
|
|
292
|
-
const uploaded = await uploadFiles(form.id, fields, values);
|
|
293
|
-
setValues(uploaded); // keep the URLs, so a rejection on another field never re-uploads
|
|
294
|
-
await createSubmission(form.id, toSubmissionValues(fields, uploaded));
|
|
295
|
-
setErrors({});
|
|
296
|
-
setValues(emptyRef.current); // back to the schema's defaults, ready for another
|
|
297
|
-
return true;
|
|
298
|
-
} catch (e) {
|
|
299
|
-
const mapped = submissionErrors(e, fields);
|
|
300
|
-
if (Object.keys(mapped).length) {
|
|
301
|
-
setErrors(mapped);
|
|
302
|
-
focusControl(formEl, Object.keys(mapped)[0]);
|
|
303
|
-
} else {
|
|
304
|
-
setErrors({
|
|
305
|
-
[FORM_ERROR]: e instanceof Error ? e.message : "Could not send the form. Please try again.",
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
return false;
|
|
309
|
-
} finally {
|
|
310
|
-
setLoading(false);
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
[fields, form, values],
|
|
72
|
+
[values, errors, store],
|
|
314
73
|
);
|
|
315
74
|
|
|
316
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
form: state.form,
|
|
77
|
+
values,
|
|
78
|
+
setValues: store.setValues,
|
|
79
|
+
bind,
|
|
80
|
+
submit: store.submit,
|
|
81
|
+
validate: store.validate,
|
|
82
|
+
errors,
|
|
83
|
+
loading: state.loading,
|
|
84
|
+
};
|
|
317
85
|
}
|