📗Sygnal Forms
Technical Challenges
A form input element rendered inside the code component’s Shadow DOM will not be serialized by the outer HTML <form>
on submit. Forms only submit light-DOM controls or form-associated custom elements (FACE).
Ways to make it work:
Slot the real input (best, native submit)
<!-- In the component's shadow: -->
<slot name="ctrl"></slot>
<!-- In your form (light DOM): -->
<form>
<code-island> <!-- your component host -->
<input type="checkbox" slot="ctrl" name="agree" value="1">
</code-island>
<button type="submit">Send</button>
</form>
The checkbox stays in light DOM → included in submission.
Mirror to a hidden input (when the component must render its own UI)
<form id="f">
<code-island id="agree-toggle"></code-island>
<input type="hidden" name="agree" id="agree-hidden" value="0">
</form>
<script>
// listen for a composed event from the component and mirror the value
document.getElementById('agree-toggle')!.addEventListener('agree-change', (e:any) => {
document.getElementById('agree-hidden')!.setAttribute('value', e.detail.checked ? '1' : '0');
});
</script>
Inside the React component (run in the shadow root), dispatch a composed event on change:
checkbox.addEventListener('input', () => {
const host = (checkbox.getRootNode() as ShadowRoot).host as HTMLElement;
host.dispatchEvent(new CustomEvent('agree-change', { detail:{ checked: checkbox.checked }, bubbles:true, composed:true }));
});
Intercept submit and append programmatically (no hidden input)
const form = document.querySelector('form')!;
form.addEventListener('formdata', (ev) => {
const host = document.getElementById('agree-toggle')!;
const checked = (host.shadowRoot!.querySelector('input[type=checkbox]') as HTMLInputElement).checked;
ev.formData.set('agree', checked ? '1' : '0');
});
Pick 1 if you can control markup; 2 if you want native posts with minimal JS; 3 if you already handle submissions via JS.
Form-Associated Custom Elements (FACE)
Form-associated custom elements (FACE) are custom elements that participate in HTML forms like native inputs.
Chromium supported- not just supported in Safari or Firefox.
Key points
Opt-in:
static formAssociated = true;
thenthis.internals = this.attachInternals()
.Association: the element is owned by the nearest
<form>
or byform="<form id>"
even if placed outside. Access viathis.internals.form
.Submission: give the element a
name
attribute and callthis.internals.setFormValue(value)
whenever its value changes; the value is included in formFormData
.Labels:
this.internals.labels
returns associated<label>
s.Validation: use
this.internals.setValidity(...)
,checkValidity()
,reportValidity()
,willValidate
,validationMessage
.Lifecycle hooks:
formAssociated(form)
,formDisabledCallback(disabled)
,formResetCallback()
,formStateRestoreCallback(state,mode)
.
Minimal example (TypeScript)
class XToggle extends HTMLElement {
static formAssociated = true;
private internals: ElementInternals;
private checked = false;
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<button part="btn" type="button" aria-pressed="false">Off</button>
<style>:host{display:inline-block}</style>`;
this.internals = this.attachInternals();
}
connectedCallback() {
const btn = this.shadowRoot!.querySelector('button')!;
btn.addEventListener('click', () => {
this.checked = !this.checked;
btn.setAttribute('aria-pressed', String(this.checked));
// include in form data under this element's name
this.internals.setFormValue(this.checked ? '1' : '');
// optional validation example
const ok = !this.hasAttribute('required') || this.checked;
this.internals.setValidity(ok ? {} : { valueMissing: true },
ok ? '' : 'Required');
});
}
// Form hooks (optional)
formResetCallback() {
this.checked = false;
this.internals.setFormValue('');
this.internals.setValidity({});
}
}
customElements.define('x-toggle', XToggle);
Usage (native submit)
<form>
<x-toggle name="agree" required></x-toggle>
<button>Submit</button>
</form>
Notes
Must have a
name
to be serialized.Works with the
formdata
and normal form submission flows.Broad support in Chromium-based browsers and Safari; Firefox does not support FACE as of now.
Last updated