querySelectorAll only selecting second element when using this.element - javascript

I've recently started learning about JavaScript objects, and how frameworks such as jQuery, and Modernizr work.
I have tried to create my own little "framework", to further learn how JavaScript objects work, and how to utilize them to their full potential.
It's all gone pretty smoothly so far, until I tried to set a global variable using the querySelectorAll() method (and a for loop), to grab multiple elements with the specified selector.
With this, I intended to add or remove a class from each of those elements with that specific selector. However, it only ever worked on the very last element of the bunch.
Here is my (relevant) JavaScript:
var aj = function(sr){
this.selector = sr || null; // set global selector variable
this.element = null;
}
aj.prototype.init = function(){
switch(this.selector[0]){
// first, second, third case e.t.c...
default:
var els = document.querySelectorAll(this.selector); // select all elements with specified selector (set above)
for(var i = 0; i < els.length; i++){
this.element = els[i];
}
}
};
aj.prototype.class = function(type, classes){
if(type === "add"){ // if the user wants to add a class
if((" " + this.element.className + " ").indexOf(" " + classes + " ") < 0){
this.element.className += " " + classes;
}
} else if(type === "remove") { // if the want to remove a class
var regex = new RegExp("(^| )" + classes + "($| )", "g");
this.element.className = this.element.className.replace(regex, " ");
}
};
Example:
<div class="example-class">Example</div>
<div class="example-class">Example 2</div> <!-- only this one will be altered !-->
<script>
$(".example-class").class("add", "classname");
</script>
Why would this be occurring? My for loop appears to be correct, so I am unsure what is wrong. Apologies if it appears pretty obvious, however, I'm still new to vanilla JavaScript.
All help (and suggestions) is appreciated,
Thanks.

for(var i = 0; i < els.length; i++){
this.element = els[i];
}
You have a loop. Each time it goes around the loop it assigns a value to this.element.
The first time it goes around the loop it assigns the value of els[0]. The second time it assigns the value of els[1].
Since you only have two elements that match, it reaches the end of the loop and stops.
At this point, this.element is still equal to els[1].
If you want to do something (like add membership of a class) to each item in els then you have to loop over els at the time you modify className.

Related

How to add additional class name to an element?

I can't believe I can't find this in google - I need to add an additional class name to a div with the classname checkField:
<div id="wth" class="checkField"><label>Ok whatever</label></div>
document.getElementById("wth").className="error"; // This works
document.getElementsByClassName("field").className="error"; // Doesn't work
I tried this:
document.getElementsByClassName("field").getElementsByTagName("label").className="error";
^ Also doesn't work.
Can someone please give me some advice? I'm too use to JQuery.
The getElementsByClassName method returns a nodeList, which is an array-like object, not a single element. To do what you want you need to iterate the list:
var divs = document.getElementsByClassName("checkField");
if (divs) {
// Run in reverse because we're
// modifying the result as we go:
for (var i=divs.length; i-- >= 0;) {
divs[i].className = 'error';
}
}
Also, just setting className actually replaces the class instead of adding to it. If you want to add another class you need to do it like this:
divs[i].className += ' error'; // notice the space bar
As for the second thing you're trying to do, that is setting the class of the label instead of the div, you need to loop through the divs and call getElementsByTagName on them:
var divs = document.getElementsByClassName("checkField");
if (divs) {
// Run in reverse because we're
// modifying the result as we go:
for (var i=divs.length; i-- >= 0;) {
var labels = divs[i].getElementsByTagName('label');
if (labels) {
for (var j=0; j<labels.length; j++) {
labels[j].className = 'error';
}
}
}
}
You need to add the class onto the current value:
document.body.className += " foo"; // adds foo class to body
Some browsers support classList which provides methods for adding, removing, and toggling classes on elements. For instance, you could add a new class like this:
document.body.classList.add("newClass");
Imagine the work involved in toggling a class; you'd have to perform some string operation to first determine whether a class is already on the element or not, and then respond accordingly. With classList you can just use the toggle method:
document.body.classList.toggle("toggleMe");
Some browsers don't currently support classList, but this won't prevent you from taking advantage of it. You can download a polyfill to add this feature where it's not natively supported.
try something like
you want to do this by document.body
so try
var demo =document.body;
// also do this by class like var demo = document.getElementsByClassName("div1one");
// or id var demo = document.getElementsId("div1one");
demo .className = demo .className + " otherclass";
or simpler solution if you have jquery as an option
$("body").addClass("yourClass");
Here's are simple utility functions for adding and removing a class:
function addClass(elem, cls) {
var oldCls = elem.className;
if (oldCls) {
oldCls += " ";
}
elem.className = oldCls + cls;
}
function removeClass(elem, cls) {
var str = " " + elem.className + " ";
elem.className = str.replace(" " + cls + " ", " ").replace(/^\s+|\s+$/g, "");
}

