# 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 **`schema`** when 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 **`schemaentry`** attribute: 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-file`** and **`hb-input-coords`**, forwards **`i18nlang`** to the child.
- Aggregates per-field validity in **`valids`**; the whole form is considered invalid until visible fields have reported validity (see [Validation and `_valid`](#validation-and-_valid)).
- Supports **`dependencies`**: fields (and nested columns) can show or hide based on other fields’ values.
- **`submitted="yes"`** triggers **`onSubmit()`** on a microtask after render (programmatic submit from the host).
- Several text-like inputs call **`onclickEnter`** to run the same submit handler as the primary button.
- Dispatches **`update`** on value/validity changes with a **300 ms** debounce (only `update` is 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-email`
- `hb-input-select`, `hb-input-radio`, `hb-input-checkbox`
- `hb-input-date`, `hb-input-datetime`, `hb-input-color`, `hb-input-range`
- `hb-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. |
| `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`

```ts
{ 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 (not `undefined` / `null` / `""`). If present, the controller’s current value must satisfy **`dep.values.includes(depVal)`** (strict equality with `string | 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`        | |

---

## Layout: `row` and columns

- Top-level (or nested) entry with **`type: "row"`** must provide **`params.columns`** as another array of **`FormSchemaEntry`** objects.
- The row’s own **`id`** participates in **`visibility`** like any other entry.
- Column cells are **`column is-flex is-align-items-center`**; checkboxes get inline **`min-height: 3.25rem`** on the column for vertical alignment.

---

## Conditional visibility

1. On string schema parse, **`setVisibility`** walks 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**.
2. **`dependencyMap`** groups entries by **every** dependency id they reference (via `groupMultipleBy`).
3. When a control fires **`onsetVal`** with `{ id, value, valid }`, **`setValByMessage`** updates **`allValues`**, **`valids`**, and for the changed id runs **`handleVisibility`** on dependents—cascading **`show`** / **`hide`**.
4. **`values`** (derived object used for submit) includes only entries where **`visibility[id]`** is true, using **`allValues[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 stores **`valids[fieldId]`**.
- **`is_invalid`** is **`true`** when there are **no** `valids` entries yet, or when **any visible** field has **`valids[id] === false`**.
- **`onSubmit`** always sets **`show_validation = "yes"`** so users see errors after an attempt.
- If invalid: dispatches **`submitinvalid`** with **`{}`** and **does not** dispatch **`submit`**.
- If valid: dispatches **`submit`** with **`{ _valid: true, ...values }`** where **`values`** is the flat map of visible field ids → current values (same shape as `FormSubmitLikeDetail` in 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](https://bulma.io/documentation/features/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).

```html
<hb-form
  id="signup"
  schema="[{&quot;type&quot;:&quot;text&quot;,&quot;id&quot;:&quot;name&quot;,&quot;label&quot;:&quot;Name&quot;,&quot;required&quot;:true}]"
  show_validation="no"
  submitted="no"
  i18nlang="en"
></hb-form>
```

### 2. Vanilla JS — programmatic submit and listening

```js
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

```js
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)

```js
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.
