Forms
Add form-associated to a definition and the component becomes a form field through ElementInternals. It takes part in submission and FormData, form.reset(), <fieldset disabled>, validation, labels and back/forward restore, like <input>.
<custom-element name="star-input" form-associated>
…
</custom-element>
<form>
<star-input name="rating" required></star-input>
</form>The name attribute works as on native fields, because the browser reads it from the element.
Values are synced for you
If a form-associated component declares a prop called value, REALM keeps the submitted form value equal to it. If it declares checked, the field submits its value (or "on") only while checked is true, like a checkbox.
On form.reset(), every prop is parsed again from its attribute, so the field returns to the value written in the HTML. When the browser restores form state (back/forward navigation or autofill), value or checked is restored.
Delegating to an inner control
Most fields wrap a native control. Mark it with ref="control" and REALM wires it up:
- typing in the control updates the
valueprop (as a number when the prop is a number), and toggling a checkbox or radio updateschecked, - the control's validity (
required,pattern,min,type="email", …) becomes the component's validity, soform.checkValidity()and:invalidwork, changefrom the control is re-dispatched from the host,- pressing Enter in a text-like control submits the form, like a native input,
- a
<label>around the component, or one pointing at it, names the inner control.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Forms demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="./quantity-input.html"></import-element>
<import-element from="./order-form.html"></import-element>
<order-form></order-form>
</body>
</html><custom-element name="quantity-input" form-associated delegates-focus>
<script>
const props = defineProps({
value: prop.number().int().default(1),
min: prop.number().default(1),
max: prop.number().default(10),
disabled: prop.boolean(),
});
function step(amount) {
props.value = Math.min(props.max, Math.max(props.min, props.value + amount));
}
</script>
<style>
:host {
display: inline-flex;
align-items: center;
gap: calc(var(--spacing) * 1);
}
input {
width: calc(var(--spacing) * 14);
height: calc(var(--spacing) * 8);
border: 1px solid var(--input);
border-radius: var(--radius-md);
background: transparent;
color: inherit;
font: inherit;
text-align: center;
}
input:invalid {
border-color: var(--destructive);
}
</style>
<ui-button size="icon-sm" variant="outline" aria-label="Less" _disabled="@disabled" onclick="step(-1)">-</ui-button>
<input ref="control" type="number" _value="@value" _min="@min" _max="@max" _disabled="@disabled">
<ui-button size="icon-sm" variant="outline" aria-label="More" _disabled="@disabled" onclick="step(1)">+</ui-button>
</custom-element><custom-element name="order-form">
<script>
const result = useState({ text: "Submit to see the form data." });
function submit(event) {
event.preventDefault();
result.text = new URLSearchParams(new FormData(event.target)).toString();
}
function cleared() {
result.text = "Reset.";
}
</script>
<style>
form {
display: grid;
gap: calc(var(--spacing) * 3);
justify-items: start;
}
label {
display: flex;
gap: calc(var(--spacing) * 3);
align-items: center;
}
.actions {
display: flex;
gap: calc(var(--spacing) * 2);
}
output {
font-family: var(--font-mono);
font-size: var(--text-sm);
}
</style>
<form onsubmit="submit(event)" onreset="cleared()">
<label>Coffee <quantity-input name="coffee" value="2"></quantity-input></label>
<label>Tea <quantity-input name="tea" value="1" max="3"></quantity-input></label>
<label>Cake (sold out) <quantity-input name="cake" disabled></quantity-input></label>
<div class="actions">
<ui-button type="submit">Order</ui-button>
<ui-button type="reset" variant="outline">Reset</ui-button>
</div>
<output><slot name="#result.text"></slot></output>
</form>
</custom-element>In the demo:
coffeeandteaare submitted.cakeis not, because it is disabled. Its inner buttons and input followprops.disabled.- Typing a number above
maxmakes the field invalid, and submitting is blocked by native validation. - Reset puts every field back to its
valueattribute.
Disabled fields
Inside a form-associated component, props.disabled is true when the disabled attribute is set or when a surrounding <fieldset disabled> disables the field. Bind it to inner controls (_disabled="@disabled") and both cases work.
Doing it by hand
Without a value or checked prop, or for anything special, use internals directly:
const props = defineProps({ tags: prop.array(prop.string()).default([]), required: prop.boolean() });
observe(() => {
internals.setFormValue(props.tags.join(","));
internals.setValidity(
props.required && props.tags.length === 0 ? { valueMissing: true } : {},
"Add at least one tag.",
);
});internals.form, internals.labels, internals.validity and internals.willValidate are the native properties.
Form lifecycle hooks
| Hook | Runs when |
|---|---|
onFormReset(fn) |
The form is reset. Runs after REALM re-reads the props |
onFormDisabled(fn) |
The field becomes disabled or enabled through a fieldset or its own attribute. fn(disabled) |
onFormStateRestore(fn) |
The browser restores the field's state. fn(state, mode), where mode is "restore" or "autocomplete" |
Submitting with fetch attributes
A <form> with fetch attributes sends its fields, including form-associated components, without a page load:
<form action="/api/orders" method="post" into="#orders.list" into-mode="append" onload="this.reset()">
<quantity-input name="coffee" value="1"></quantity-input>
<ui-button type="submit">Order</ui-button>
</form>REALM UI fields
Every REALM UI input is form-associated: Input, Textarea, Select, Checkbox, Switch, Radio Group, Slider, Input OTP, Combobox, Calendar, Toggle, Toggle Group, and Button for submit and reset.