Why does removing an element with javascript prevent iteration of elements?

I am trying to replace all text fields on a page with labels.
function replaceInputTextFieldsWithValues() {
var inputFields = document.getElementsByTagName("input");
for(var i = 0; i < inputFields.length; i++) {
if(inputFields[i].getAttribute("type")== "text") {
var parent = inputFields[i].parentNode;
var value = inputFields[i].value;
parent.removeChild(inputFields[i]);
var label = document.createElement('label');
label.setAttribute('for', value);
label.innerHTML = value;
parent.appendChild(label);
}
}
}
My HTML document is organized in tables. This function only seems to work on the first element in each table.
On the other hand, when I remove the line:
parent.removeChild(inputFields[i]);
The code seems to work fine. Why is this happening and how do I fix it?
What you get back from getElementsByTagName is an HTMLCollection, which is live. (This is true for the other getElementsByXYZ methods, but not querySelectorAll.) That means if you remove the element at index 0, the HTMLCollection's length will go down and you'll have a new element at index 0 instead of the one you just removed.
Just work your way through it backward and you'll be fine:
for(var i = inputFields.length - 1; i >= 0; i--) {
// ...
}
Alternately, convert the HTMLCollection into an array and then loop through the array. (See the live example and code below).
Edit: Or, as Chris Shouts points out in the comments, you can just make use of the changing length, but it's not quite as simple as Chris' suggestion because you're only removing the elements sometimes. It would look like this:
var inputFields = document.getElementsByTagName("input");
var i = 0;
while (i < inputFields.length) {
if(inputFields[i].getAttribute("type")== "text") {
// Remove it and DON'T increment `index`
}
else {
// Skip this one by incrementing `index`
++index;
}
}
Which of these three approaches to use will depend on the situation. Copying to an array gives you a nice static dataset to work with, and if you make sure to release the reference to the HTMLCollection, you're giving the browser the opportunity to realize it doesn't have to keep that list up-to-date when things change, which could reduce overhead. But you're copying the references briefly, which increases overhead a bit. :-)
Additional: Here's an example showing this effect, and also showing a fairly efficient (but obscure) way to create an array from a HTMLCollection:
HTML:
<ul>
<li>LI0</li>
<li>LI1</li>
<li>LI2</li>
</ul>
JavaScript:
var lilist, liarray;
// Get the HTMLCollection, which is live
lilist = document.getElementsByTagName('li');
// Create an array of its elements
liarray = Array.prototype.slice.call(lilist, 0);
// Show initial length of both
display("lilist.length = " + lilist.length); // Shows 3
display("liarray.length = " + liarray.length); // Shows 3
// Show what the 0th element of both is (both show "LI0" in the live example)
display("lilist[0].innerHTML = " + lilist[0].innerHTML); // Shows LI0
display("liarray[0].innerHTML = " + liarray[0].innerHTML); // Shows LI0
// Remove the first list item
display("Removing item 0");
lilist[0].parentNode.removeChild(lilist[0]);
// Show the length of both, note that the list's length
// has gone down, but the array's hasn't
display("lilist.length = " + lilist.length); // Shows 2, not 3
display("liarray.length = " + liarray.length); // Still shows 3
// Show what the 0th element of both *now* is
display("lilist[0].innerHTML = " + lilist[0].innerHTML); // Shows LI1 now
display("liarray[0].innerHTML = " + liarray[0].innerHTML); // Still shows LI0
Live copy

