With web components one of the elements that people want to create and override most is <input>
. Input elements are bad because they are many things depending on their type and usually hard to customize, so it's normal that people always want to modify their looks and behavior.
Two years ago more or less, when I first heard of web components, I was pretty excited and the first kind of elements that came to my mind that I wanted to create were custom input elements. Now that the spec is finished it looks like the need I had for input elements is not solved. Shadow DOM was supposed to allow me to change their internal structure and looks but input elements are blacklisted and can not have a shadow root because they already have a hidden one. If I want add extra logic and behavior, custom, built-in elements with the is
attribute should do the trick; I can't do the shadow DOM magic but at least I have this, right? well Safari is not going to implement it, polymer won't use them for that reason which smells like a standard that is going to be deprecated soon.
So I'm left with normal custom elements; they can use the shadow DOM and have whatever logic I want, but I want them to be inputs! they should work inside a <form>
, but if I'm correct, form elements don't like them. Do I have to write my own custom form element as well that replicates all of what the native one does? Do I have to say goodbye to FormData
, validation API, etc? Do I lose the ability to have a form with inputs that works without javascript?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With