How to quickly update the classes of many elements in Javascript? - javascript

I have a couple thousand elements on a page that should have their CSS class changed upon the pressing of a button.
Currently I have js code like the following:
for each (var id in list) {
var element = document.querySelector('[data-id="' + id + ']');
element.className = 'myClass';
}
In IE, this takes almost 20 seconds. I did some profiling, and it simply seems that the .className operation takes like half a millisecond; that along with garbage collection is what makes it take so long. I'm not sure if .className is causing a reflow or not.
Either way, how can I accomplish this task faster than I currently am?

I would make the CSS do all the hard work..
Just have some sort of parent, add a class to this and make your CSS target it.
eg.
document.querySelector("button").onclick = function () {
document.body.classList.toggle("button-clicked");
}
[data-id] {
background-color: yellow;
}
.button-clicked [data-id] {
background-color: pink;
}
<div data-id="1">One</div>
<div data-id="2">Two</div>
<div data-id="3">Three</div>
<div data-id="4">Four</div>
<div data-id="5">Five</div>
<button>Click Me</button>
If doing this CSS only is not an option, the best way to update the DOM is to do it detached,. Here is an example that creates 10,000 div's, and on the button click it randomly toggles the selected class of each one, but before doing this will temporarily detach them from the DOM, and then reattach after flipping the classes.
var container = document.querySelector(".container");
function addLine (txt) {
var div = document.createElement("div");
div.innerText = txt;
div.classList.add("line");
container.appendChild(div);
}
for (var l = 1; l <= 10000; l += 1) {
addLine("This is Line " + l + ", and some extra text");
}
document.querySelector("button").onclick = function () {
//temporally remove from DOM
container.parentNode.removeChild(container);
//all DOM methods here on conatiner should now be fast
//as there is no UI updates required..
var lines = container.querySelectorAll(".line");
for (var l = 0; l < lines.length; l ++) {
var e = lines[l];
if (Math.random() > 0.5)
e.classList.toggle("selected");
};
//Ok done, lets now put it back into the DOM
document.body.appendChild(container);
}
.line {
background-color: yellow;
}
.line.selected {
background-color: red;
}
<button>toggle class</button>
<div class="container">
</div>

Related

How can i select a specific element in a HTMLCollection in javascript

