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.
Related
I'm working on a Google Fonts plugin for WordPress and I try to have the same effect as the core WYSIWYG editor. Basically when you click on element (inside the Editor) with font class I want to get the class and then based on that reload the font family/style listbox in the Toolbar.
(I found couple of hacks here on SO like this one Proper Way Of Modifying Toolbar After Init in TinyMCE but nothing that works like the WP core example)
There is the same functionality when you click on P, H1, H3, H3 ... How they do it? Can you point me at least to the JS file in WordPress distro; I think I can figure it out if see the code.
Here is GIF that demonstrates what I'm talking about. Thanks in advance.
I found the solution and because it's not a hack, like the other ones I found on SO, I will post it in here and hopes it will help anyone else that's trying to do something similar.
First to access the button/listbox need to use onpostrender with a callback function.
editor.addButton( 'developry_google_font_family_button', {
type : 'listbox',
onpostrender : fontFamilyNodeChange,
value : '',
...
Next the callback function should look something like this:
function fontFamilyNodeChange() {
var listbox = this;
editor.on('NodeChange', function( e ) {
// This next part is specific for my needs but I will post it as an example.
var selected = [];
if ( $( e.element ).hasClass( 'mce-ga' ) ) { // this a class I add to all elements that have google fonts format
// Then I strip the classes from classList that I don't need and add the rest into an array (e.g ['roboto', '100'])
var gfont_options = $( e.element ).attr('class')
.replace('mce-ga', '')
.replace('mce-family-', '')
.replace('mce-weight-', '')
.trim()
.split(' ');
selected.push( gfont_options );
// At end I add the new value to listbox select[0][0] (e.g. 'roboto')
listbox.value(selected[0][0]);
}
});
}
And here is an example:
I am using CKEditor's editing capabilities, but with my own ui controls that calls into CKEditor's api to perform its commands. E.g.,
var style = new CKEDITOR.style({
element: 'span',
attributes: {
'style': 'font-size: 20px'
}
});
editor.applyStyle(style);
to set the font size of the selected text.
Problem is that I need a way to know the status of the currently selected text so I can update the controls accordingly. Is it bold? Then the bold button should be in an activated state, and clicking it should remove the boldness instead of attempting to add it again.
Is there a way to query CKEditor for certain style attributes of the currently selected text? Much like how tinymce.activeEditor.queryCommandValue('bold') works in tinyMCE.
Usually, the best way to create a button-command-style trio is like it is done in the basicstyles plugin:
var style = new CKEDITOR.style( { ... } );
editor.attachStyleStateChange( style, function( state ) {
!editor.readOnly && editor.getCommand( 'commandName' ).setState( state );
} );
editor.addCommand( 'commandName', new CKEDITOR.styleCommand( style ) );
editor.ui.addButton( 'buttonName', {
label: 'buttonLabel',
command: 'commandName'
} );
This code will take care of everything - the command and the button state will be updated according to selection changes. You can also get the command state easily:
editor.getCommand( 'commandName' ).state; // Returns on of CKEDITOR.TRISTATE_*.
However, if you want to query the state of the style directly, then you can use the style.checkActive() method:
style.checkActive( editor.elementPath(), editor );
You don't need to create command and buttons for this to work.
Edit
The CKEditor styles system has its limitations. For example, you cannot have variable font size in the style. This means that to check the current font size you need to do a quick search in the DOM:
function getFontSize() {
var span = editor.elementPath().contains( isFontSizeSpan );
return span ? span.getStyle( 'font-size' ) : null;
function isFontSizeSpan( el ) {
return el.is( 'span' ) && el.getStyle( 'font-size' );
}
}
Now, just use this method in an editor#selectionChange event listener to update your control's value.
attempting to have my webpage be a bit more dynamic by having the background change on some elements when a checkbox is clicked. I am trying to do this via class change and a CSS sheet. I have the following which is kicking out an error that my onclick function ins not defined (in IE9). More importantly will the webpage update if I only change the class of the object which would have a different class in the CSS file. Whats a better alternative if this does not work?
my elemenet and function
UPDATE
I made updates to both my HTML and CSS file as suggested by many. I am still getting no change in my webpage but the console is claiming that my function called from the onclick event is not defined which is a bit odd since it is. Also does this type for scripting belong in the HTML or should I pull it out and put in a seperate file. I figured since it was creating elements it belongs in the main html. Is there a cleaner more compact way of accomplishing this and not making my home screen html huge?
<tr class= 'tr.notchosen'><td><input type='checkbox' onclick='handleClick(this.id)'/></td></tr>
function handleClick(cb) {
var currentColumn = cb.parentNode
var currentRow = currentColumn.parentNode
if (currentRow.className === "chosen")
{
currentRow.className = "notchosen";
}
else
{
currentRow.className = "chosen";
}
}
and my css file is the following
tr.chosen
{
background-color:rgba(255,223,0,0.75);
}
tr.notchosen
{
background-color:rgba(255,223,0,0);
}
There are a couple of things going on here. First, your css selector is not quite right. In fact, I would suggest making the class name just "chosen" or "not chosen" and then selecting tr elements with that class.
<tr class='notchosen'>
And then you can target it from css (which was probably the original intention)
tr.notchosen
{
background-color:rgba(255,223,0,0);
}
Further, although I would not suggest using inline javascript, using your example, you should pass this if you want to work with the element and not this.id which would pass a string.
onclick='handleClick(this)'
The last part would be to sync up your javascript with the class name change
if (currentRow.className == "chosen")
{
currentRow.className = "notchosen";
}
else
{
currentRow.className = "chosen";
}
I've searched SO and googled for this and there are many answers on how to read CSS propeties but non that allow the reading of a pseudo class property.
I have used the following to allow me to easily reuse certain pages (firmware/config upload) on a number of products.
.productname:before
{
content: "My Shiny New Product";
}
then
<span class="productname" />
in the html to insert the correct name.
Currently when sending a Firmware update to the Server no check is done on the client browser and the server returns [please reboot to contunue...] or [this is not a [productname] update file]. As you can imagine the firmware updates can be quite large and if transfered over 3G take some time.
I want to get the productname from the CSS :before pseudo class to allow me to check the upload file name before send it. I have implemented this JS Fiddle to illustrate the issue.
I have tried putting the content property on the .productname class directly (as a copy placeholder) and FF, Opera and Safari will read this but you guessed it IE returns undefined.
I know I can use a global var in JS and might have to do that but I'd rather have it defined in one place to avoid potential mistakes.
So does anyone know how to (or workaround ) read properies of the :pseudo CSS classes?
Thanks in advance.
Update
Since i cant get a solution for IE8, I've changed to using the following code instead.
window.addEvent( "domready",
function()
{
window.productName = "My Shiny New Product";
var def = '.productname:before { content: "'+window.productName+'"; }';
var style = new Element("style");
style.setAttribute( "type", "text/css" );
if( style.styleSheet )
{
style.styleSheet.cssText = def;
}
else
{
style.innerHTML = def;
}
document.getElementsByTagName("head")[0].appendChild(style);
document.getElementsByTagName("head")[0].appendChild(style);
} );
with reference to this site Dynamic SCRIPT and STYLE elements in IE
you can use window.getComputedStyle. however, an answer notes that some browsers may not support this, so tread lightly. here's a demo
<span class="test" />
<span class="test" />
<span class="test" />
.test:before{
content: "My Shiny New Product";
}
$(function() {
//get all element class test
var test = $('.test');
//each of them, alert the content
test.each(function() {
var style = window.getComputedStyle(this, "before");
alert(style.content);
});
});
It appears as though IE (older versions at least) does not apply CSS that is loaded dynamically. This can be a pain point if you load a page containing CSS via ajax into a "lightbox" or "colorbox".
For example, say your HTML page has a div named "taco":
<style>#taco {color:green;}</style>
<div id="taco">Hola Mundo!</div>
"Hola Mundo!" will be green since the CSS was included in the original HTML page. Then some Javascript happens and appends this to "taco":
<style>#taco {color:green;}</style>
<div id="taco">
Hola Mundo!
<style>#burrito {color:red;}</style>
<span id="burrito">mmmm burrito</span>
</div>
In all browsers except IE, burrito's font will be red.
So is there a way to do this in IE? It seems as though there is not.
The style tag is only allowed in the head section. Placing it somewhere else is simply invalid and that has nothing to do with IE.
More information.
By the way, to solve your problem if you can´t put the styles in a global style-sheet, you can use the 'style' attribute to modify elements:
<p style="...">
Or you can use an iframe but then you'd have to serve a whole page instead of just a few tags.
You might want to start using jQuery's .CSS methed for dynamic style changes like that.
$("#jane").css('color', '#0F0');
Or just plain jane Javascript:
document.getElementById['sally'].style.color = '#0F0';
EDIT:
Have your ajax inject this:
<div id="jane">
<div id="sally">Hi, I'm Sally!</div>
<script>document.getElementById['sally'].style.color = '#0F0';</script>
</div>
Or Why not just inject elements with inline styles computed server side?:
<div id="jane">
<div id="sally" style="color:#0F0">Hi, I'm Sally!</div>
</div>
If there is no way to do this, and you don't want to change your server-side code, here is a way for very simple style elements:
// In the callback function, let's assume you're using jQuery
success: function( data ) {
// Create a dummy DOM element
var el = document.createElement( 'div' );
// Add the html received to this dummy element
el.innerHTML = data;
// so that you can select its html:
var s = $( 'style', el ).text();
// Delegate to another function, it's going to get messy otherwise
addRules( s );
}
function addRules( s ) {
// First, separate your strings for each line
var lines = s.split( '\n' ),
// Declare some temporary variables
id,
rule,
rules;
// Then, loop through each line to handle it
$.each( lines, function() {
id = $( this ).split( ' ' )[ 0 ];
// Get the rules inside the brackets, thanks #Esailija
rules = /\{\s*([^}]*?)\s*\}/.exec( $( this ) )[ 1 ];
// Split the rules
rules = rules.split( ';' );
$.each( rules, function() {
rule = $( this ).split( ':' );
// Apply each rule to the id
$( id ).css( $.trim( rule[ 0 ] ), $.trim( rule[ 1 ] ) );
} );
} );
}
So, yeah, basically I'm making a CSS parser.
This is a very basic parser however.
It will parse the following rules only:
#some-id { some: rule; another: rule; }
#other-id { some: rule; yet: another; rule: baby; }
If you load a linked stylesheet dynamically (via AJAX) into a webpage, IE < 8 does not even recognize the LINK tag.
If you load a SCRIPT tag dynamically IE < 8 will not parse it.
Jeron is correct, the only way to dynamically load HTML and have it styled is via iframe, but I am testing the idea of reflowing the container.