Custom input element in native form

拈花ヽ惹草 提交于 2019-11-28 09:45:18
Supersharp

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>

I think @supersharp's answer is the most practical solution for this problem but I'll also answer my self with a less exiting solution. Don't use custom elements to create custom inputs and complain about the spec being flawed.
Other things to do:
Assuming the is attribute is dead from its birth, I think we can achieve similar functionality by just using proxies. Here's an idea that would need some refinement:

class CrazyInput {
  constructor(wowAnActualDependency) { ... }

  doCrazyStuff() { ... }
}

const behavesLike = (elementName, constructor ) => new Proxy(...)

export default behavesLike('input', CrazyInput) 

// use it later
import CrazyInput from '...'

const myCrazyInput = new CrazyInput( awesomeDependency )
myCrazyInput.value = 'whatever'
myCrazyInput.doCrazyStuff()

This just solves the part of creating instances of the custom elements, to use them with the browser APIs some potentially ugly hacking around methods like querySelector,appendChild needs to be done to accept and return the proxied elements and probably use mutation observers and a dependency injection system to automatically create instances of your elements.

On the complaining about the spec side, I still find it a valid option to want something better. For mortals like myself who don't have the whole big picture is a bit difficult to do anything and can naively propose and say things like, hey! instead of having is on native elements let's have it on the custom ones(<my-input is='input'>) so we can have a shadow root and user defined behavior on a custom input that works as a native one. But of course I bet many smart people who have worked on refining those specs all this years have though of all of the use cases and scenarios where something different wouldn't work in this broken Web of ours. But I just hope they will try harder because a use case like this one is something that should have been solved with the web components holy grail and I find it hard to believe that we can't do better.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!