I have a div container with a certain number of div's created with a for loop inside of it. When i click one of these a divs, i need to make it change the colour. My problem is can't figure out how to select an specific element with the addEventListener to change the color.
<body>
<div id="main-container"></div>
<script src="script.js"></script>
</body>
const mainContainer = document.getElementById("main-container");
for (let i = 0; i <= 11; ++i) {
const gridChildrens = document.createElement("div");
gridChildrens.setAttribute("class", `gridChildrens`);
const grids = document.querySelector('.gridChildrens')
mainContainer.appendChild(gridChildrens);
}
For the moment, i figure out how to change the color of the firt or the last of the elements with a click listener, but not for the rest of the of the divs.
For the moment, i figure out how to change the color of the firt or the last of the elements with a click listener, but not for the rest of the of the divs.
I expect to click any of the divs and change the color.
Try to add an event listener to each div created in the loop and then use 'this' to set your colour. Here's an example :
const mainContainer = document.getElementById("main-container");
for (let i = 0; i <= 11; ++i) {
const gridChildrens = document.createElement("div");
gridChildrens.setAttribute("class", `gridChildrens`);
gridChildrens.addEventListener('click', function() {
this.style.backgroundColor = 'red';
});
mainContainer.appendChild(gridChildrens);
}
Code snippet sample:
<html>
<head>
<style>
.gridChildrens {
width: 50px;
height: 50px;
background-color: blue;
margin: 10px;
}
</style>
</head>
<body>
<div id="main-container"></div>
<script>
const mainContainer = document.getElementById("main-container");
for (let i = 0; i <= 11; ++i) {
const gridChildrens = document.createElement("div");
gridChildrens.setAttribute("class", `gridChildrens`);
gridChildrens.addEventListener('click', function() {
this.style.backgroundColor = 'red';
});
mainContainer.appendChild(gridChildrens);
}
</script>
</body>
</html>
To explicitly target an element the querySelector without click event (which will inspect the event.target ) you can use the nth-child or nth-of-type style css selector as below.
To identify an element based upon user click the event itself will expose the target property which will be the element that caused the event handler to fire. The following uses a delegated event listener bound to the document itself which processes all click events if required but here responds only to those bound to the gridChildrens elements
const mainContainer = document.getElementById("main-container");
for (let i = 0; i <= 11; ++i) {
const div = document.createElement("div");
div.setAttribute("class", `gridChildrens`);
div.textContent=i;
mainContainer.appendChild( div );
}
document.querySelector('.gridChildrens:nth-of-type(4)').classList.add('banana')
document.querySelector('.gridChildrens:nth-of-type(7)').classList.add('banana')
document.querySelector('.gridChildrens:nth-of-type(10)').classList.add('banana')
document.addEventListener('click',e=>{
if( e.target instanceof HTMLDivElement && e.target.classList.contains('gridChildrens') ){
e.target.classList.toggle('tomato')
}
})
.gridChildrens{
width:50%;
height:1rem;
margin:0.25rem;
}
.banana{
background:yellow
}
.tomato{
background:tomato
}
<div id="main-container"></div>
This should give you a good idea of how to use addEventlistner. Basically, you can pass the event object whenever you make some event. That has all the information of the specific div that you are looking for, you can change anything with that. But remember to bind the elements with addEventlistner first.
var containers= document.getElementsByClassName("container");
const changeColor = (e)=>{
if(e.target.style.background =="orange"){
e.target.style.background ="red"
}
else
e.target.style.background ="orange";
}
for(var i=0; i< containers.length; i++){
containers[i].addEventListener('click',function(e){
changeColor(e)
} );
}
.container{
height:50px;
width:100px;
background: #000;
margin :10px 10px;
border-radius:10px;
cursor:pointer;
}
.holder{
display:flex;
flex-wrap:wrap;
}
<div class="holder">
<div class="container"></div>
<div class="container"></div>
<div class="container"></div>
<div class="container"></div>
<div class="container"></div>
</div>
There are several ways to do this. For example, you can add the same class to your divs inside the loop. Then you can access them via document.querySelectorAll('.class-name'). So smth like this:
[...document.querySelectorAll('.class-name')].forEach( el => {
el.addEventListener('click', (e) => { changeColor(e); });
});

Only the last element I added using innerHTML keeps its event handlers, why?

I am trying to make a script that injects interactable object information in a list of the markup page. Whenever I try to add an onclick event on a div, it works fine, however whenever I try to add more within a for loop, it does not work the way I intended.
I took a look of what is going on using breakpoints in the webpage debugger, and I see that the problem is that it seems to delete the event on the previous div before adding to the next div. In the end, the only event remaining is the last div after the loop exits.
I want to keep these events on all my divs, not just the last one... what seems to be the problem here?
var objects = ['Tom', 'Sauna', 'Traum'];
for (var i = 0; i < objects.length; i++){
document.getElementById('list').innerHTML += "<div class='item' id='"+ i +"'>" + objects[i] + "</div>";
document.getElementById(i).addEventListener("mouseup", function() {
Select(this);
});
}
function Select(char) {
console.log(char);
}
div.item {
border: 1px solid black;
padding: 4px;
margin: 4px;
}
<div id="list"></div>
When you change innerHTML browser reconstructs the element's contents, throwing away all event handlers attached. Use DOM methods instead:
for (let i = 0; i < objects.length; i++){
var block = document.createElement('div');
block.setAttribute('id', i);
document.getElementById('list').appendChild( block );
block.addEventListener("mouseup", function() {
Select(this);
});
}
UPD: alternatively use a insertAdjacentHTML method instead of redefining innerHTML:
document.getElementById('list').insertAdjacentHTML(
'beforeend', "<div id='"+ i +"'>" + i + "</div>");
The reason is the way you are appending. innerHtml += effectively overwrites the existing content in the list. So, any elements that you added and bound are simply gone, and new items are added each time.
There are a couple ways to make this work.
First instead of assigning an innerHtml you can append elements.
const items = ['taco', 'apple', 'pork'];
const list = document.getElementById("list");
for (const item of items) {
const el = document.createElement("div");
el.addEventListener('click', (e) => console.log(`clicked ${item}`));
el.innerText = item;
list.appendChild(el);
}
<div id="list"></div>
Since we are appending an explicit element and not overwriting content, this will work.
A better approach would be to use delegation. We assign a single event handler onto the list and listen for any clicks. We then figure out what specific element was clicked.
const items = ['taco', 'apple', 'pork'];
const list = document.getElementById("list");
const add = document.getElementById("add");
list.addEventListener('click', (e) => {
const parent = e.target.closest("[data-item]");
if (parent != null) {
console.log(`clicked on ${parent.dataset['item']}`);
}
});
for (const item of items) {
list.innerHTML += `<div data-item="${item}">${item}</div>`;
}
add.addEventListener('click', () => {
const item = `item ${Date.now()}`;
list.innerHTML += `<div data-item="${item}">${item}</div>`;
})
<div id="list"></div>
<button id="add">add</button>
The magic here is we assign a single event handler on the parent, and use closest to figure out what item was clicked. I'm using innerHTML here for simplicity but it should be avoided for security reasons.
A good pattern to use when appropriate is event delegation. It allows following the Don't Repeat Yourself principle, making code maintenance considerably easier and potentially making scripts run significantly faster. And in your case, it avoids the pitfalls of an element being responsible for modifying its own content.
For example:
const container = document.getElementById('container');
container.addEventListener("click", toggleColor); // Events bubble up to ancestors
function toggleColor(event) { // Listeners automatically can access triggering events
const clickedThing = event.target; // Event object has useful properties
if(clickedThing.classList.contains("click-me")){ // Ensures this click interests us
clickedThing.classList.toggle("blue");
}
}
.click-me{ margin: 1em 1.5em; padding 1em 1.5em; }
.blue{ color: blue; }
<div id="container">
<div id="firstDiv" class="click-me">First Div</div>
<div id="firstDiv" class="click-me">Second Div</div>
</div>

