【问题标题】:Custom input element in native form本机形式的自定义输入元素
【发布时间】:2017-03-03 11:07:16
【问题描述】:

对于 Web 组件,人们最想创建和覆盖的元素之一是 <input>。输入元素很糟糕,因为它们的类型很多,而且通常很难自定义,所以人们总是想修改他们的外观和行为是正常的。

大约两年前,当我第一次听说 Web 组件时,我非常兴奋,我想到的第一种元素就是自定义输入元素。现在规范已经完成,看起来我对输入元素的需求没有得到解决。 Shadow DOM 应该允许我更改它们的内部结构和外观,但输入元素被列入黑名单并且不能有影子根,因为它们已经有一个隐藏的根。如果我想添加额外的逻辑和行为,具有is 属性的自定义内置元素应该可以解决问题;我不能做影子 DOM 魔法,但至少我有这个,对吧?好吧 Safari 不会实施它,聚合物不会因为这个原因使用它们,这听起来像是一个很快就会被弃用的标准。

所以我只剩下普通的自定义元素了;他们可以使用 shadow DOM 并拥有我想要的任何逻辑,但我希望它们成为输入!它们应该在<form> 中工作,但如果我是正确的,表单元素不喜欢它们。我是否还必须编写自己的自定义表单元素来复制本地元素的所有功能?我必须告别FormData、验证API 等吗?我是否失去了在没有 javascript 的情况下使用输入的表单的能力?

【问题讨论】:

    标签: javascript forms web-component custom-element


    【解决方案1】:

    您可以创建具有所需外观和行为的自定义元素。

    在其中放入一个隐藏的<input> 元素,右边是name(将传递给<form>)。

    每当自定义元素“可见值”被修改时,更新其value 属性。

    我在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>

    【讨论】:

    • 还不错,感觉有点hacky,并不能解决所有问题,但我有点喜欢;)
    • - 它仍然需要 JS 来保持同步并实际做一些事情。我知道我不应该太在意 1% 没有或禁用 JS 的用户,但仍然是原生输入可以做的事情。 - 额外的标记和一些冗余的语义。 - 在自定义元素中包含输入只是一种声明性方式,它执行我们多年来一直在做的相同包装/代理模式,并且应该通过自定义元素来解决。所以自定义输入元素变成了一些无用的声明性包装噪音。为此,我宁愿只使用带有代理的 JS。
    • 你没有错。它仍然是封装在自定义元素中的代理。我发布了一个例子。它适用于 Chrome 的自动填充和外部更新。它还不完美...感谢您的支持:-)
    【解决方案2】:

    更新: 一段时间过去了,我遇到了这篇描述表单相关自定义元素https://web.dev/more-capable-form-controls的帖子,似乎最终会有一种合适的方法来创建可以用作表单控件的自定义元素,无需包装输入或被限制内置自定义元素中的阴影 DOM 的不良支持和无法实现。我创建了一个玩具组件来使用最新的 API(仅限 Chrome ATM)https://github.com/olanod/do-chat 聊天消息由一个表单生成,该表单具有一个自定义元素字段,该字段被视为常规输入,并在表单更改时在表单中设置其值。
    查看文章以获取更多详细信息,或者尝试使用新的自定义聊天消息字段创建 PR? ;)

    旧: 我认为@supersharp 的答案是解决这个问题的最实用的解决方案,但我也会用一个不那么令人兴奋的解决方案来回答我自己。 不要使用自定义元素来创建自定义输入并抱怨规范存在缺陷。
    其他要做的事情:
    假设is 属性一出生就死了,我认为我们可以通过使用代理来实现类似的功能。这是一个需要改进的想法:

    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()
    

    这只是解决了创建自定义元素实例的部分,为了将它们与浏览器 API 一起使用,需要对诸如 querySelectorappendChild 之类的方法进行一些潜在的丑陋黑客攻击,以接受和返回代理元素,并且可能使用突变观察者和依赖注入系统来自动创建元素的实例。

    在抱怨规范方面,我仍然认为想要更好的东西是一个有效的选择。对于像我这样没有大局的凡人来说,做任何事情都有些困难,并且可以天真地提出和说诸如,嘿!而不是在原生元素上使用is,让我们在自定义元素上使用它(&lt;my-input is='input'&gt;),这样我们就可以在作为原生元素的自定义输入上拥有影子根和用户定义的行为。但是,我敢打赌,多年来一直致力于完善这些规范的许多聪明人都知道在我们这个破碎的网络中无法使用不同的用例和场景的所有用例和场景。但我只是希望他们能更加努力,因为像这样的用例本应通过 Web 组件的圣杯来解决,我很难相信我们不能做得更好。

    【讨论】:

    猜你喜欢
    • 2019-09-23
    • 2020-07-02
    • 2018-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多