How to polyfill RadioNodeList? - javascript

Background
As per the WHATWG someForm.elements should return a HTMLFormElementsCollection.
A HTMLFormElementsCollection returns a RadioNodeList if multiple elements share the same name.
The RadioNodeList has special value semantics where it returns the value of the first checked radio list in the nodelist.
This would allow the following answer to work if it were implemented
I naively attempted a polyfill that is based on host objects being well behaved (as per WebIDL), which they are clearly not.
Question
What is an alternative efficient implementation for this polyfill whilst we wait for browsers to become either RadioNodeList or WebIDL compliant?
Example Reference code
<form name="myform">
<input type="radio" name="foo" value="10" /> foo
<input type="radio" name="foo" value="30" /> bar
</form>
var myform = document.forms.myform;
var radio = myform.elements.foo;
var price = radio.value;
Naive attempt reference code
(function () {
var pd = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, "elements"),
getElements = pd.get;
pd.get = get;
Object.defineProperty(HTMLFormElement.prototype, "elements", pd);
function get() {
var elements = getElements.call(this);
if (elements.length) {
Object.defineProperty(elements, "value", {
get: getRadioNodeListValue,
set: setRadioNodeListValue,
configurable: true
});
}
return elements;
}
function getRadioNodeListValue() {
for (var i = 0, len = this.length; i < len; i++) {
var el = this[i];
if (el.checked) {
return el.value;
}
}
}
function setRadioNodeListValue(value) {
for (var i = 0, len = this.length; i < len; i++) {
var el = this[i];
if (el.checked) {
el.value = value;
return;
}
}
}
}());

If you can accept bolting the value getter onto NodeList then the following should work
RadioNodeList polyfill
Credit to ##Esailija
you could also just add .value to NodeList prototype