Scope issues inside an Event Listener?

The following code basically shows/hides paragraph tags, I'm having to re-declare the paras variable. Is this because I'm dynamically injecting the button into the DOM, or is it to do with scope? How could I better construct this markup?
// vars
var revealContainer = document.querySelector('.reveal-more');
var paras = revealContainer.querySelectorAll('p');
var status = true;
// return
if (paras && paras.length <= 3) return;
// generate show more link
revealContainer.innerHTML += '<button class="button--text reveal-more__btn">Read more</button>';
var revealBtn = revealContainer.querySelector('.reveal-more__btn');
// click event
revealBtn.addEventListener('click', function () {
var paras = revealContainer.querySelectorAll('p');
// toggle show/hide class
for (var i = 0; i < paras.length; i++) {
var p = paras[i];
p.classList.toggle('is-shown');
}
// check status
if (status) {
this.textContent = 'Read less';
status = false;
} else {
this.textContent = 'Read more';
status = true;
}
});
You can use the live HTMLCollection returned by .getElementsByTagName() instead of the static NodeList returned by .querySelectorAll()
The getElementsByTagName method of Document interface returns an HTMLCollection of elements with the given tag name. The complete document is searched, including the root node. The returned HTMLCollection is live, meaning that it updates itself automatically to stay in sync with the DOM tree without having to call document.getElementsByTagName() again.
var paragraphs = document.getElementById("container").getElementsByTagName("p");
console.log(paragraphs.length);
setInterval(function() {
document.getElementById("container").insertAdjacentHTML("beforeend", "<p>p</p>");
}, 1000);
setInterval(function() {
console.log(paragraphs.length);
}, 2000);
<div id="container"></div>
Below is a really simple Snippet that demonstrates delegated events in pure Javascript, instead of using jQuery.
Here you can see I've attached the eventListener to the div with id elements, this will then listen for click events under this, a simple matches is used just in case you have other elements your not interested in..
document.querySelector("#elements").addEventListener("click", (e) => {
if (!e.target.matches('.element')) return
console.log(`Clicked ${e.target.innerText}`);
});
.element {
border: 1px solid black;
margin: 5px;
}
<div id="elements">
<div class="element">1</div>
<div class="element">2</div>
<div class="element">3</div>
<div>Clicking this does nothing.</div>
</div>

removing div from html in chrome extension

