Custom input element in native form
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>
UPDATE:
Some time has passed and I ran into this post describing form-associated custom elements 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 there chat messages are produced by a form that has a custom element field that is seen as a regular input and sets its value in the form whenever it's changed.
Check the article for more details and perhaps experiment with it creating a PR with a new custom chat message field? ;)
OLD:
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.