Is there a way to change css selector with javascript? - javascript

I have a div with 5 elements, and I only want to show 1 at a time.
Is there a way to change .classname:nth-child(1) to .classname:nth-child(3) in javascript?
Or do I need a different approach?
I have a display none on the parent and i want to display flex the nth child

I suppose this different approach may be viable. The snippet uses event delegation:
document.addEventListener(`click`, handle);
function handle(evt) {
if (evt.target.id === `next`) {
let next;
// remove .active from all elements
document.querySelectorAll(`.className`).forEach(
(elem, i) => {
if (elem.classList.contains(`active`)) {
// Note: nth-child is not zero based
next = i + 2 > 5 ? 1 : i + 2;
}
elem.classList.remove(`active`);
});
// add .active to the next element
document.querySelector(`.className:nth-child(${next})`)
.classList.add(`active`);
}
}
.className {
display: none;
}
.active {
display: block;
}
<div class="className active">div 1</div>
<div class="className">div 2</div>
<div class="className">div 3</div>
<div class="className">div 4</div>
<div class="className">div 5</div>
<button id="next">next</button>
In reply to #tacoshys
It is inefficient and consumes way more resources and time than
necessary
I beg to differ. Let's cite Donald Knuth here
Programmers waste enormous amounts of time thinking about, or worrying
about, the speed of noncritical parts of their programs, and these
attempts at efficiency actually have a strong negative impact when
debugging and maintenance are considered. We should forget about small
efficiencies, say about 97% of the time: premature optimization is the
root of all evil. Yet we should not pass up our opportunities in that
critical 3%."
About inefficiency of event delegation I cite the linked page #javascript.info:
... the delegation may add CPU load, because the container-level
handler reacts on events in any place of the container, no matter
whether they interest us or not. But usually the load is negligible,
so we don’t take it into account.
It does not scale
Sure, it's up to anyone to make it more scalable. The snippet was just an rough indication for how to solve OPs problem.
(it is hard to read)
Readability, like beauty, is in the eye of the beholder.
Now about your code: the const you define for the div.className nodeList may give you trouble later on, e.g. when elements are added dynamically. So, about scalability ...
The latter is demonstrated in the next snippet (which uses your idea - which, by the way, is not a bad idea ;).
document.addEventListener(`click`, handle);
const logLine = txt => (console.clear(), txt && console.log(txt));
const ALL_ELEMS = document.querySelectorAll(`.className`);
for (let i = 0; i < 5; i += 1) {
addElemDemo(i);
}
logLine(`There are now ${document.querySelectorAll(`.className`)
.length} div.className in the document. `);
function activate(activeClass) {
logLine();
let activeIndex = [...ALL_ELEMS]
.findIndex(el => el.classList.contains(activeClass));
if (activeIndex >= 0) {
ALL_ELEMS[activeIndex].classList.remove(activeClass);
return ALL_ELEMS[activeIndex + 1 >= ALL_ELEMS.length ?
0 : activeIndex + 1].classList.add(activeClass);
}
return logLine(`Well, there you have it ... ` +
`you have to make a round trip using 'no really, next'`);
}
function activate4Real(selector, activeClass) {
const all_elems = document.querySelectorAll(selector);
let activeIndex = [...all_elems]
.findIndex(el => el.classList.contains(activeClass));
if (activeIndex + 1 > ALL_ELEMS.length - 1) {
logLine(`Try clicking the 'next' button now`);
}
all_elems[activeIndex].classList.remove(activeClass);
all_elems[activeIndex === all_elems.length-1 ?
0 : activeIndex + 1].classList.add(activeClass);
}
function addElemDemo(index) {
const lastIndex = ALL_ELEMS.length + index;
document.querySelector(`#next`).insertAdjacentElement(
`beforebegin`,
Object.assign(document.createElement(`div`),
{className: `className`, textContent: `div ${lastIndex + 1}`} ) );
};
function handle(evt) {
if (evt.target.id === `next`) {
return activate( evt.target.dataset.activeclass );
}
if (evt.target.id === `reallyNext`) {
return activate4Real(
evt.target.dataset.forselector,
evt.target.dataset.activeclass );
}
}
.className {
display: none;
}
.active {
display: block;
}
<div class="className active">div 1</div>
<div class="className">div 2</div>
<div class="className">div 3</div>
<div class="className">div 4</div>
<div class="className">div 5</div>
<button
id="next"
data-activeclass="active";
>next</button>
<button
id="reallyNext"
data-forselector=".className"
data-activeclass="active";
>no really, next</button>

