@coding-blocks/vmc-web-components
v0.2.1
Published
Embeddable HTML web components (React powered) for Vidyamandir Classes forms
Keywords
Readme
@coding-blocks/vmc-web-components
Embeddable HTML web components for Vidyamandir Classes forms. React on the
inside, plain custom elements on the outside — a host page only needs one
<script> tag and the element.
Every component in this repo ships in one bundle: dist/vmc-web-components.js
— 37.8 kB, ~14 kB gzipped over the wire, with the runtime and the CSS inlined.
No peer dependencies, no extra network requests.
The source is written against React, but the build aliases react/react-dom
to preact/compat, which is what actually ships — same JSX and hooks, ~5x
smaller for a script that loads on other people's pages. Nothing in src/
imports Preact directly, so the alias in vite.config.ts is the only place that
knows. Two build notes worth keeping: the published bundle carries no
sourcemap (it was 4.5x the size of the code), and terser's unsafe_* /
booleans_as_integers options are deliberately off — they saved ~2 kB and broke
Preact's event proxy at runtime.
Usage
<script src="https://unpkg.com/@coding-blocks/vmc-web-components"></script>
<vmc-online-lead></vmc-online-lead>The API base URL is compiled into the bundle — embedding pages never pass it. See Environments.
Pin a version in production:
<script src="https://unpkg.com/@coding-blocks/[email protected]/dist/vmc-web-components.js"></script>The script can go anywhere — elements already in the DOM upgrade as soon as it loads. Each component renders inside a shadow root, so the host page's CSS can't leak in and the component's CSS can't leak out.
Components
<vmc-online-lead>
The "Want a call back?" lead form. Fields posted to the backend:
name, email, dial_code, mobile, form_class_id, form_course_id,
state_id, city_id.
All eight visible fields are mandatory — name, email, mobile, class, course,
state and city are validated on submit, each showing its own message under the
field (Please select a class.) and a red border, plus required /
aria-required on the control itself. Course stays disabled until a class is
picked and city until a state is picked, and each resets when its parent
changes, so an orphaned pair can't be submitted.
Mobile verification is mandatory: Submit stays disabled until the number is OTP-verified and the consent box is ticked. Editing the number after verification drops it back to unverified, so a submission can never carry an OTP that belongs to a different number.
Once verified, the dial code and mobile inputs are disabled, and the verified number is held in verification state. The submit payload is built from that stored value rather than from the inputs, so re-enabling the fields through devtools cannot put an unverified number on the wire.
| Attribute | Default | Purpose |
| --- | --- | --- |
| api-base | build-time VITE_API_URL | Override the compiled-in base (testing only) |
| heading | Want a call back ? | Card heading |
| dial-code | +91 | Pre-selected dial code |
| consent-text | VMC authorisation copy | Consent checkbox label |
| submit-label | Submit | Submit button label |
| success-title | Thank you! | Heading after a successful submit |
| success-message | counsellor call-back copy | Body after a successful submit |
| extra-payload | — | JSON object merged into the submit payload |
Events (bubbling and composed, so they cross the shadow boundary):
const el = document.querySelector('vmc-online-lead')
el.addEventListener('vmc-lead-success', (e) => console.log(e.detail.response))
el.addEventListener('vmc-lead-error', (e) => console.log(e.detail.error))<vmc-online-leed-neet>
The NEET stream. Identical to <vmc-online-lead> — same fields, validation, OTP
flow and payload — with two differences:
- the class list is fetched narrowed to that category:
GET /lookups/form-classes?filter={"category":"NEET"} sourcedefaults tovmc-online-lead-form-neetinstead ofvmc-online-lead-form(adata-sourceattribute still wins)
Courses are unchanged: fetched for whichever class is selected, via
?filter={"form_class_id":<id>}.
<vmc-online-leed-neet data-source="neet-landing-page"></vmc-online-leed-neet>It takes every attribute listed for <vmc-online-lead> above.
Both elements are the same React component behind a Variant
(src/components/OnlineLead.tsx) holding the default source and an optional
class-list filter — so a new stream is a Variant plus one defineElement
call, not a copy of the form.
Environments
The base URL comes from VITE_API_URL, read at build time and inlined into
dist/vmc-web-components.js:
| File | Used by | Value |
| --- | --- | --- |
| .env.development | npm run dev, npm run build:dev | http://localhost:3000/api |
| .env.production | npm run build (what gets published) | https://vmcrm-api.codingblocks.com/api |
npm run build always builds in production mode, so npm publish can only ever
ship the production base. The build refuses to start if VITE_API_URL is
missing or isn't a URL, so a bundle pointing nowhere can't be produced.
Changing the base means a rebuild and a republish — that's the point: embedding pages carry no configuration and can't drift.
Endpoints
Defaults are relative to the compiled-in base. Each is overridable per element with the matching attribute, so a component can be repointed without a rebuild.
| Purpose | Default path | Attribute |
| --- | --- | --- |
| Classes | GET /lookups/form-classes | classes-path |
| Courses | GET /lookups/form-courses | courses-path |
| States | GET /lookups/states | states-path |
| Cities | GET /lookups/cities | cities-path |
| Send OTP | POST /auth/login/send-otp | otp-send-path |
| Verify OTP | POST /auth/login/verify-otp | otp-verify-path |
| Submit form | POST /forms/vmc-online-lead | submit-path |
Dependent lookups pass a Prisma-shaped where clause as the filter query
param, matching the CRM's lookup controller:
GET /lookups/form-courses?filter={"form_class_id":6}
GET /lookups/cities?filter={"state_id":2}Lookup responses may be either a bare array or { data: [...] }; both are
accepted. Items are { id, name }.
OTP flow (same shape as the CRM login flow):
POST {otp-send-path} { mobile_number, dial_code } -> { otp_id }
POST {otp-verify-path} { mobile_number, dial_code, otp_id, otp } -> 2xxSubmit payload:
{
"name": "…",
"email": "…",
"dial_code": "+91",
"mobile": "9999999999",
"form_class_id": 6,
"form_course_id": 61,
"state_id": 2,
"city_id": 21,
"admission_test_id": 2147483647,
"otp_id": "…",
"mobile_verified": true,
"consent": true,
"source": "vmc-online-lead-form",
"page_url": "https://…"
}source is always vmc-online-lead-form, and admission_test_id is always
2147483647 — the fixed placeholder row for leads
that arrive without a test assigned.
Errors are read from { error } (falling back to { message }) on any non-2xx
response, so backend validation messages surface directly in the form.
Theming
Override the CSS custom properties on the element — they pierce the shadow root:
<style>
vmc-online-lead {
--vmc-primary: #3b3663;
--vmc-heading: #322c78;
--vmc-font: 'Poppins', sans-serif;
--vmc-max-width: 560px;
--vmc-card-radius: 24px;
--vmc-field-height: 56px;
}
</style>The component does not load webfonts. If the page wants Poppins (the VMC look), include it in the host page and it will be inherited.
Development
npm install
npm run dev # http://localhost:5173/playground/index.html (.env.development)
npm run build # production bundle + dist/types (.env.production)
npm run build:dev # same bundle, pointed at localhost — for testing dist/ locally
npm run typecheckplayground/index.html mounts every component and logs every request plus the
events it fires. Two controls at the top:
- API base override — leave empty to use the compiled-in
.env.developmentbase; fill it in to point the component elsewhere. - Mode:
hybrid(default) — classes, courses and the real OTP flow hit the backend;/lookups/states,/lookups/citiesandPOST /forms/vmc-online-leadare stubbed, because the first two sit behindrequireAuthand the third isn't written yet.live— nothing is stubbed.mock— every endpoint is stubbed, no backend needed. OTP is123456.
Both can be driven from the URL: ?api=…&mode=mock.
The OTP defaults point at
/auth/login/send-otp+/auth/login/verify-otp, which only accept mobiles that already belong to a registered CRM user — an unknown number gets404 No account found for this mobile. A public lead form will eventually need its own pair; pointotp-send-path/otp-verify-pathat them when they exist.
Adding a component
- Write the React component in
src/components/, taking{ props, ctx }. - Register it in
src/index.tsviadefineElement('vmc-…', Component, attrs), listing the attributes that should trigger a re-render. - Mount it in
playground/index.html.
It lands in the same single bundle automatically.
Publishing
npm version patch
npm publish # runs the build via prepublishOnlyPublished under the @coding-blocks scope with public access.
