📗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.

Key points

  • Opt-in: static formAssociated = true; then this.internals = this.attachInternals().

  • Association: the element is owned by the nearest <form> or by form="<form id>" even if placed outside. Access via this.internals.form.

  • Submission: give the element a name attribute and call this.internals.setFormValue(value) whenever its value changes; the value is included in form FormData.

  • 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