LEARNING OBJECTIVES ⌵
- Configure autonomous custom elements to participate as first-class native controls inside HTML
<form>elements. - Master the
ElementInternalsinterface (attachInternals(),setFormValue(),setValidity(),reportValidity()). - Implement the 4 specialized form lifecycle callbacks (
formAssociatedCallback,formDisabledCallback,formResetCallback,formStateRestoreCallback). - Provide seamless native label integration (
internals.labels) and custom constraint validation tooltips.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a country where the native citizens (<input>, <select>, <textarea>, <button>) have full legal rights:
- They can register with the municipal government (the
<form>). - When a census is taken (submitting the form or calling
new FormData(form)), their data is automatically counted. - When an alarm is pulled (
<button type="reset">), they clean up their records. - When they violate local laws, the city bailiff displays an official citation banner (browser validation tooltip).
Historically, autonomous custom elements were like unregistered foreign tourists. Even if you built a gorgeous custom slider or date picker, placing it inside a <form> did nothing. It could not submit its value, it was ignored by new FormData(), clicking a <label for="..."> did nothing, and it couldn't trigger standard HTML5 constraint validation popups. Developers had to hack hidden <input type="hidden"> fields into their components to bridge the gap.
The Form-Associated Custom Elements (FACE) specification and the ElementInternals API grant custom elements full first-class citizenship.
+-----------------------------------------------------------------------------------------------+
| ELEMENT INTERNALS FORM ARCHITECTURE |
| |
| <form id="order-form"> |
| <label for="rating">Product Rating</label> |
| <star-rating id="rating" name="rating" required></star-rating> |
| <button type="submit">Submit</button> |
| </form> |
| |
| class StarRating extends HTMLElement { |
| static formAssociated = true; <-- 1. Declare Form Citizenship |
| |
| constructor() { |
| super(); |
| this._internals = this.attachInternals(); <-- 2. Obtain Internal Gateway |
| } |
| |
| updateValue(val) { |
| this._internals.setFormValue(val); <-- 3. Direct Native Form Submission |
| this._internals.setValidity({ ... }); <-- 4. Native Constraint Validation |
| } |
| |
| formResetCallback() { <-- 5. Native Form Reset Integration |
| this.resetToDefault(); |
| } |
| } |
+-----------------------------------------------------------------------------------------------+
Technical Deep Dive & Specifications
The Form-Associated Declaration
To transform an autonomous custom element into a form control:
- Declare the static property:
static formAssociated = true; - Obtain the internals object:
this._internals = this.attachInternals();inside theconstructor().
⚠️ Calling
attachInternals()withoutstatic formAssociated = truewill throw aNotSupportedErrorDOMException.
Key ElementInternals Methods & Properties
| API Member | Signature | Purpose & Specification Behavior |
|---|---|---|
setFormValue() |
setFormValue(value, state?) |
Sets the value submitted with the form. Can be a string, File, FormData, or null (to omit). |
setValidity() |
setValidity(flags, message, anchor?) |
Configures constraint validation flags (valueMissing, typeMismatch, rangeUnderflow, etc.) and validation message. |
checkValidity() |
checkValidity() |
Returns true if valid, or fires invalid event on element and returns false. |
reportValidity() |
reportValidity() |
Displays the browser's native constraint validation tooltip if invalid. |
form |
readonly form: HTMLFormElement | null |
Returns the enclosing <form> element, or null. |
labels |
readonly labels: NodeList |
Returns all <label> elements associated with this control via for="id". |
validationMessage |
readonly validationMessage: string |
Returns the current localized validation error message. |
The 4 Form Lifecycle Callbacks
Custom elements with static formAssociated = true can implement four specialized lifecycle methods:
1. formAssociatedCallback(form)
-> Invoked when the element is associated with or disassociated from a <form>.
2. formDisabledCallback(disabled)
-> Invoked when the disabled state of the element or an ancestor <fieldset disabled> changes.
3. formResetCallback()
-> Invoked when the enclosing form is reset (e.g. via <button type="reset"> or form.reset()).
4. formStateRestoreCallback(state, mode)
-> Invoked when the browser restores form state (e.g. during Back/Forward cache navigation or autocomplete).
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Line 97:
static formAssociated = trueregisters this element with the browser's form control subsystem. - Line 106:
this._internals = this.attachInternals()creates the privateElementInternalsbridge. - Lines 112–117:
this._internals.setFormValue(val)passes the custom selected hex color directly into the browser form's submission dataset. - Lines 135–138:
formResetCallback()runs when the user clicks<button type="reset">, clearing selected state. - Lines 150–161:
this._internals.setValidity({ valueMissing: true }, '...')connects to the native browser constraint validation engine. If the form is submitted without selecting a color, the browser halts submission and displays a native error bubble. - Line 214:
new FormData(form)captures the value under the name"theme_color"automatically.
Expected Browser Render Output
- Clicking "Submit Form" with no color selected displays the browser's native validation popup pointing directly to the color swatches.
- Selecting a color swatch (e.g. Blue
#3b82f6) and clicking "Submit Form" serializes{ "username": "Ada Lovelace", "theme_color": "#3b82f6" }. - Clicking "Reset Form" clears the selection and restores initial state.
🏋️ Hands-On Exercise
🎯 The Challenge: Build a Form-Associated <pin-code-input>
Instructions:
- Create a custom element
<pin-code-input>withstatic formAssociated = true. - Render 4 numeric inputs (
<input maxlength="1">). When the user types a digit in one box, auto-focus the next box. - Compute the full 4-digit code and call
this._internals.setFormValue(pin). - If
pin.length < 4, callsetValidity({ valueMissing: true }, '4-digit PIN is required'). - Implement
formResetCallback()to clear all 4 input boxes.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Omitting
static formAssociated = true: Callingthis.attachInternals()without this static declaration causes a fatalNotSupportedErrorDOMException. - Calling
attachInternals()Multiple Times:attachInternals()can only be called once per custom element instance in its constructor. Subsequent calls throw an error. - Neglecting
formResetCallback(): If a user clicks a reset button in a<form>, standard inputs reset automatically. If your custom element doesn't implementformResetCallback(), it will remain in an outdated state.
💡 Pro Tips
- Multi-Field Form Submission: You can submit multiple keys from a single custom element by passing a
FormDataobject tosetFormValue():const fd = new FormData(); fd.append('lat', this.latitude); fd.append('lng', this.longitude); this._internals.setFormValue(fd); - Anchor Validation Popups: Pass a specific child element as the 3rd argument to
setValidity(flags, message, anchorElement)to position the browser validation tooltip precisely on the faulty input widget.
📌 Key Takeaways
- Form-Associated Custom Elements (FACE) enable autonomous custom elements to participate natively in
<form>submission and validation. - Components must declare
static formAssociated = trueand instantiatethis.attachInternals(). setFormValue()synchronizes data withnew FormData(form)and HTTP submissions without hidden inputs.setValidity()andreportValidity()trigger standard browser constraint validation popups.- Implement
formResetCallback()andformDisabledCallback()to mirror native form control ergonomics. - --