@doany-ai/forms
v0.1.0
Published
The business's forms on a Doany site: a form hook and a default component that renders a form's questions and submits the answers
Readme
@doany-ai/forms
The business's forms on a Doany site: contact, inquiry, subscription, application, quiz. The business defines a form (its questions, which services it asks about, what to show after); the site renders it and submits what the visitor fills in. A submission lands in the business's Inbox. Nothing here creates forms.
import { Form } from "@doany-ai/forms";
// /contact — the form by its id, as the business defined it
<Form formId="…" context={{ page_path: "/contact" }} />List the page in doany/features.jsonc under forms.
Records
- FormView (
useForm):id,type(contact|inquiry|subscription|promotion|application|quiz|custom…),definition_hash(identifies what the visitor saw; sent back with the answers),definition(title,description,fields[],settings),availability(can_submit,reason:preview_onlyon a preview address,no_service_optionswhen a required service question has nothing to offer). - FormField:
key,type,label,help,required,control(radio|dropdownfor a choice;hiddenon aservicequestion, which is then not shown and answered with the services it offers),validation(min_length,max_length,min,max),options[](value,label),selectionandresolved_options[](servicequestions: the services on offer,service_modebooking|order|inquiry),consent(modecheckbox|on_submit). - Field types:
text,textarea,email,phone,number,date,select,multiselect,checkbox,scale,agreement,consent,hidden,statement,service. - FormSubmitReceipt (
useSubmitForm):id,submitted_at,scoreandoutcome(a quiz),completion(the form'sthank_you_message,redirect_url,cta).
Form
Renders the questions, submits the answers, shows the confirmation the form sets (or a quiz's outcome), redirects when the form says so.
| prop | |
| --- | --- |
| formId | required |
| context | { page_path?, referrer?, utm?, language?, timezone?, source_service_id? }, kept with the submission |
| defaultAnswers | answers to start with, by field key (a hidden value, a preselected service) |
| submitLabel | wins over the submit label |
| renderSuccess | (receipt) => node: replaces the confirmation |
| showTitle | the form's title and description above the questions; default true |
| variant | "single" (default): every question on one page. "steps": the questions in pages with a progress bar and Back / Next — a survey. The pages are steps when given, else one page per statement question (the statement heads its page), else one page. Each page's required questions gate Next |
| steps | for variant="steps": the field keys of each page, in order; keys left out go on the last page |
| frame | "plain" (default): just the questions. "card": on a raised card, the way a form embeds on a page |
| onSubmitted | (receipt) => void |
| labels, className | |
Answers go to the platform by field key: text, date and single choices as strings, several choices as arrays, checkbox / agreement / consent as booleans, numbers as numbers; blanks are left out. A consent with mode: "on_submit" is sent as agreed when the form is submitted.
A question that may be left blank says Optional at the end of its label row; required questions carry nothing (the merchant app's convention). A required text, textarea, email or phone question that is blank once trimmed is refused before anything is sent: answerRequired under the question, which takes focus (Next in a stepped form checks its page the same way); a phone that is not a number says phoneFormat the same way. The rest is the browser's check (required, the email format, lengths). The fields are @doany-ai/app-core/form's, shared with the booking form: an email question shows an envelope and an example address; a phone question is typed as a local number behind a country, shown as its flag and calling code (the chip opens a searchable list, the United States first and to start with; a number typed with its own + or 00 moves the flag), and is sent in E.164.
On a preview address, a notice appears before the questions. Visitors can fill in their answers, validate them and move through every page of a stepped form. The final action checks locally, keeps the answers and sends no submission; it never calls onSubmitted, renders a success receipt or follows a completion redirect. Use the published website for a real submission. A backend INVALID_STATE with details.reason="preview_only" is handled the same way if the page had older availability. A required service question with no options still blocks submission.
Failures: VERSION_CONFLICT (the questions changed meanwhile) re-reads the form and says so; RATE_LIMITED asks to wait; a VALIDATION_ERROR whose details.errors[].field names a question the visitor is asked (answers.<key>) is said under it (answerRequired or answerInvalid), on its page; anything else shows the site's submitFailed label when set, else the platform's message.
To put the form in a popup, a sidebar or a slide-in panel, wrap it in Embed from @doany-ai/app-core/ui; inside, useEmbed().markSubmitted() and close() tell the shell the visitor is done.
Hooks
| hook | returns |
| --- | --- |
| useForm(formId) | query of the FormView |
| useSubmitForm(formId) | mutation: mutate({ definition_hash, answers, context? }) → FormSubmitReceipt |
Labels
submit, submitting, thanks, score(score), loadError, submitFailed, previewOnly, previewChecked, noServiceOptions, definitionChanged, rateLimited, tryAgain, notFound, choose, optional, emailPlaceholder, country, searchCountry, noCountry, phoneFormat, answerRequired, answerInvalid (required is kept for sites that set it, unused). The questions' own words come from the form's definition.
Customizing
- Theme: the site's variables style the inputs, buttons and the confirmation.
- Props:
labels,submitLabel,renderSuccess,defaultAnswers,className. - Hooks: draw the form yourself on
useFormanduseSubmitForm.
The package ships no CSS: the site's Tailwind build scans node_modules/@doany-ai/forms/dist.
Develop
npm install
npm run typecheck # includes tests/sdk-contract.types.ts
npm test
npm run build