I am trying to remove the following div from a page with my chrome extension
HTML (TO REMOVE)
<div class="base-popup js-base-popup"><div class="js-obscurity base-popup__obscurity"></div>
<div class="base-popup__indent"></div>
<div class="base-popup__wrap">
<div class="base-popup__container clearfix base-popup__container -decor" style="width:500px;">
<i class="s-icon -m -close base-popup__close js-close"></i>
<div class="base-popup__content js-content"><div><div class="s-text">Sample Text.
<!-- close tag -->
</p>
<!-- close tag in translate -->
</div></div></div>
</div>
Here is the JS in my content script
function removeElementsByClassName(names) {
var els = document.getElementsByClassName(names),
i, element;
for (i = els.count - 1; i > 0; i -= 1) {
element = els[i];
element.parentElement.removeChild(element);
}
}
removeElementsByClassName('base-popup js-base-popup');
getElementsByClassName only accepts a single class name, but you're giving it two. Since the HTML you've shown only has a single element that has either of the two classes you're using, if that's the only element you want to remove, just pick one:
removeElementsByClassName("base-popup");
// or
removeElementsByClassName("js-base-popup");
Alternately, you could use querySelectorAll with a CSS selector:
function removeElementsBySelector(selector) {
var els = document.querySelectorAll(selector),
i, element;
for (i = els.count - 1; i > 0; i -= 1) {
element = els[i];
element.parentElement.removeChild(element);
}
}
Then if you want to remove elements that have either class:
removeElementsBySelector('.base-popup, .js-base-popup');
Or if you only want to remove a single element that has both classes:
removeElementsBySelector('.base-popup.js-base-popup');
And as this is a Chrome extension, you can do that rather more simply with Array.from, forEach, and Element#remove:
function removeElementsBySelector(selector) {
Array.from(document.querySelectorAll(selector)).forEach(element => {
element.remove();
});
}
your javascript is completely wrong. the right way:
function removeElementsByClassName(names){
names=names.split(" ");//you just get elems by one class so you need to split it into multiple operations
for(var a=1;a<names.length;a++){//ability to remove multiple classes
removeElementsByClassName(names[a]);
}
var els = document.getElementsByClassName(names[0]);
for (var i =0; i<els.length ; i++) { // its length not count
var element = els[i];
element.parentElement.removeChild(element);
}
}
removeElementsByClassName('base-popup js-base-popup');
this removes all elements that contain one of these classes, if you wanted sth else see the other solution.

JavaScript destroys all children after I append a break?

