Form Controls
Every Azalea Kit UI component makes use of a
shadow DOM
to encapsulate markup, styles, and behavior. Historically, native <form> elements did
not recognize form controls located inside a shadow root.
Azalea Kit UI form controls are
form-associated custom elements, so the browser treats them like native form controls: they appear in form.elements, submit
their values through FormData, participate in constraint validation (including
form.reportValidity() and form.checkValidity()), respond to form resets, and
honor the form attribute for associating with a form by id. Things “just work” — including
with form serialization libraries built on FormData.
For privacy reasons, Azalea Kit UI form controls never ask the browser to remember their values for session restore — after a browser restart or back/forward-cache restore, form controls come back empty rather than repopulated. This is deliberate: Azalea products handle protected health information on shared workstations.
Data serialization
If you’re relying on standard form submissions, e.g. <form action="...">, you can
probably skip this section. However, most modern apps use the
Fetch API
or a library such as
axios
to submit forms using JavaScript.
The
FormData
interface offers a standard way to serialize forms in the browser. You can create a
FormData object from any <form> element like this.
const form = document.querySelector('form');
const data = new FormData(form);
// All form control data is available in a FormData object
However, FormData can be tricky to work with or you might need to pass a JSON payload to the
server. To accommodate this, Azalea Kit UI offers a serialization utility that gathers form data and returns
a simple JavaScript object instead.
import { serialize } from '@azaleahealth/azalea-kit-ui/utilities/form.js';
const form = document.querySelector('form');
const data = serialize(form);
// All form control data is available in a plain object
This results in an object with name/value pairs that map to each form control. If more than one form control
shares the same name, the values will be passed as an array, e.g.
{ name: ['value1', 'value2'] }.
Constraint validation
Client-side validation can be enabled through the browser’s
Constraint Validation API
for Azalea Kit UI form controls. You can activate it using attributes such as required,
pattern, minlength, and maxlength. Azalea Kit UI implements many of
the same attributes as native form controls, but check each form control’s documentation for a list of all
supported properties.
If you don’t want to use client-side validation, you can suppress this behavior by adding
novalidate to the surrounding <form> element.
Client-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. You should always validate and sanitize user input on the server!
Required fields
To make a field required, use the required attribute. The form will not be submitted if a
required form control is empty. Required fields will automatically receive a * after their
labels. This is configurable through the --ahi-form-control-required-content custom property.
The form will not be submitted if a required field is incomplete.
<form class="input__required-example">
<ahi-input name="name" label="Name" required></ahi-input>
<br />
<ahi-select label="Gender" clearable required>
<ahi-option value="male">Male</ahi-option>
<ahi-option value="female">Female</ahi-option>
<ahi-option value="other">Other</ahi-option>
<ahi-option value="unknown">Unknown</ahi-option>
</ahi-select>
<br />
<ahi-textarea name="comment" label="Comment" required></ahi-textarea>
<br />
<ahi-checkbox required>Check me before submitting</ahi-checkbox>
<br /><br />
<ahi-button type="submit" variant="primary">Submit</ahi-button>
</form>
<script type="module">
const form = document.querySelector('.input__required-example');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('ahi-button'),
customElements.whenDefined('ahi-checkbox'),
customElements.whenDefined('ahi-input'),
customElements.whenDefined('ahi-option'),
customElements.whenDefined('ahi-select'),
customElements.whenDefined('ahi-textarea')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
});
</script>
Input types
Some input types will automatically trigger constraints, such as email and url.
<form class="input__type-example">
<ahi-input type="email" label="Email" placeholder="you@example.com" required></ahi-input>
<br />
<ahi-input type="url" label="URL" placeholder="https://example.com/" required></ahi-input>
<br />
<div style="display: flex; gap: var(--ahi-space-2)">
<ahi-button type="submit" variant="primary">Submit</ahi-button>
<ahi-button type="reset" variant="default">Reset</ahi-button>
</div>
</form>
<script type="module">
const form = document.querySelector('.input__type-example');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('ahi-button'),
customElements.whenDefined('ahi-input')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
});
</script>
Custom error messages
To create a custom validation error, pass a non-empty string to the setCustomValidity() method.
This will override any existing validation constraints. The form will not be submitted when a custom
validity is set and the browser will show a validation error when the containing form is submitted. To make
the input valid again, call setCustomValidity() again with an empty string.
<form class="input__custom-example">
<ahi-input label="Type “azalea”" required></ahi-input>
<br />
<div style="display: flex; gap: var(--ahi-space-2)">
<ahi-button type="submit" variant="primary">Submit</ahi-button>
<ahi-button type="reset" variant="default">Reset</ahi-button>
</div>
</form>
<script type="module">
const form = document.querySelector('.input__custom-example');
const input = form.querySelector('ahi-input');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('ahi-button'),
customElements.whenDefined('ahi-input')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
input.addEventListener('ahi-input', () => {
if (input.value === 'azalea') {
input.setCustomValidity('');
} else {
input.setCustomValidity("Hey, you're supposed to type 'azalea' before submitting this!");
}
});
});
</script>
Showing your own error message
When your app decides what is wrong with a field — its own validation rules, or a rejection from the server
— set the control’s error property to the message. The control becomes invalid, shows that
message in its error row right away, without waiting for the user to touch the field, and shows it
instead of the browser’s message, so a required field left empty shows one error, in your words and
your language. Clear error to make the control valid again. Use the error slot
when the message needs markup.
ahi-input, ahi-textarea, ahi-select, ahi-combobox,
ahi-input-mask (and so ahi-phone-input, ahi-zip-code-input and
ahi-ssn-input), ahi-memorable-date and ahi-radio-group all support
it.
<ahi-phone-input label="Mobile phone" value="229555" error="Enter all 10 digits, like (229) 555-0142."></ahi-phone-input>
Don’t show the same message in help-text as well: once the browser also considers the field
invalid, the control shows its own error, and the field ends up with two.
Styling validation states
Form controls expose their validation state through
custom states, which you can target in CSS with the
:state()
pseudo-class:
| State | Description |
|---|---|
required |
The control is required. |
optional |
The control is not required. |
valid |
The control currently satisfies its validation constraints. |
invalid |
The control currently fails its validation constraints. |
user-valid |
The control is valid and the user has interacted with it. |
user-invalid |
The control is invalid and the user has interacted with it (or a submit was attempted). |
Azalea Kit UI’s built-in error styles key off user-invalid, so errors only appear after the
user has interacted with a control or tried to submit the form — never while they’re still filling it in.
Use the same state for custom styling:
ahi-input:state(user-invalid)::part(input-wrapper) {
outline: 2px solid var(--ahi-border-critical);
}
Getting associated form controls
Because Azalea Kit UI form controls are form-associated custom elements, they appear in HTMLFormElement.elements alongside native controls, in DOM order — no special API is needed:
const form = document.querySelector('#my-form');
console.log([...form.elements]); // e.g. [input, ahi-input, ...]
The getFormControls() utility remains available for backwards compatibility and returns the
same controls as an array:
import { getFormControls } from '@azaleahealth/azalea-kit-ui/utilities/form.js';
const formControls = getFormControls(document.querySelector('#my-form'));
Browser autofill & PHI
Azalea Kit UI form controls forward the standard
autocomplete
attribute to their underlying native input. Because Azalea products run on shared clinical workstations, be
deliberate about which fields the browser (or a password manager) is allowed to remember and pre-fill.
Guidelines for handling protected health information (PHI):
-
Fields holding PHI should not be autofilled. Set
autocomplete="off"on controls that collect identifiers or sensitive values you don’t want cached in the browser profile. Note that browsers and password managers may ignoreofffor credential-like fields, so treat it as a strong hint rather than a guarantee, and rely on your workstation/browser policy as the real control. -
<ahi-ssn-input>defaults toautocomplete="off". Social Security Numbers are never autofilled unless a consumer explicitly overrides the attribute for a specific, reviewed reason. -
Opt in to autofill only where it helps the patient, not the clinician. Patient-facing
self-entry flows (for example, a patient completing their own registration) benefit from autofill of their
own details; the same fields inside a clinician charting on many patients should generally stay
off. - Enterprise policy is the backstop. Deployments should pair these attribute defaults with browser/OS policies that disable form and password saving on shared machines.