Why? You know that requiring ES5 and WebIDL will fail in common-use browsers on the web today. Your code requires new features, both in ES5 )Object.getOwnPropertyDescriptor, get/set), and as you mentioned WebIDL interface objects being well-behaved.
The HTML5 polyfill aims at generalization and generally fails at implementation. It is a harmful trend.
Don't modify other objects. Especially host objects.
If there is any variance from the HTML5 definition (and there is) there will be problems when a second feature-testing script tries to detect if there is a value property on a group of radios, and then assumes standard support.It's way off from the 2-step algorithm in HTML5.
Your code polyfills only if elements.length > 0 and affects checkboxes, too (keep in mind that radios aren't the only element with a checked property). Your setter changes the value of the first checked radio. Shouldn't setting the value check the first unchecked radio of that name, having that value?
Instead, write functions that are only as general as you need them to be.
function getRadioValue(form, radioName) {
// Delete comment, write code.
}
function setRadioValue(form, radioName, value) {
// Delete comment, write code.
}

Related

Setting up a property inside function and call it back after Element get

In the example below, I have a function called color(). Is it possible to make it work as I would expect?
<div> text text </div>
<script>
function color(property) {
return this.style.color = property;
}
document.getElementsByTagName("div").color("red");
</script>
The code above gives me the error:
color(...) is not a function
There are two problems and an issue with that code:
getElementsByTagName returns a collection of elements, not just a single element. More here.
There's nothing that attaches the function color to an element or collection of elements.
To use this as the element, you'll need to make sure color is called in a special way.
Also note there's no need for the return in color.
If you want to apply the color function to all matching elements, you'll need a loop:
function color(property) {
this.style.color = property;
}
var list = document.getElementsByTagName("div");
for (var i = 0; i < list.length; ++i) {
color.call(list[i], "red");
}
<div> text text </div>
But if you don't use this, the call is clearer:
function color(el, property) {
el.style.color = property;
}
var list = document.getElementsByTagName("div");
for (var i = 0; i < list.length; ++i) {
color(list[i], "red");
}
<div> text text </div>
Note: You could add a property to the HTMLElement prototype (or even NodeList or HTMLCollection) so you could call color as a method, as you did in your question. That's probably not a good idea; if everyone did it, it would quickly lead to conflicts. Generally it's better to only modify the built-in DOM prototypes when applying a polyfill, but not for custom functions.

JavaScript function that return with different types (Array vs NodeList)

I wrote a JavaScript function that will return a "list" of elements that has Id that start with some value:
function getElementsWithIdPrefix(prefix){
if (document.querySelectorAll){
return document.querySelectorAll('*[id^="' + prefix + '"]');
} else {
// none modern browsers support
var elements = document.getElementsByTagName('*');
var relevantElements = [];
for (var i = 0; i < elements.length; i++){
if (element.id && element.id.indexOf(prefix) !== -1){
relevantElements.push(element);
}
}
return relevantElements;
}
}
As you can see my function will return different types depends on browser support for document.querySelectorAll and I have two question regarding this:
Is this bad? I mean - In JavaScript typing system does it consider as a code smell or bad practice?
How can I create Node objects for each Element by my self and construct a new NodeList containing these elements to return?
Is this bad?
Probably, but it's probably a matter of opinion.
How can I create Node objects for each Element by my self and construct a new NodeList containing these elements to return?
You can turn your QSA result into a true array like this:
return Array.prototype.slice.call(document.querySelectorAll('*[id^="' + prefix + '"]'));
That way, you're returning an array in both cases.
Side note: You have to look pretty hard to find a browser that doesn't have QSA. It's on all modern browsers, and also IE8.
Side note 2: This line
if (element.id && element.id.indexOf(prefix) !== -1){
...doesn't look for a prefix. It looks for a substring match anywhere in the ID. For a prefix, use === 0 (or element.id.substring(0, prefix.length) === prefix).

If = one class, but has two classes

I have a validation script that validates two different classes. I want to add an additional class for some select boxes that generate an array onmouseover. Problem is, the select boxes have two classes, one for the validation, one for the array, and my script doesn't recognize it unless it has the exact name. If using the == it doesn't work at all, but using the = it tries to validate all the elements on my page, even those without any classes.
I'm wondering how I can make this the most efficient without breaking the rest of my pages using this script.
This is an example of the HTML
<div class="select-styled">
<select id="pm" name="pm" class="selValidate pmpop" onmouseover="pmpop();">
<option value="Foo">Foo</option>
<option value="">- Remove -</option>
</select>
</div>
Here is the validation script
for (i=0; i<thisform.elements.length; i++) {
if (thisform.elements[i].className = "selValidate" || thisform.elements[i].className == "req" || thisform.elements[i].className == "valfocus") {
//do stuff
}
}
Why don't you use match()?
if (thisform.elements[i].className.match('selValidate')) {
// do stuff
}
You can't use the assignment operator in an if because it will do that...assign. The == is a comparison operator. But you seem to want to check if one css class is set for an element that has many..
So match() would work better as it searches for the occurrence of a string within a string, rather than comparing two string literals to see if they're equal.
Also, match() can accept regular expressions, which can be powerful. :)
EDIT:
Per Barney's comment, I recommend using a regular expression (as I mentioned earlier) to avoid false matches where you might have a similar class name (e.g. selValidate and selValidateOff or something like that).
if (thisform.elements[i].className.match(/\bselValidate\b/)) {
// now, really do stuff
}
Both are good, really. I just depends on how narrow you want your search to be.
Here is a nice cheat sheet for getting started with regular expressions in JavaScript.
The problem is that with multiple classes, the className property lists each class separated by a space. So markup like:
<select id="pm" name="pm" class="selValidate pmpop" onmouseover="pmpop();">
has a className equal to "selValidate pmpop".
To use JavaScript to find an individual class using className, you either need to use the String .match method to test against a regex that will find the class in the string, or you need to break the string up into searchable tokens.
Example of .match:
for (i = 0; i < thisform.elements.length; i += 1) {
if (thisform.elements[i].className.match(/\bselValidate\b|\breq\b|\bvalfocus\b/g) { // relies on .match returning null for no match
//do stuff
}
}
Example of breaking the string into searchable tokens:
for (i = 0; i < thisform.elements.length; i += 1) {
var allClasses = thisform.elements[i].className.split(' ');
if (allClasses.indexOf('selValidate') > -1 || allClasses.indexOf('req') > -1 || allClasses.indexOf('valfocus') > -1) { // relies on ECMAScript 5 Array.indexOf support
//do stuff
}
}
// or
for (i = 0; i < thisform.elements.length; i += 1) {
var allClasses = thisform.elements[i].className.split(' ');
for (var j = 0; j < allClasses.length; j += 1) {
if (allClasses[j] == 'selValidate' || allClasses[j] == 'req' || allClasses[j] == 'valfocus') { // should work with any browser that supports JavaScript
//do stuff
}
}
}
You could also use something like .classList, but I don't know what browsers you need to target.
Example of .classList:
for (i = 0; i < thisform.elements.length; i += 1) {
if (thisform.elements[i].classList.contains('selValidate') || thisform.elements[i].classList.contains('req') || thisform.elements[i].classList.contains('valfocus')) { // relies on ECMAScript 5 Array.indexOf support
//do stuff
}
}
Also, only use = when you want to assign a value to the lefthand operator (that's what it's for).
== is a type-coercive equality (or "equal-ish") test, and === is a strict (non-coercive) equality test.
The DOM method for this is classList. If you only care about IE 10+, it's your best bet:
thisform.elements[ i ].classList.contains( 'selValidate' );
If you need to support older browsers, use RegExp and className:
thisform.elements[ i ].className.match( /\bselValidate\b/ );
EDIT: after running a few tests on jsperf, it appears that classList is often much faster (the only browser that this isn't the case for is IE11). For this reason – and because classList.contains is an API designed to do precisely the task in question (whereas className.match is a kludge) – I'd recommend using the classList method if you can afford to ditch IE9 and below.
If you want to reap the speed benefits where possible but still support all browsers, here's a polyfill:
function hasClass( element, className ){
if( element.classList && element.classList.contains ){
hasClass = function classList_contains( element, className ){
return element.classList.contains( className );
}
}
else {
hasClass = function className_match( element, className ){
return new RegExp( '\\b' + className + '\\b' ).test( element.className );
}
}
return hasClass( element, className );
}
If you are using jQuery, check for class existence instead of class-name comparison using
$(thisform.elements[i]).hasClass('selValidate');
If you want Javascript only solution the use this library classie which helps to check the existence of a class using hasClass() method.

Wildcards in HTML5 data attributes

Is it possible to find all DOM elements with jQuery with wildcards in the attribute name?
Consider the following HTML:
<input
id="val1"
type="text"
data-validate-required
data-validate-minlength="3"
data-validate-email />
What I am trying to achieve is to find all dom nodes with an attribute name starting with data-validate-
As far as I understand the wildcards described here are concerned with "value" of the attribute.
The reason for this is - I want to find out which elements should be validated at all - and afterwards find out which validation parameters (like -email) comes in play.
Thanks
You can create a custom pseudoclass to e.g. match attribute names against a regexp: http://jsfiddle.net/hN6vx/.
jQuery.expr.pseudos.attr = $.expr.createPseudo(function(arg) {
var regexp = new RegExp(arg);
return function(elem) {
for(var i = 0; i < elem.attributes.length; i++) {
var attr = elem.attributes[i];
if(regexp.test(attr.name)) {
return true;
}
}
return false;
};
});
Usage:
$(":attr('^data-')")
Because JQuery relies heavily on XPath, and XPath does not support wildcard attribute selection - it is not possible without the overhead you're looking to avoid.
There's always the possibility of creating your own selector, just to keep things clean:
//adds the :dataValidate selector
$.extend($.expr[':'],{
dataValidate: function(obj){
var i,dataAttrs=$(obj).data()
for (i in dataAttrs) {
if (i.substr(0,8)=='validate') return true;
}
return false;
}
})
Which will allow you to use :dataValidate in your normal jQuery selectors:
$(".element:dataValidate .etc")
Working JSFiddle: http://jsfiddle.net/rZXZ3/
You could loop over the atributes:
$('.element').each(function() {
$.each(this.attributes, function(i, att){
if(att.name.indexOf('data-validate')==0){
console.log(att.name);
}
});
});
You can use filter method and dataset object:
Allows access, both in reading and writing mode, to all the custom data attributes (data-*) set on the element. It is a map of DOMString, one entry for each custom data attribute.
$("input").filter(function(){
var state = false;
for (i in this.dataset)
if (i.indexOf('validate') > -1) state = true;
return state
})​
http://jsfiddle.net/Pxpfa/

Javascript equivalent of $('input[name$="value"]') in jQuery?

How can I get an input element whose name attribute ends with a particular value using just javascript?
I found this $('input[name$="value"]') method available in jQuery.
Is there a function like that available in javascript? I cannot use jQuery for some reason.
I'm surprised that nobody has mentioned this yet. Have you tried:
var elements = document.querySelectorAll('input[name$="value"]');
This will only work if the browser supports the querySelectorAll method but it is what jQuery uses underneath if support is found.
Here's the table that lists browser support for the method:
When can I use querySelector/querySelectorAll?
You can do this:
console.log(document.getElementsByName("value")[0]);
Or if you want to find the first input element with "value" in its name:
var searchString = "value";
var elems = document.getElementsByTagName('input'), wantedElem;
for(var i = 0, len = elems.length; i < len; i++) {
if(elems[i].name.lastIndexOf(searchString) == elems[i].name.length-searchString.length) {
wantedElem = elems[i];
break;
}
}
console.log(wantedElem);
I'm going to assume that you've placed these <input> elements where they belong (in a <form> element).
The easiest way to traverse form controls via the DOM API is via the HTMLFormElement::elements HTMLCollection. It allows named traversal, which is very handy for specific elements.
For example, consider the following markup:
<form action="./" method="post" name="test"
onsubmit="return false;">
<fieldset>
<legend>Test Controls</legend>
<input name="control_one" type="text"
value="One">
<input name="control_two" type="text"
value="Two">
<input name="control_three" type="text"
value="Three">
</fieldset>
</form>
Following that, some simple document tree traversal is required. Here's the entirety of it:
var test = document.forms.test,
controls = test.elements;
function traverseControl(control)
{
var patt = /one$/,
match = patt.test(control.name);
if (match) {
// do something
console.log(control);
}
}
function traverseControls(nodes)
{
var index;
if (typeof nodes === "object" &&
nodes.length) {
index = nodes.length - 1;
while (index > -1) {
traverseControl(
nodes[index]
);
index -= 1;
}
}
}
traverseControls(controls);
As you can see, it really isn't too difficult. The upshot of using HTMLCollections is the support of browsers old and new. Since HTMLCollections were implemented in DOM 0, they're widely supported.
In the future, I'd suggest using traversal that's far less vague. If you're in control of the document tree being traversed (i.e. you wrote the markup), you should already know the names of controls. Otherwise, vague approaches like the preceding must be used.
Working example: http://jsbin.com/epusow
For more, this article can be perused.
This method returns an array of elements
var elements = document.getElementsByName("value");

Categories

Resources