<baht-form>
The L1 engine: one dumb controlled form. JSON in → UI out; emits baht-change / baht-validity. No buttons, no cross-field logic.
JSON in, form out
Build this JSON visually in the Builder, then copy it here.
Events (baht-validity, baht-change, name)
<baht-form></baht-form>
<script>
const el = document.querySelector('baht-form');
el.form = {
"form_id": "contact",
"form_name": "Contact",
"fields": [
{
"order": 1,
"type": "text",
"key": "name",
"label": "Name",
"width": "1/2",
"required": true,
"value": ""
},
{
"order": 2,
"type": "text",
"key": "email",
"label": "Email",
"width": "1/2",
"required": true,
"value": ""
},
{
"order": 3,
"type": "dropdown",
"key": "topic",
"label": "Topic",
"width": "full",
"options": [
{
"key": "bug",
"label": "Bug report"
},
{
"key": "feature",
"label": "Feature request"
}
],
"value": null
},
{
"order": 4,
"type": "textarea",
"key": "message",
"label": "Message",
"rows": 4,
"value": ""
}
]
};
</script>Read-only display
Same JSON, shown as values: option keys become labels, passwords are masked, missing answers read "Not provided". getData() still returns the full data.
Events (baht-validity, baht-change, name)
<baht-form readonly="true"></baht-form>
<script>
const el = document.querySelector('baht-form');
el.form = {
"form_id": "contact-ro",
"form_name": "Contact",
"fields": [
{
"order": 1,
"type": "text",
"key": "name",
"label": "Name",
"width": "1/2"
},
{
"order": 2,
"type": "email",
"key": "email",
"label": "Email",
"width": "1/2"
},
{
"order": 3,
"type": "dropdown",
"key": "topic",
"label": "Topic",
"options": [
{
"key": "bug",
"label": "Bug report"
},
{
"key": "feature",
"label": "Feature request"
}
]
},
{
"order": 4,
"type": "password",
"key": "secret",
"label": "API secret"
},
{
"order": 5,
"type": "textarea",
"key": "message",
"label": "Message"
}
]
};
el.value = {
"name": "Malee Srisuk",
"email": "malee@example.com",
"topic": "bug",
"secret": "hunter2"
};
</script>| Name | Type | Default | Description |
|---|---|---|---|
formJS | FormDefinition | — | { form_id, form_name, fields[] } |
valueJS | Record<string, unknown> | {} | Controlled values |
registryJS | Registry | {} | Custom formatters/validators |
show-optional | boolean | false | Label every non-required field "(optional)" (localized) instead of relying on asterisks alone. |
optionsJS | BahtFormOptions | {} | { validateOn?: 'change'|'blur'|'submit', inputDebounce?: number } |
fieldStatesJS | Record<string, DerivedFieldState> | {} | Per-field UI state from L2 |
view | string | undefined | — | Name of a view from `form.views`. Display-only: when set, only that view's fields are rendered and validated (the user can only fix what they see), but getData() still returns the FULL form — the server always validates the complete schema, views never change the submit payload. |
flagsJS | FeatureFlags | {} | Feature-flag values (spec 01 §12) — fields whose `flag` is off behave as if they were never in the schema: no render, no validation, no payload. |
readonly | boolean | false | Render every field as a display value (not a disabled form). Per-field readonly comes from the descriptor or a rule |
Events
Section titled “Events”| Event | detail | Description |
|---|---|---|
baht-validity | { valid, errors[] } | Validity recomputed |
baht-change | { form_name, key, value, data } | Any field edited |
name | CustomEvent |