@tvsgroup/appointment-form
v0.1.8
Published
TVS Engineering appointment booking form as a <tvs-appointment-form> web component with a typed Vue wrapper
Readme
@tvsgroup/appointment-form
Multi-step appointment booking form for TVS Engineering, shipped as the <tvs-appointment-form> web component (Vue 3 inside a shadow root) with an optional typed Vue wrapper. Consumable three ways:
- Web component from any framework or plain HTML (Astro, WordPress, …)
- Vue wrapper component (
AppointmentForm) for Vue apps / Astro with@astrojs/vue - Standalone bundle (
dist/appointment-form.es.js) from a CDN or self-hosted — the legacy embed, unchanged
Installation
npm install @tvsgroup/appointment-formVue 3 is a peer dependency (npm ≥7 installs it automatically). All other runtime dependencies (Pinia, intl-tel-input, js-datepicker, …) are bundled.
Attributes / Props
| Custom-element attribute | Vue wrapper prop | Type (wrapper) | Default | Description |
|---|---|---|---|---|
| lang | lang | string | 'nl' | UI language (nl or en) |
| aws_url | awsUrl | string | — (required) | Base URL of the appointment API |
| vacation_mode | vacationMode | boolean | false | Holiday closure: disables queue/direct lanes, shows closure banner. Attribute form accepts "true" / "1" / "yes" / bare attribute |
| vacation_start | vacationStart | string | '' | First closed day, ISO YYYY-MM-DD |
| vacation_end | vacationEnd | string | '' | Last closed day, ISO YYYY-MM-DD |
| vacation_reopen | vacationReopen | string | '' | First day back at work, ISO YYYY-MM-DD |
Usage
Astro — web component (no framework integration needed)
---
// src/pages/afspraak.astro
---
<tvs-appointment-form
lang="nl"
aws_url="https://<api-id>.execute-api.us-east-1.amazonaws.com"
></tvs-appointment-form>
<script>
import '@tvsgroup/appointment-form/register';
</script>The /register import defines the element on the client (idempotent, SSR-safe).
Astro + @astrojs/vue — Vue wrapper
---
import { AppointmentForm } from '@tvsgroup/appointment-form';
---
<AppointmentForm
client:only="vue"
lang="nl"
awsUrl="https://<api-id>.execute-api.us-east-1.amazonaws.com"
vacationMode={false}
/>client:only="vue" is recommended — the form is fully interactive and gains nothing from SSR.
Plain HTML (standalone bundle — legacy embed)
<script type="module" src="https://unpkg.com/@tvsgroup/appointment-form/dist/appointment-form.es.js"></script>
<tvs-appointment-form
lang="nl"
aws_url="https://<api-id>.execute-api.us-east-1.amazonaws.com"
></tvs-appointment-form>Host-page notes
- Multiple instances supported. Each
<tvs-appointment-form>element gets its own Pinia instance (installed per element viaconfigureApp), so several forms on one page keep fully independent state. - Fonts: the component inherits the host page's font stack; the TVS look expects Inter to be loaded by the host.
- Styles are encapsulated in a shadow root — the host page's CSS (and Tailwind config) won't clash with the form's.
- Runtime network: the phone input loads the intl-tel-input utils script from jsDelivr and does a geo-IP lookup (ipapi.co) for the default country flag.
Package layout
| Entry | File | Vue |
|---|---|---|
| @tvsgroup/appointment-form | dist/index.js — AppointmentForm, register(), TAG_NAME, types | external (peer) |
| @tvsgroup/appointment-form/register | dist/register.js — side-effect: defines the element | external (peer) |
| standalone | dist/appointment-form.es.js — self-contained, self-registering | bundled |
Development
npm install
npm run dev # dev harness (src/main.ts + App.vue)
npm run build # standalone bundle + npm library (dist/)
npm run test:unit # vitest
npm run e2e:dry # Playwright suite in dry-run mode
npm run type-checkReleasing
Publishing is automated by .github/workflows/publish-npm.yml: every push to main builds the package and publishes it to npm, skipping the publish when that version is already on the registry. So a release is just a version bump in package.json merged to main.
Auth uses npm trusted publishing (OIDC) — there is no NPM_TOKEN secret to rotate, and provenance is attached automatically.
The standalone bundle is no longer deployed to S3; it ships inside the npm package as dist/appointment-form.es.js and is reachable from any npm CDN.
Known Issues / Pending Fixes
These items are tracked here so the team has a single source of truth for outstanding tech debt. Update this list as items are resolved or new issues are discovered.
Cross-repo: Lambda createAppointment (tvs-cloud-services/services/rest/src/createAppointment.ts)
loanCarcontract mismatch (worked around in this repo). The Lambda typesloanCarasstring(scalar) and forwards it directly to Frappe's scalar fieldsloan_carandcustom_loan_car. This webcomponent originally sentloanCarasstring[](multiselect), which caused Frappe to throwfrappe.exceptions.ValidationError: Waar voor Loan car kan geen lijst worden(HTTP 417).- Current workaround:
buildSubmitPayloadinsrc/stores/appointmentForm.tsserializes the array to a CSV string before fetch. - Proper fix: keep
loanCar: string[]end-to-end and let the Lambda do the join. The transformation belongs in the anti-corruption layer (Lambda), not the UI.
- Current workaround:
Bug at
createAppointment.ts:152(latent). In the PUT branch (when a project already exists), the field assignment iscustom_is_loan_car: data.loanCar— it should bedata.needsLoanCar(boolean). Today it never fires because the flow always goes through POST, but it will break the moment a customer updates an existing appointment.
Internal: this repo
Duplicate
LOAN_CAR_LABELSmapping. The mapgolf6 → "GOLF 6 DSG",up → "VW UP!",caddy → "VW CADDY"lives in two places:src/stores/appointmentForm.ts— used bybuildSubmitPayloadfor the wire payload.src/views/steps/ConfirmView.vue— used byformatLoanCarsfor the confirmation summary shown to the customer.
If a label changes in only one place, the customer will see one value on Confirm but a different one will land in ERPNext. Candidate to dedupe into a shared util / constants module.
