Habizsabi
Habizsabi

Reputation: 47

Is there a way to change css selector with 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

Upvotes: 1

Views: 573

Answers (2)

KooiInc
KooiInc

Reputation: 122906

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>

Upvotes: 1

tacoshy
tacoshy

Reputation: 13002

@Kooilnc Answer works but has 2 major issues:

  1. It is inefficient and consumes way more resources and time than necessary
  2. It does not scale
  3. (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?

  1. It creates a NodeList out of all the elements
  2. Then it takes the index from the element with the active class in the NodeList
  3. 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
  4. It adds the active class to the next element

Upvotes: 0

Related Questions