#Kooilnc Answer works but has 2 major issues:
It is inefficient and consumes way more resources and time than necessary
It does not scale
(it is hard to read)
Inefficient
For once the code appends the EventListener to the whole document and then checks on every click if the button was clicked. The `EventListener should be added to the button itself:
const BUTTON = documents.querySelector('#next');
BUTTON.addEventListener('click', function() {
...
})
Then he uses querySelectorAll which returns a NodeList. Then he iterates through the NodeList and checks every Element if it has the "active" class. If he has found that element in his iteration he removes that class. The efficient (fast and resource-saving) solution is to select the element that has the class directly and remove the class from that element:
document.querySelector('.className.active').classList.remove('active);
No need for an iteration that takes more time and resources.
Scalability
Scalability should always be a concern. The code from the mentioned answer has the issue that it requires to be exact 5 elements. It will no longer work correctly if you have more or fewer elements. As such it requires manual maintenance and fixing as soon as you add or removes content.
A good and clean code should not care about how many elements you have to work correctly. The trick here is to check in the script how many elements you have by using NodeList.length.
Improved Solution
const BUTTON = document.querySelector('#next');
const ELEMENTS = document.querySelectorAll('.className')
BUTTON.addEventListener('click', function() {
let active = document.querySelector('.className.active');
let indexOfActive = [].indexOf.call(ELEMENTS, active) + 1;
active.classList.remove('active');
if (indexOfActive === ELEMENTS.length) {
indexOfActive = 0;
}
ELEMENTS[indexOfActive].classList.add('active');
})
.className {
display: none;
}
.active {
display: block;
}
<div class="className active">div 1</div>
<div class="className">div 2</div>
<div class="className">div 3</div>
<div class="className">div 4</div>
<div class="className">div 5</div>
<button id="next">next</button>
How does the code Work?
It creates a NodeList out of all the elements
Then it takes the index from the element with the active class in
the NodeList
It adds + 1 to the index to select the next element in the NodeList unless the index is equal to the length of the NodeList in which case it resets the index to 0
It adds the active class to the next element

Related

Add class to next integer on click while removing from current jQuery

I am wrapping the class; slideVisible to the first 3 blog articles displayed. This class removes the css property; display - none. I have added some indicators with the class; carousel-buttons, which also is based on the number of loops of sets of 3 blog posts. On click of these carousel-buttons I would like to remove the class slideVisible from the element which currently has it, and then add to the next element in the sequence.
I have used an index-related selector to demonstrate a way of creating this function, however this is not dynamic. How would I do this correctly?
jQuery(document).ready(function() {
var carosuelPost = jQuery(".post-slider-mango .post");
jQuery('.post-slider-mango .fusion-posts-container').wrapAll('<div id="dog-slider"><div class="carousel-inner"></div></div>');
for (var i = 0; i < carosuelPost.length; i += 3) {
let activeClass = '';
if(i == 0) activeClass = 'slideVisible';
carosuelPost.slice(i, i + 3).wrapAll('<div class="slideElements ' + activeClass + '"> </div>');
}
for (var i = 0; i < carosuelPost.length; i += 3) {
if(i == 0) activeClass = 'slideVisible';
jQuery(".post-slider-mango .fusion-posts-container").after('<a class="carousel-buttons"><li></li></a>');
jQuery(".carousel-buttons:eq(0)").on("click", function() {
jQuery(".slideElements").removeClass("slideVisible");
jQuery(".slideElements:eq(0)").addClass("slideVisible");
});
jQuery(".carousel-buttons:eq(1)").on("click", function() {
jQuery(".slideElements").removeClass("slideVisible");
jQuery(".slideElements:eq(1)").addClass("slideVisible");
});
}
});
So it's hard to say because it depends on the element that has the activeClass class and the heirarchy. You can have the .carousel-button on click remove the class from its parent, or parent's parent.
You can use .closest() to find the closest ancestor with x class. So:
// Declare function that removes a given class from given element
// Set click event listener for .carousel-button elements
$('.carousel-button').click(function(e){
e.preventDefault();
$(this).closest('div.slideVisible').removeClass('slideVisible');
});
Also to note, I would use a <button> element for the carousel-button so you do not then need to use the e.preventDefault() in the click event. If it is only to be used to run JavaScript via user interaction then you do not need to use an <a> tag.
See snippet for working example.
// Set click event listener for .carousel-button elements
$('.carousel-button').click(function(){
const next = $(this).closest('div.slideVisible').next().is('.slide');
if (next) {
return $(this).closest('div.slideVisible').removeClass('slideVisible').next().addClass('slideVisible');
} else {
return $(this).closest('div.slideVisible').removeClass('slideVisible').prev().addClass('slideVisible');
}
});
.slide {
display:none;
padding:2rem;
text-align:center;
background:orange;
}
.slide2 {
background:yellow;
}
.slideVisible {
display:block;
}
.carousel-button {
padding:1rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="slide slide1 slideVisible">
<div class="buttons">
<button class="carousel-button">Hide Me!</button>
</div>
</div>
<div class="slide slide2">
<div class="buttons">
<p>This is slide 2</p>
<button class="carousel-button">Hide Me!</button>
</div>
</div>
Edit: This answer assumes that you're trying to remove the class from a parent of the button you're clicking. If you need to get it from the sibling before or after you can use .prev() and .next() after calling the .closest() method:
$(this).closest('div.slideVisible').prev().removeClass('slideVisible');

How to remove one css class from div container with multiple elements and classes?

I want to get all elements which belong to two specific different classes and remove and add these classes seperately from these elements. I tried:
.concealed {
display: none;
}
.slide {
background: #333;
color: #fff;
}
// <div class="slide concealed">myText</div><br>
<div class="slide"><div class="concealed">myText</div></div><br>
<div class="slide"><div class="concealed">myText_2</div></div><br>
<div class="slide"><div class="concealed">myText_3</div></div><br>
// var slides = document.getElementsByClassName('slide');
// var slides = document.querySelectorAll('.slide, .concealed');
var slides = document.getElementsByClassName('slide concealed');
slides[0].classList.remove("concealed");
As you can see I tried several ways to achieve this but I can only remove and add "concealed" when I do var slides = document.getElementsByClassName('concealed'); . When doing getElementsByClassName with multiple classnames it seems to miss out concealed and only get slide. E.g. doing slides[0].classList.remove("concealed"); after document.getElementsByClassName('slide concealed'); has no effect.
I am sure I am missing something, this can't that hard to implement. Any ideas? Thanks.
The issue is that getElementsByClassName is a live HTMLCollection. When an element no longer matches (because you've removed the class), it's removed from the collection:
const list = document.getElementsByClassName("example");
console.log(list.length); // 2
console.log(list[0].innerHTML); // "A"
list[0].classList.remove("example");
console.log(list.length); // 1
console.log(list[0].innerHTML); // "B"
<div class="example">A</div>
<div class="example">B</div>
This means that if you're doing a loop and operate on the first entry, then increment your index, you'll miss what used to be the second entry because it's the first entry now.
A couple of ways to fix that:
Loop backward, or
Convert the HTMLCollection into an array before starting, or
Use querySelectorAll to get a snapshot NodeList instead, so that the lists contents don't change while you're updating.
Here's an example removing the classes red and bold from a series of elements using querySelectorAll so the NodeList is static:
setTimeout(() => {
const list = document.querySelectorAll(".red, .bold");
for (let n = 0; n < list.length; ++n) {
list[n].classList.remove("red");
list[n].classList.remove("bold");
}
}, 800);
.red {
color: red;
}
.bold {
font-weight: bold;
}
<div class="red">red 1</div>
<div class="bold">bold 1</div>
<div class="red bold">red 2 and bold 2</div>
<div class="bold">bold 3</div>
<div class="bold">bold 4</div>
See also my answer here: NodeList is now officially iterable, meaning you should be able to use a for-of loop on it. That answer shows how to polyfill that on slightly-older environments that haven't implemented it yet. It also shows how to add iterability to HTMLCollection (but note that HTMLCollection is not specified to be iterable).

Asynchronously remove children of a DOM element?

I have a div#parent element which has a few thousand sibling children div#1, div#2, div#3 like so:
<div id="parent">
<div id="1"> </div>
<div id="2"> </div>
<div id="3"> </div>
…
…
…
<div id="N"> </div>
</div>
What is the best way to remove all children of the parent node -- preferably asynchronously or through a separate process?
Here are a few standard answers from the wild that deal with slow and fast, but not async/e̶f̶f̶i̶c̶i̶e̶n̶t̶l̶y̶ or non-blocking:
Option 1:
var myNode = document.getElementById("foo");
myNode.innerHTML = '';
Option 2:
let parentNode = document.getElementById("parent");
while (parentNode.firstChild) {
parentNode.removeChild(parentNode.firstChild);
}
Since a while-loop gives a "busy state" in the dom's single threaded environment I wonder if there is a possibility to remove the nodes from the dom asynchronously/e̶̶̶f̶̶̶f̶̶̶i̶̶̶c̶̶̶i̶̶̶e̶̶̶n̶̶̶t̶̶̶l̶̶̶y̶ or in a non-blocking fashion?
Obviously innerHTML = '' will remove all items simultaneously. If this is too slow and you want to remove the elements little by little, then you need something similar to your second approach.
let parentNode = document.getElementById("parent");
(function remove() {
// Remove elements in groups of 100
for(let i=0; i<100 && parentNode.firstChild; ++i) {
parentNode.removeChild(parentNode.firstChild);
}
// Continue asynchronously after 50ms
setTimeout(remove, 50);
})();
Adjust the group size and the time delays as needed.

JavaScript insertBefore not working properly

In an attempt to make my answer more flexible on this question:
function invertDivs(parentDiv) {
var first = document.getElementById(parentDiv).firstChild;
console.log(first);
var second = document.getElementById(parentDiv).lastChild;
console.log(second);
document.getElementById(parentDiv).insertBefore(second, first);
}
<div id="parent">
<div id="first">Div 1</div>
<div id="second">Div 2</div>
</div>
<button onclick="invertDivs('parent');">Invert Divs</button>
However, the divs are only inverted sometimes, not always.
An initial click on the button yields this on the console:
A second click:
After a bunch of clicks:
I'm confused as to what's wrong with the code. I select the first child of parent div, and do the same for the last child. Then I just insert the current second div before the first. That's the end of the function. They are also the direct children of the parent div, as required by the insertBefore function.
As mentioned in comments, firstChild and lastChild can return text nodes for the whitespace between elements. You can use firstElementChild and lastElementChild to ignore these.
function invertDivs(parentDiv) {
var first = document.getElementById(parentDiv).firstElementChild;
console.log(first);
var second = document.getElementById(parentDiv).lastElementChild;
console.log(second);
document.getElementById(parentDiv).insertBefore(second, first);
}
<div id="parent">
<div id="first">Div 1</div>
<div id="second">Div 2</div>
</div>
<button onclick="invertDivs('parent');">Invert Divs</button>
For some other workarounds, which you might need for older browsers, see element.firstChild is returning '<TextNode ...' instead of an Object in FF
You're not taking into account text nodes. In your HTML example above, there are 5 nodes.
[0] => TextNode
[1] => #first
[2] => TextNode
[3] => #second
[4] => TextNode
It seems pretty evident that you don't care about the text nodes here. You have quite a few options.
One option would be to filter out all the text nodes. (Can't use Array.prototype.filter method because childNodes is not an array, but a NodeList)
This will give you an array of only DOM elements.
function invertDivs(parentNodeId) {
var childElements = [],
parentNode = document.getElementById(parentNodeId);
//Filter out the child nodes that aren't elements.
//parentNode.childNodes is a NodeList, and not an array (even though it looks like one)
for (var i = 0; i < parentNode.childNodes.length; ++i) {
if (parentNode.childNodes[i].nodeType === 1)
childElements.push(parentNode.childNodes[i]);
}
parentNode.insertBefore(childElements[childElements.length - 1], childElements[0]);
}
<div id="parent">
<div id="first">Div 1</div>
<div id="second">Div 2</div>
</div>
<button onclick="invertDivs('parent');">Invert Divs</button>
Another option would be to use the more modern DOM API properties: See Barmar or GolezTrol's answers. They would be much more performant if you audience has support for IE9+ browsers.
It's not random. If I click 2 times, to add Div 2 to the end of the list, then click 3 times to get Div 1 at the end of the list. This pattern repeats.
The reason is because there are also next nodes inbetween. This is the whitespace inbetween the elements.
To work around this, use the children attribute. This selects the child elements (instead of nodes).
function invertDivs(parentDiv) {
var parent = document.getElementById(parentDiv);
var first = parent.children[0];
console.log(first);
var second = parent.children[parent.children.length-1];
console.log(second);
document.getElementById(parentDiv).insertBefore(second, first);
}
<div id="parent">
<div id="first">Div 1</div>
<div id="second">Div 2</div>
</div>
<button onclick="invertDivs('parent');">Invert Divs</button>
The answer to your question is given in the MDN docs(https://developer.mozilla.org/en-US/docs/Web/API/Node/firstChild) for Node.firstChild. If you refer to docs you will understand why you are getting the #text as the first Node.

Use jQuery to check if all div's are hidden

How would I check if all the div's with class test are hidden. And if they are all hidden set wrap1 to hidden. Thanks.
<div id='wrap1'>
<div class="header">Header 1</div>
<div class='test'>Test 1</div>
<div class='test'>Test 2</div>
<div class='test'>Test 3</div>
</div>
UPDATE:
Thanks everyone for the really quick answers, I got it working. They were all very helpful.
You can do the check as by using selector as suggested above and to it like this:
if ( $("div.test:visible").length === 0)
$("#wrap1").hide( );
This snippet will loop all <div id="wrap#"> and hide them if the test are hidden.
$("div[id^='wrap']").each(function() {
var wrap = $(this);
if(wrap.children("div[class^='test']:visible").length == 0) {
wrap.hide();
} else {
wrap.show();
}
});
If you still want to keep your <div id="wrap#"> visible if there are no test at all (as in none in the markup), you can use the following modified snippet:
$("div[id^='wrap']").each(function() {
var wrap = $(this);
if(wrap.children("div[class^='test']").length > 0 &&
wrap.children("div[class^='test']:visible").length == 0) {
wrap.hide();
} else {
wrap.show();
}
});
There is no compelling reason to number classes (other than in edge cases). Your numbering complicates the above code as well as your CSS. It would be easier just to remove the numbering from test. (You don't need it as you can always select a subset of them using :lt(index), :gt(index), :eq(index), :first and :last.
As for numbering ids, that's fine since each id must be unique.
Better way to see if they are all visible is the count for visibility the same as the total count.
$("#wrap1 div:visible").length == $("#wrap1 div").length
jQuery("#wrap1").find("div").each(function()
{
if ($(this).is(':hidden'))
{
}
}
);

Categories

Resources