Why is this Javascript loop taking one minute for 100 iterations?

I am using the below code in my program but it seems that these few line of code is taking too much time to execute. For 100 iteration it is consuming 1 mins approx. for 200+ iteration my broser is showing a warning message that script is taking too much time. As per the scenario 500+ ids can be pushed into the array.
for (var i = 0; i < arrid.length; i++)
{
$("#" + outerDiv + "> div[id=" + arr[i] + "]").attr("class", "selected");
}
arrid is an array of div ids. Outerdiv is the the parent div of all these div ids present in arrid. arr ids cannot be accessed directly, it has to be referenced using the parent div i.e. outerDiv.
One quick thing you could do is cache your selector so jQuery does not have to query the dom 500+ times.
var $div = $("#" + outerDiv);
for (var i = 0; i < arrid.length; i++)
{
$div.children("div[id=" + arr[i] + "]").attr("class", "selected");
}
On second thought, since you have a list of id's, you shouldn't need any of that as the id should be unique per the dom.
for (var i = 0; i < arrid.length; i++)
{
$("#" + arr[i]).attr("class", "selected");;
}
If outerDiv is an element, you can write
for (var i = 0; i < arrid.length; i++)
{
$("#" +arr[i], outerDiv).attr("class", "selected");
}
But assuming the id's are unique, you shouldn't need to reference the outer div at all. It might even be faster not to.
Also, if this is all you're doing and you're concerned about performance, why not just use plain ol' javascript?
for (var i = 0; i < arrid.length; i++)
{
document.getElementById(arr[i]).className = "selected";
}
Using jQuery function is expensive - as is manipulating the DOM.
You can reduce to one call to jQuery like this:
var divSelector = [];
for (var i = 0; i < arrid.length; i++)
{ // add selector to array
divSelector.push( "#" + outerDiv + "> div[id=" + arr[i] + "]" );
}
// execute jquery once with many selectors
$(divSelector.join(',')).attr("class", "selected");
This code is untested, but should work in principal.
Don't call jQuery selection function too many times
Instead select your elements once and do the rest in a different way. For this thing to work it would be better to convert your arr array of IDs into an accosiative array that can make searching much much faster.
// convert arr = ["idOne", "idTwo", ...]
// into an associative array/object
// a = { idOne: true, idTwo: true, ... }
var a = {};
$.each(arr, function(index, el){
a[el] = true;
})
// do the rest
$("#" + outerDiv " > div[id]").each(function(){
if (a[this.id] === true)
{
$(this).addClass("selected");
}
})
The most inner call can be as well replaced with:
this.className = "selected"
when you can be sure no other classes will be added to the element.
But when you want to set selected on all child div elements (if your IDs cover all elements) then a simple:
$("#" + outerDiv " > div[id]").addClass("selected");
would do the trick just fine.
Store the instance into a variable($div) and use another variable to store the max length the loop for will do.
var $div = $("#" + outerDiv);
for (var i = 0, max = arrid.length; i < max; i++)
{
$div.children("div[id=" + arr[i] + "]").attr("class", "selected");
}
How about this as you have the id's of all the elements you are trying to change the class on:
for (var i = 0; i < arrid.length; i++)
{
$("#" + arr[i]).addClass("selected");
}
I know using the selector div[id=val] performs very slow is certain browser based on a speed test I ran, worth a look as it is pretty interesting:
http://mootools.net/slickspeed/
I think you can directly push jQuery object to the array, then you can just arr[i].attr('class', 'selected')

Storing elements in memory to prevent updating the DOM too often?