I'm creating a script that takes two input dimensions, width, and height, and creates a scaled grid which is representative of how many blocks could fit in a box with the given dimensions with the following function:
function makeRow() {
for (var i = 1; i <= blocksTall; i++) {
var mb = document.createElement("div");
mb.setAttribute("class", "matrix-block mb-off");
mb.setAttribute("onClick", "select_mb('" + j + "," + i + "');");
placeBlocks.appendChild(mb);
if (i = blocksWide) {
placeBlocks.appendChild('br');
}
}
}
This function works fine to display the first row of blocks, and then inserts a break tag after the row has finished being rendered, which is exactly what I want to do. The problem is I need to generate 17 more rows, with the same number of blocks, each one under the previous row, so my first thought was, I'll just wrap another for loop around this first for loop and since there is a break there, it will render the new row below the previous one:
for (var j = 1; j <= blocksTall; j++) { // Vertical for loop.
for (var i = 1; i <= blocksWide; i++) { // Horizontal for loop.
var mb = document.createElement("div");
//mb.setAttribute("id", "matblock-" + i + "-" + j);
mb.setAttribute("class", "matrix-block mb-off");
mb.setAttribute("onClick", "select_mb('" + i + "," + j + "');");
placeBlocks.appendChild(mb);
}
if (j = blocksWide) {
placeBlocks.appendChild(brk);
}
}
Where blocksWide = 17. Here is a fiddle with the complete script. When I log the value for j in the console, it does in fact increment (which tells me that the for loop is working). What seems to be happening though is that it is for some reason rendering the row, and then either rendering the new row on top of it (seems unlikely since the break tag is rendered after each row completes) or, for some reason the children are destroyed each time a new "horizontal" for loop is run.
Does anybody know why this might be happening and how to properly get each row to be appended under the last row so it produces a grid of blocks instead of just one row?
Thanks in advance, any help is greatly appreciated.
So, I'm a bit confused about some aspects of your script, but I think you have two major issues.
Firstly, you only ever call document.createElement("br") once, which means you only ever create a single line-break; and a single line-break can only appear in one place in the DOM. This:
placeBlocks.appendChild(brk);
removes brk from its current position in the DOM and then puts it at the end of placeBlocks. You should change it to this:
placeBlocks.appendChild(document.createElement("br"));
Secondly, I don't think that if (j = blocksWide) { makes sense. Note that it's equivalent to this:
j = blocksWide;
if (blocksWide != 0) {
which means that it interferes with your for-loop by manipulating the value of j. I think the fix for that issue is simply to remove the whole if-check, and to perform its body unconditionally.
I really don't understand what you were trying to do with the remainder operators and the dividing, but blocksWide resolved to infinity causing an infinite loop, and blocksHigh was just 17. All of the other variables besides full weren't used.
You don't actually need two loops, although it is ok to do that. If you want to use just one loop you basically just need to know if i is a multiple of dispW.
So you divide i by dispW then you want to know if it is an integer, to find this you use the remainder operator for 1 and if it resolves to 0 it is an interger. It looks like this...
if ((i / dispW) % 1 === 0)
// if ( dispW=3 && ( i=3 || i=6 || i=9 || ... ) ) true;
This in a loop would look like
totalWidth = dispW * dispH; // total number of blocks
for (var i = 1; i <= totalWidth; i++) {
// do stuff;
if((i / dispW) % 1 === 0) {
// insert new line break;
}
}
The method you used for selecting the blocks was a round about way of doing it. First you shouldn't use inline javascript, second you shouldn't use javascript to embed inline javascript in a dynamically created element. Use element.onclick = function; instead.
Notice there is no braces after the function. This is because you are actually passing the function reference and not the returned value of the function.
element.onclick passes an event object to the function reference. You can use this to select the block that was clicked on like so.
for ( ... ) {
...
var element = document.createElement('div');
element.onclick = myFunction;
...
}
function myFunction(e) {
var clicked = e.target // this is the element that was clicked on
}
Also, you were creating one <br> element outside of the loop. Because appendChild moves elements and does not create elements it will just keep moving the line break until the loop finishes. It should look like this.
placeBox.appendChild(document.createElement('br'))
// append a newly created line break;
Then even if all the logic worked as intended and you create a new line break every time, floated blocks means no line breaks use display: inline-block; instead.
So in the end what you get is...
(Full difference)
window.onload = function () {
renderGrid();
};
function renderGrid() {
var blocksTall = document.getElementById('height-in').value;
var blocksWide = document.getElementById('width-in').value;
var blocksTotal = blocksWide * blocksTall;
var placeBlocks = document.getElementById('matrix-shell');
while (placeBlocks.firstChild) {
placeBlocks.firstChild.remove();
}
console.log(blocksWide + "/" + blocksTall);
for (var i = 1; i <= blocksTotal; i++) {
var mb = document.createElement("div");
mb.className = 'matrix-block mb-off';
mb.onclick = select_mb;
placeBlocks.appendChild(mb);
if (((i / blocksWide) % 1) === 0) {
var brk = document.createElement("br");
placeBlocks.appendChild(brk);
}
}
}
function select_mb(e) {
var cur_mb = e.target;
if (cur_mb.className == "matrix-block mb-off") {
// Turn cell on.
cur_mb.style.backgroundColor = "#00FF00";
cur_mb.className = "matrix-block mb-on";
} else {
//Turn cell off.
cur_mb.style.backgroundColor = "#000";
cur_mb.className = "matrix-block mb-off";
}
}
.matrix-block {
height: 10px;
width: 10px;
border: 1px solid #fff;
display: inline-block;
background-color: black;
}
.mb-off {
background-color: black;
}
#matrix-shell {
font-size: 0;
display: inline-block;
border: 1px solid red;
white-space: nowrap;}
<table>
<tr>
<td>Width:</td>
<td>
<input id="width-in" name="width-in" type="text" />
</td>
</tr>
<tr>
<td>Height:</td>
<td>
<input id="height-in" name="height-in" type="text" />
</td>
</tr>
<tr>
<td colspan="2">
<button onClick="renderGrid()">Compute</button>
</td>
</tr>
</table>
<br/>
<div id="matrix-shell"></div>

Categories