Search code examples
javascriptweb-componentform-data

how to extract data on form submission from custom web component?


I need to understand how form submission could extract data from custom web component?

I tried out the following steps

1- appending input to the ShadoWroot
2- using the concept of form association I connected input value and name with the form
3- finally I submit the form to get FormData.

unfortunately, it's not successful. I get nothing from the FormData

// https://web.dev/more-capable-form-controls/
// https://webkit.org/blog/13711/elementinternals-and-form-associated-custom-elements/
// https://html.spec.whatwg.org/#custom-elements

let form = document.getElementById('frm');

// on form submit I need to recive form data from the input by getting name and its value
form.addEventListener('submit', (event) => {
    event.preventDefault();
    var formData = new FormData(form);
    // Display the key/value pairs
    for (const pair of formData.entries()) {
      console.log(`${pair[0]}, ${pair[1]}`);
    }

});

// I need here to assign value to form data using input name and input value
class SomeTextFieldElement extends HTMLElement {
    static formAssociated = true;
    internals;
    shadowRoot;
    constructor()
    {
        super();
        this.internals = this.attachInternals();
        this.shadowRoot = this.attachShadow({mode: 'closed', delegatesFocus: true});
        this.shadowRoot.innerHTML = "<input name='test' autofocus>";
        const input = this.shadowRoot.querySelector('input');
        input.addEventListener('change', () => {
            this.internals.setFormValue(input.value, input.value);
        });
    }
    formStateRestoreCallback(state, reason)
    {
        this.shadowRoot.querySelector('input').value = state;
    }
}
customElements.define('some-text-field', SomeTextFieldElement);
<form id='frm'>
  <some-text-field></some-text-field>
  <button type="submit">Submit</button>
</form>


Solution

  • The link that @Danny posted in the comments has the right answer. (Thanks for posting that.) The following code snippet, therefore, is not my code but code from the sample in the linked web.dev article. I've incorporated it into your code sample from your original post.

    I commented out some functions that do not affect insertion of the custom component's input value into form's data so it's clear what functions are participating in the form data insertion process.

    let form = document.getElementById('frm');
    
    // On form submit I need to receive form data from 
    // the input by getting name and its value.
    form.addEventListener('submit', (event) => {
      event.preventDefault();
      var formData = new FormData(form);
      // Display the key/value pairs
      for (const pair of formData.entries()) {
        console.log(`${pair[0]}, ${pair[1]}`);
      }
    });
    
    // I need here to assign value to form data using input name and input value
    class SomeTextFieldElement extends HTMLElement {
      // [The following function is listed in the web.dev article.
      // It's not needed to add data to the form on form
      // submission but it's kept here to be uncommented.]
    
      // Useful values for <input>
      //static get observedAttributes() {
      //    return ['name', 'disabled', 'placeholder', 'value'];
      //}
    
      static formAssociated = true;
      internals;
      shadowRoot;
      constructor() {
        super();
        this.internals = this.attachInternals();
        this.shadowRoot = this.attachShadow({
          mode: 'closed',
          delegatesFocus: true
        });
        this.shadowRoot.innerHTML = "<input name='test' autofocus>";
        this.input = this.shadowRoot.querySelector('input');
    
        // Your original code. The 'change' event is not necessary
        // as the value of the 'input' element (named 'test')
        // is added to the form's data by the function 
        // 'handleFormData()' below.
        //const input = this.shadowRoot.querySelector('input');
        //input.addEventListener('change', () => {
        //    this.internals.setFormValue(input.value, input.value);
        //});
    
        // Keep reference to <form> for cleanup
        this._form = null;
        this._handleFormData = this.handleFormData.bind(this);
      }
    
      // FormData event is sent on <form> submission, 
      // so we can modify the data before transmission.
      // It has a .formData property, and that's all we need.
      handleFormData({
        formData
      }) {
        // Add our name and value to the form's submission 
        // data if we're not disabled.
        if (!this.input.disabled) {
          // https://developer.mozilla.org/en-US/docs/Web/API/FormData
          formData.append(this.input.name, this.input.value);
        }
      }
    
      // [The following function is listed in the web.dev article.
      // It's not needed to add data to the form on form
      // submission but it's kept here to be uncommented.]
    
      // Sync observed attributes to <input>
      //attributeChangedCallback(name, oldValue, newValue) {
      //    const value = name === 'disabled' ? this.hasAttribute('disabled') : newValue;
      //    this.input[name] = value;
      //}
    
      // Find the <form>, and attach the `formdata` listener.
      // This function is called when the following line is
      // executed by the browser.
      // customElements.define('some-text-field', SomeTextFieldElement);
      connectedCallback() {
        this._form = this.findContainingForm();
        if (this._form) {
          this._form.addEventListener('formdata', this._handleFormData);
        }
      }
    
      // [The following function is listed in the web.dev article.
      // It's not needed to add data to the form on form
      // submission but it's kept here to be uncommented.]
    
      // Remove the `formdata` listener if we're removed
      //disconnectedCallback() {
      //    if (this._form) {
      //        this._form.removeEventListener('formdata', this._handleFormData);
      //        this._form = null;
      //    }
      //}
    
      // Find the <form> we are contained in
      findContainingForm() {
        // Can only be in a form in the same "scope", ShadowRoot or Document
        const root = this.getRootNode();
        const forms = Array.from(root.querySelectorAll('form'));
        // We can only be in one <form>, so the first
        // one to contain us is the correct one.
        return forms.find((form) => form.contains(this)) || null;
      }
    }
    
    customElements.define('some-text-field', SomeTextFieldElement);
    <form id='frm'>
      <label>Label</label>
      <some-text-field></some-text-field>
      <input type="text" name="normalInput" value="Normal input" />
      <button type="submit">Submit</button>
    </form>