原生形式的自定义输入元素

Custom input element in native form

对于 Web 组件,人们最想创建和覆盖的元素之一是 <input>。输入元素不好,因为它们有很多东西取决于它们的类型并且通常很难定制,所以人们总是想修改它们的外观和行为是正常的。

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

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

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

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

每当修改自定义元素 "visible value" 时更新其 value 属性。

我在 中发布了一个示例。

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>

更新: 一段时间过去了,我 运行 进入这个 post 描述与表单相关的自定义元素 https://web.dev/more-capable-form-controls, it seems there will finally be an appropriate way to create custom elements that can be used as form controls, no need to wrap inputs or be limited by the bad support and inability of having a shadow DOM in built-in custom elements. I created a toy component to play with latest APIs(chrome only 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 等方法进行一些潜在的丑陋黑客攻击才能接受和return 代理元素并可能使用变异观察器和依赖注入系统来自动创建元素的实例。

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