Currently I have a loop that updates the DOM in each iteration; I have learned this is a bad practice & you should update the DOM as little as possible for better speed.
So I was wondering how I go about editing the below so I can store all the elements in one element or something & then do a single DOM addition once the loop ends.
Here is the loop..
for (var i = spot; i < spot + batchSize && i < cats.options.length; i++) {
// Check if the cat is selected
if (cats.options[i].selected == true) {
// Set this category's values to some variables
var cat_id = cats.options[i].getAttribute('value');
var cat_name = cats.options[i].text;
if (checkCatSICAdd(cat_id) === false) {
// Now we create the new element
var new_option = document.createElement('option');
// Add attribute
new_option.setAttribute('value',cat_id);
// Create text node
var new_text_node = document.createTextNode(cat_name);
// Append new text node to new option element we created
new_option.appendChild(new_text_node);
// Append new option tag to select list
sel_cats.appendChild(new_option);
} else {
failed++;
}
}
}
Working with DOM element in the loop is slow - no matter if you attach them to the document or not. Attaching them at the end is a bit faster since only only redraw is required but it's still slow.
The proper way is generating a plain old string containing HTML and attaching this string to the DOM using the innerHTML property of a DOM element.
Your code should be ok. The DOM won't actually redraw until the Javascript has finished executing. However, if you've encountered a problem browser that does perform badly, you could try creating a new select before your loop that is not yet attached to the DOM, populating it as you are now, then replacing sel_cats with that new select at the end. That way, the DOM is only updated once.
Your way is good enough unless you have great many items added to sel_cats - you add to the DOM only once.
The only way to improve efficiency might be to store the options as raw HTML then assign that after the loop:
var arrHTML = [];
for (var i = spot; i < spot + batchSize && i < cats.options.length; i++) {
// Check if the cat is selected
if (cats.options[i].selected == true) {
// Set this category's values to some variables
var cat_id = cats.options[i].value;
var cat_name = cats.options[i].text;
if (checkCatSICAdd(cat_id) === false) {
arrHTML.push("<option value=\"" + cat_id + "\">" + cat_name + "</option>";
}
else {
failed++;
}
}
}
sel_cats.innerHTML = arrHTML.join("");
Once you have the select list assigned to a variable, remove it from the dom using removeChild on its parent tag. You can then use appendChild in the loop before adding the select list back into the dom.
Your code is way bloated, DOM 0 methods will be much faster.
If speed really matters, store spot + batchSize && i < cats.options.length in variables so they aren't re-calcuated on each loop (modern browsers probably don't, but older ones did):
for (var i=spot, j=spot+batchSize, k=cats.options.length; i < j && i < k; i++) {
// Store reference to element
var opt = cats.options[i];
// The selected property is boolean, no need to compare
if (opt.selected) {
// if checkCatSICAdd() returns boolean, just use it
// but maybe you need the boolean comparison
if (checkCatSICAdd(opt.name) === false) {
// Wrapped for posting
sel_cats.options[sel_cats.options.length] =
new Option(opt.value, opt.name);
} else {
failed++;
}
}
}

how to access element whose id is variable

I need to access elements in html file using javascript, their names are like arr_1, arr_2, arr_3, I wish to use a loop to dynamically create the id then to access them like below:
for(var i=0; i< 10; i++) {
var id = "arr_" + i;
$document.getElementById('id')....
}
But it doesn't work. I remember there is an function to allow me do that, anyone know what that is?
You don't need the dollar sign preceding document, and you should pass your id variable to the getElementById function, not a string containing 'id':
for(var i=0; i< 10; i++) {
var id = "arr_" + i;
var element = document.getElementById(id);
// work with element
}
You might also want to check if getElementById actually found your element before manipulating it, to avoid run-time errors:
if (element) {
element.style.color = '#ff0000';
}
for (var i = 0; i < 10; i++) {
var obj = document.getElementById("arr_" + i);
obj.style.border = "1px solid red";
}
change
$document.getElementById('id')
to
$document.getElementById(id)
Since this question was published and answered quite correctly several times by using document.getElementById(id), another contender has entered the fray, querySelector, which takes any valid CSS selector and returns the first matched element.
Note that because querySelector takes a CSS selector, selecting an ID requires prefixing the ID with #.
for(var i=0; i< 10; i++) {
// Added the # here in the id variable.
var id = "#arr_" + i;
// assuming $document is a reference to window.document in the browser...
var element = $document.querySelector(id);
if (element) {
// do something with element
}
}
getElementById is typically faster than querySelector (in some browsers, twice as fast), which makes sense, since it doesn't have to invoke the CSS subsystem to find the element.
However, the option exists, and Stack Overflow is nothing if not thorough when answering questions.

Categories