Пользовательский элемент ввода в нативной форме

С веб-компонентами один из элементов, которые люди хотят создавать и переопределять, - это <input>. Элементы ввода плохи, потому что их много, в зависимости от их типа и обычно трудно настраивать, поэтому нормально, что люди всегда хотят изменить внешний вид и поведение.

Два года назад более или менее, когда я впервые услышал о веб-компонентах, я был очень взволнован, и первые элементы, которые мне придумали, я хотел создать, были пользовательскими элементами ввода. Теперь, когда спецификация закончена, похоже, что необходимость в элементах ввода не решена. Shadow DOM должен был позволить мне изменить внутреннюю структуру и внешний вид, но элементы ввода занесены в черный список и не могут иметь теневой корень, потому что у них уже есть скрытый. Если я хочу добавить дополнительную логику и поведение, пользовательские, встроенные элементы с атрибутом is должны делать трюк; Я не могу сделать тёмную магию DOM, но, по крайней мере, у меня есть это, не так ли? хорошо Safari не собирается его реализовывать, полимер не будет использовать их по той причине, которая пахнет стандартом, который скоро будет устаревшим.

Итак, у меня остались обычные пользовательские элементы; они могут использовать теневую DOM и иметь любую логику, которую я хочу, но я хочу, чтобы они были входами! они должны работать внутри <form>, но если я прав, элементы формы им не нравятся. Должен ли я написать собственный собственный элемент формы, который реплицирует все то, что делает родной? Должен ли я попрощаться с FormData, API проверки и т.д.? Я теряю возможность иметь форму с входами, которые работают без javascript?

Ответы

Ответ 1

Вы можете создать пользовательский элемент с желаемым внешним видом и поведением.

Поместите внутри него скрытый элемент <input> с правом name (который будет передан <form>).

Обновите свой атрибут value всякий раз, когда изменен пользовательский элемент "видимое значение".

Я отправил пример в этот ответ на аналогичный вопрос SO.

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>

Ответ 2

Я думаю, что ответ @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-интерфейсами браузера. Некоторый потенциально уродливый взлом методов, таких как querySelector, appendChild, должен быть выполнен, чтобы принимать и возвращать проксированные элементы и, возможно, использовать наблюдатели мутаций и систему впрыскивания зависимостей для автоматического создания экземпляров ваших элементов.

На жалобы на спецификацию я все же считаю это правильным вариантом, чтобы что-то лучше. Для смертных, таких как я, у которых нет всей большой картины, немного сложно что-либо сделать и может наивно предлагать и говорить такие вещи, как, эй! вместо is на нативных элементах пусть это будет на пользовательских (<my-input is='input'>), поэтому мы можем иметь теневой корень и пользовательское поведение на пользовательском входе, который работает как собственный. Но, конечно, я сделал ставку на многих умных людей, которые работали над обновлением этих спецификаций все эти годы, хотя из всех случаев использования и сценариев, где что-то другое не будет работать в этой сломанной нашей сети. Но я просто надеюсь, что они будут пытаться усерднее, потому что пример использования, подобный этому, - это то, что нужно было решить с помощью веб-компонентов святого Грааля, и мне трудно поверить, что мы не можем сделать лучше.