@htmlbricks/hb-form
v0.76.5
Published
JSON `schema`-driven form engine: each entry’s `type` maps to an `hb-input-*` web component or layout `row`. Handles grouping, conditional visibility, validation messages, disabled state, and dispatches rich `update` payloads (field values + `_valid`) plu
Readme
hb-form
Category: forms · Tags: forms · Package: @htmlbricks/hb-form
hb-form is a JSON schema-driven form engine for HTML Bricks: each schema row describes one logical field (or a layout row). The host maps type to the correct hb-input-* custom element, wires validation state, conditional visibility from dependencies, and exposes a submit / update event contract for backends, SPAs, or low-code flows.
You integrate by (1) loading this component plus its input dependencies, (2) passing a schema (JSON string from HTML, or an object when using a JS framework), and (3) listening for update, submit, submitinvalid, and optionally getValues.
What it does
- Renders a Bulma-styled form (
columns/field/label) inside the shadow root. - Parses
schemawhen it arrives as a string (typical HTML attribute), merges live values, and avoids redundant resets when the same schema string is re-applied. - Passes each control a
schemaentryattribute: JSON serialization of the schema entry merged with the current value (allValues[id] ?? entry.value). - Forwards
show_validation("yes"/"no") to every nested input so validation UI can be shown or suppressed consistently. - For
hb-input-fileandhb-input-coords, forwardsi18nlangto the child. - Aggregates per-field validity in
valids; the whole form is considered invalid until visible fields have reported validity (see Validation and_valid). - Supports
dependencies: fields (and nested columns) can show or hide based on other fields’ values. submitted="yes"triggersonSubmit()on a microtask after render (programmatic submit from the host).- Several text-like inputs call
onclickEnterto run the same submit handler as the primary button. - Dispatches
updateon value/validity changes with a 300 ms debounce (onlyupdateis debounced; other events fire immediately).
Runtime prerequisites
hb-form dynamically registers child packages via addComponent (see component.wc.svelte). Your page or bundle must actually load the corresponding hb-input-* scripts/styles so those custom elements are defined.
Declared dependency graph (from extra/docs.ts metadata) includes at least:
hb-input-text,hb-input-area,hb-input-number,hb-input-emailhb-input-select,hb-input-radio,hb-input-checkboxhb-input-date,hb-input-datetime,hb-input-color,hb-input-rangehb-input-file,hb-input-coords,hb-input-array-objects,hb-input-array-tags
Nested packages (e.g. hb-input-array-objects → hb-form, hb-table, dialogs, paginate, etc.) apply when you use those field types—plan bundle size accordingly.
Custom element
| Tag | Notes |
| --------- | ------------------------------------------ |
| hb-form | Shadow root; forwards Bulma + local SCSS. |
Attributes and properties (snake_case, string-friendly)
HTML and setAttribute only carry strings. Booleans and numbers must follow your host conventions (this codebase generally uses yes / no for booleans on attributes where documented). TypeScript Component (see types/webcomponent.type.d.ts) describes the logical shape.
| Name | Required | Type / values (logical) | Role |
| -------------------- | -------- | ------------------------------------------ | ---- |
| schema | yes | FormSchema (array) or JSON string | Field definitions. Recommended: pass schema as a JSON string from HTML or setAttribute: the parse $effect runs JSON.parse, resets valids / allValues / visibility, and calls setVisibility. Identical consecutive strings are skipped to avoid churn. If you bind a live object without going through that string path, setVisibility may never run in the reference implementation—controls can stay invisible until you adjust the integration. |
| id | no | string | Passed through to update detail as _id (see events). Default "". |
| style | no | string | Present on Component typings; not consumed by current script logic (commented in source)—safe for host/CSS use if your toolchain forwards it. |
| values | no | FormValues | Declared on Component for typing / tooling; not read in component.wc.svelte—values come from schema defaults, user input, and allValues. |
| isInvalid | no | boolean | Declared on Component typings; not read in component.wc.svelte (internal state is the derived is_invalid variable). Treat as reserved / wrapper metadata unless your build maps it. |
| submitted | no | "yes" | "no" | null | "yes" queues onSubmit() on a microtask. Inside submit, the implementation sets show_validation = "yes" and submitted = "no" before validating. |
| getvals | no | "yes" | "no" | null | Typed API for getValues. The source defines getVals() but does not call it from a reactive effect; toggling this attribute alone may not emit getValues until your bundle wires it. Prefer update / submit for snapshots unless you confirm behavior in your build. |
| show_validation | no | "yes" | "no" (default "no") | Forwarded to all child inputs. Forced to "yes" when submit runs. |
| hide_submit | no | boolean (coerced) | If truthy (true, "yes", "true"), the entire submit area (including slots) is not rendered. |
| buttons_outlined | no | "yes" | "no" (default "no") | When "yes" (or coerced "true" / boolean true from JS), the default submit <button> gets Bulma is-outlined in addition to is-primary. Does not affect fully custom submit_button slot markup. |
| i18nlang | no | string (e.g. en, it) | Passed to hb-input-file and hb-input-coords. |
schema — FormSchema and each entry
FormSchema is FormSchemaEntry[]. Shared fields (FormSchemaEntryShared) and the full entry type are defined in types/webcomponent.type.d.ts.
Common fields (every entry, including inside row columns)
| Field | Type | Purpose |
| ------------------- | ---------------------------- | ------- |
| id | string | Stable key for DOM for=, internal maps, and event payloads. Required for non-row entries you expect to submit. |
| type | string | Discriminator mapped in registeredComponents (see table below). On standalone schemaentry payloads to inputs, type may be implied by the host tag. |
| label | string | Rendered above the control unless labelIsHandledByComponent applies (checkbox). |
| value | unknown | Default / initial value when nothing is in allValues yet. |
| dependencies | FormSchemaDependency[] | Conditional visibility (see below). |
| readonly | boolean | Carried in schemaentry for inputs that support it. |
| disabled | boolean | Same. |
| required | boolean | Shown as * on label; enforced by child inputs + validation. |
| validationRegex | string | Carried to inputs that honor regex validation. |
| validationTip | string | Tip text for invalid state in children. |
| placeholder | string | Passed through schemaentry. |
| params | Record<string, unknown> | Type-specific options (e.g. min/max for number, options for select/radio, nested schema for arrayobjects). |
FormSchemaDependency
{ id: string; values?: (string | number | boolean)[] }id: id of the controlling field.values: optional whitelist. If omitted, the dependent is eligible when the controller has any non-empty value (notundefined/null/""). If present, the controller’s current value must satisfydep.values.includes(depVal)(strict equality withstring | number | boolean).
Dependency resolution uses valueForDependency: live allValues[depId] first, otherwise the value on the schema entry for that id.
Schema type → nested custom element
Mapping is defined in registeredComponents in component.wc.svelte. Unknown type values cause setVisibility / getControls to throw at runtime—there is no silent skip.
| Schema type | Child element | Notes |
| --------------- | ----------------------------- | ----- |
| row | (none — layout only) | Renders Bulma columns is-multiline. Nested entries live in params.columns. registeredComponents.row uses options.row: true and component: undefined. |
| text | hb-input-text | Enter key submits. |
| textarea | hb-input-area | Enter key submits (handler on component). |
| number | hb-input-number | Enter key submits. |
| email | hb-input-email | Enter key submits. |
| select | hb-input-select | |
| radio | hb-input-radio | |
| checkbox | hb-input-checkbox | labelIsHandledByComponent: true — outer hb-form label is not rendered (the checkbox handles labeling). Column cells get a slightly taller min-height for alignment. |
| date | hb-input-date | Enter key submits. |
| datetime | hb-input-datetime | Enter key submits. |
| color | hb-input-color | |
| file | hb-input-file | Receives i18nlang. |
| range | hb-input-range | |
| coords | hb-input-coords | Receives i18nlang. |
| arrayobjects | hb-input-array-objects | Nested schema under params.schema. |
| arraytags | hb-input-array-tags | Optional schema field array_style: "pills" or "area" (string). When omitted, hb-form passes array_style="area" on the host (textarea-like chips). Set array_style: "pills" for the classic tag row + add control. |
Layout: row and columns
- Top-level (or nested) entry with
type: "row"must provideparams.columnsas another array ofFormSchemaEntryobjects. - The row’s own
idparticipates invisibilitylike any other entry. - Column cells are
column is-flex is-align-items-center; checkboxes get inlinemin-height: 3.25remon the column for vertical alignment.
Conditional visibility
- On string schema parse,
setVisibilitywalks the tree: for each entry,visibility[id] = visibility[id] || !dependencies?.length. So fields with dependencies start hidden (false) unless already true; fields without dependencies start visible. dependencyMapgroups entries by every dependency id they reference (viagroupMultipleBy).- When a control fires
onsetValwith{ id, value, valid },setValByMessageupdatesallValues,valids, and for the changed id runshandleVisibilityon dependents—cascadingshow/hide. values(derived object used for submit) includes only entries wherevisibility[id]is true, usingallValues[id] ?? entry.value.
Hidden branch values are therefore omitted from submit / getValues payloads (matching legacy “visible only” shape).
Validation and _valid
- Each child emits
onsetVal; the form storesvalids[fieldId]. is_invalidistruewhen there are novalidsentries yet, or when any visible field hasvalids[id] === false.onSubmitalways setsshow_validation = "yes"so users see errors after an attempt.- If invalid: dispatches
submitinvalidwith{}and does not dispatchsubmit. - If valid: dispatches
submitwith{ _valid: true, ...values }wherevaluesis the flat map of visible field ids → current values (same shape asFormSubmitLikeDetailin typings:_valid+FormValues).
Events (types/webcomponent.type.d.ts)
All events are native CustomEvent on the host element (bubbles / composed follow your Svelte CE compile settings; detail shapes below match the implementation).
| Event | detail type (logical) | When |
| ---------------- | ------------------------- | ---- |
| update | { _valid: boolean; _id: string } & FormValues | Value or validity changed; debounced 300 ms and coalesced. _id is the form’s id prop (string). |
| submit | FormSubmitLikeDetail = { _valid: boolean } & FormValues | Successful validation after submit (button or submitted="yes"). _valid is true when dispatched. |
| submitinvalid | Record<string, never> ({}) | Submit attempted while the form is invalid. |
| getValues | FormSubmitLikeDetail | Same merge as submit when getVals() runs with getvals === "yes" (see attribute notes). |
Correction vs older docs: submit and getValues details are a flat object { _valid, ...fieldValues }, not a nested { values: { ... } } object. update is also flat field keys plus _valid and _id, not a nested values property.
Slots (::part host children)
Declared in extra/docs.ts / rendered near the bottom of component.wc.svelte.
| Slot | Description |
| ----------------- | ----------- |
| submit_button | Replaces the entire default submit control (default content is the primary Bulma button + inner label slot). Still wrapped in the same clickable span that calls onSubmit(). |
| submit_label | Default text inside the primary button (Submit). Ignored if submit_button fully replaces the button. |
| other_buttons | Extra controls rendered after the submit slot, still inside button_container. |
If hide_submit is true, no submit region (and no slots) is rendered.
CSS parts
| Part | Description |
| -------------------- | ----------- |
| button_container | Flex wrapper around submit + other_buttons. Element id button_container. |
| main_button | Default <button type="button" class="button is-primary"> when using the stock submit_button slot. |
Style the host with ::part(button_container) / ::part(main_button) from the light DOM (where supported).
CSS custom properties
From styleSetup / Bulma integration (extra/docs.ts):
| Variable | Meaning |
| ---------------------- | ------- |
| --bulma-column-gap | Horizontal padding / column gap on :host; tuned so Bulma .columns negative margins fit. |
Additional --bulma-* variables come from forwarded Bulma modules in styles/bulma.scss (form, grid, button, etc.). See Bulma CSS variables.
Internationalization
i18nlang is forwarded to file and coords inputs. Supported languages listed in extra/docs.ts include English and Italian; extend via package metadata if more locales are registered upstream.
Integration patterns
1. Declarative HTML (minimal)
Pass schema as a single JSON string (escape quotes for HTML).
<hb-form
id="signup"
schema="[{"type":"text","id":"name","label":"Name","required":true}]"
show_validation="no"
submitted="no"
i18nlang="en"
></hb-form>2. Vanilla JS — programmatic submit and listening
const form = document.querySelector("hb-form");
form.addEventListener("update", (e) => {
const { _valid, _id, ...rest } = e.detail;
console.log("valid?", _valid, "form id:", _id, "values:", rest);
});
form.addEventListener("submit", (e) => {
if (e.detail._valid) {
console.log("posted", e.detail);
}
});
form.addEventListener("submitinvalid", () => {
console.warn("fix validation errors");
});
// Trigger the same path as clicking submit (after schema is parsed):
form.setAttribute("submitted", "yes");3. Building schema in JavaScript
const schema = [
{
id: "profile",
type: "row",
params: {
columns: [
{ id: "firstName", type: "text", label: "First name", required: true },
{ id: "lastName", type: "text", label: "Last name", required: true },
],
},
},
{
id: "age",
type: "number",
label: "Age",
required: true,
params: { min: 0, max: 120 },
validationTip: "Enter a realistic age.",
},
];
form.setAttribute("schema", JSON.stringify(schema));4. Conditional field (dependencies)
const schema = [
{ id: "code", type: "text", label: "Access code", required: true },
{
id: "secret",
type: "text",
label: "Secret phrase",
dependencies: [{ id: "code", values: ["VIP"] }],
},
];The secret field stays hidden until code’s live value is exactly "VIP" (or the number/boolean you list in values).
Troubleshooting
| Symptom | Likely cause |
| ------- | ------------- |
| Console error unknown component type | type string not in the mapping table—fix the schema or extend the registry in source. |
| Submit always invalid at first | valids empty until children emit onsetVal—ensure inputs mount and fire validation for visible fields. |
| Schema changes ignored | String compare: identical schema string to the previous one is intentionally skipped; mutate or append a cache-busting suffix if you truly need a no-op reparse. |
| Dependents never appear | Controller value empty or not in values whitelist; dependency controller may still be hidden. |
TypeScript
Authoring types: types/webcomponent.type.d.ts — Component (including schema as string | FormSchema), Events, FormSchemaEntry, FormSubmitLikeDetail, FormValues, IComponentName, etc.
See also
extra/docs.ts—styleSetup, slots, Storybook args, dependency list, and packaged examples (schema1, conditional schemas, file + array-objects samples).component.wc.svelte—registeredComponents, visibility, submit pipeline, and slot markup.
