Search code examples
javascriptformsweb-componentcustom-element

Custom input element in native form


With web components one of the elements that people want to create and override most is <input>. Input elements are bad because they are many things depending on their type and usually hard to customize, so it's normal that people always want to modify their looks and behavior.

Two years ago more or less, when I first heard of web components, I was pretty excited and the first kind of elements that came to my mind that I wanted to create were custom input elements. Now that the spec is finished it looks like the need I had for input elements is not solved. Shadow DOM was supposed to allow me to change their internal structure and looks but input elements are blacklisted and can not have a shadow root because they already have a hidden one. If I want add extra logic and behavior, custom, built-in elements with the is attribute should do the trick; I can't do the shadow DOM magic but at least I have this, right? well Safari is not going to implement it, polymer won't use them for that reason which smells like a standard that is going to be deprecated soon.

So I'm left with normal custom elements; they can use the shadow DOM and have whatever logic I want, but I want them to be inputs! they should work inside a <form>, but if I'm correct, form elements don't like them. Do I have to write my own custom form element as well that replicates all of what the native one does? Do I have to say goodbye to FormData, validation API, etc? Do I lose the ability to have a form with inputs that works without javascript?


Solution

  • You can create a custom element with the look and behavior you want.

    Put inside it a hidden <input> element with the right name (that will be passed to the <form>).

    Update its value attribute whenever the custom element "visible value" is modified.

    I posted an example in this answer to a similar SO question.

    class CI extends HTMLElement 
    {
        constructor ()
        {
            super()
            var sh = this.attachShadow( { mode: 'open' } )
            sh.appendChild( tpl.content.cloneNode( true ) )
        }
    
        connectedCallback ()
        {
            var view = this
            var name = this.getAttribute( 'name' )
    
            //proxy input elemnt
            var input = document.createElement( 'input' )
            input.name = name
            input.value = this.getAttribute( 'value' )
            input.id = 'realInput'
            input.style = 'width:0;height:0;border:none;background:red'
            input.tabIndex = -1
            this.appendChild( input )
    
    
            //content editable
            var content = this.shadowRoot.querySelector( '#content' )
            content.textContent = this.getAttribute( 'value' )
            content.oninput = function ()
            {
                //console.warn( 'content editable changed to', content.textContent )
                view.setAttribute( 'value', content.textContent)
            }
    
            //click on label
            var label = document.querySelector( 'label[for="' + name + '"]' )
            label.onclick = function () { content.focus() }
    
            //autofill update
            input.addEventListener( 'change', function ()
            {
                //console.warn( 'real input changed' )
                view.setAttribute( 'value', this.value )
                content.value = this.value 
            } )
    
            this.connected = true 
        }
    
        attributeChangedCallback ( name, old, value )
        {
            //console.info( 'attribute %s changed to %s', name, value )
            if ( this.connected )
            {
                this.querySelector( '#realInput' ).value = value 
                this.shadowRoot.querySelector( '#content' ).textContent = value 
            }                
        }
    
    }
    CI.observedAttributes = [ "value" ]
    customElements.define( 'custom-input', CI )
    //Submit
    function submitF ()
    {
        for( var i = 0 ; i < this.length ; i++ )
        {
            var input = this[i]
            if ( input.name ) console.log( '%s=%s', input.name, input.value )
        } 
    }
    S1.onclick = function () { submitF.apply(form1) }
    <form id=form1>
        <table>
            <tr><td><label for=name>Name</label>        <td><input name=name id=name>
            <tr><td><label for=address>Address</label>  <td><input name=address id=address>
            <tr><td><label for=city>City</label>        <td><custom-input id=city name=city></custom-input>
            <tr><td><label for=zip>Zip</label>          <td><input name=zip id=zip>
            <tr><td colspan=2><input id=S1 type=button value="Submit">
        </table>
    </form>
    <hr>
    <div>
      <button onclick="document.querySelector('custom-input').setAttribute('value','Paris')">city => Paris</button>
    </div>
    
    <template id=tpl>
      <style>
        #content {
          background: dodgerblue;
          color: white;
          min-width: 50px;
          font-family: Courier New, Courier, monospace;
          font-size: 1.3em;
          font-weight: 600;
          display: inline-block;
          padding: 2px;
        }
      </style>
      <div contenteditable id=content></div>
      <slot></slot>
    </template>