4
\$\begingroup\$

Every time I toggle active class in a list (to dynamically change DOM content) I find that I always go to this approach to check and clear which class is active. I'm still fairly new to using pure javascript so I'm wondering if there is a better, simpler approach?

It feels like having to run a second function to clear out all active classes seems grunty.

I've included an example of how I do it.

var specialNav = document.getElementById("special-nav");

if (specialNav) {
  var navItems = specialNav.getElementsByTagName('li');
  var i;
  for (i = 0; i < navItems.length; i++) {
    navItems[i].addEventListener("click", function() {
      if (!this.classList.contains('active')) {
        clearActives(navItems);
        this.classList.toggle('active');
      }
    });
  }
}

function clearActives(classlist) {
  if (classlist) {
    for (i = 0; i < classlist.length; i++) {
      classlist[i].classList.remove('active');
    }
  }
}
.active {
  color: red
}

li {
  cursor: pointer;
}
<div id="special-nav">
  <ul>
    <li class="active"> 1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
</div>

\$\endgroup\$
1
\$\begingroup\$

Build reusable

Always try to code in such a way that the effort you put in can be reused, first in the same app, and maybe in other apps.

One would assume that you likely have more than one list that acts like radio group, or even unrelated DOM items as part of a radio group.

Your best bet is to create a simple object that tracks the state of a group and provides functionality to switch selected items.

Track current selected item

There is no need to check all the elements each time one is clicked if you keep track of the currently active element. If you have the currently selected item then you need only remove the class name from that element and add it to the new element.

The following snippet is a very basic radio group via JavaScript.

const RadioGroup = (activeClass, ...elements) => {
    var active = elements.find(el => el.classList.contains(activeClass));
    return {
        set selected(element) {
            if(elements.includes(element) && active !== element) {
                active && active.classList.remove(activeClass);
                element.classList.add(activeClass);
                active = element;
            }
        },
        get selected() { return active },
    };
}
// example create group and select first list item
const rGroup = RadioGroup("active", ...document.querySelectorAll("li"));
rGroup.selected = document.querySelector("li");

Custom interfaces

Javascript lets you build powerful reusable interfaces to suit your specific needs.

The example below creates a more sophisticated object that stores many radio groups by name and provides two interfaces and a on selected callback

Example

Note that the second group is unselected at start.

The interface also lets you turn off a selection by selecting an item not in the selectable elements list eg undefined. This is demonstrated when an item in the first group is selected, if there is one selected in the second it is turned off.

setTimeout(() => {  // for snippet rather than doc load/ed
    createGroup(document.getElementById("special-nav-one"), );
    createGroup(document.getElementById("special-nav-two"), "activeTwo");
    function createGroup(container, className = "active") {
        const group = radioGroups.addGroup(
            container.id, onItemSelected, className, 
            ...container.querySelectorAll('LI')
        );
        // NOTE property tagName is UPPERCASE
        container.addEventListener("click", e => (e.target.tagName === "LI") && (group.selected = e.target));
    }
}, 0);

function onItemSelected(group, element) {
    info.textContent = "Selected from \"" + group.name + "\" item " +  element.textContent;
    if(group.name === "special-nav-one") {
        radioGroups.select("special-nav-two"); // unselect group 2
    }
}



const radioGroups = (()=>{
    const namedGroups = {};
    function select(groupName, element) {
        const group = namedGroups[groupName];
        if (group && group.active !== element) {
            group.active && group.active.classList.remove(group.activeClass);
            if (group.elements.includes(element)) {
                group.callback && group.callback(group.API, element);
                element.classList.add(group.activeClass);
                group.active = element;
            } else {
                group.active = undefined;
            }
        }
    }
    return Object.freeze({
        select,
        addGroup(name, callback, activeClass, ...elements) {
            const active = elements.find(el => el.classList.contains(activeClass));
            const API = Object.freeze({
                get name() { return name },
                set selected(element) { select(name, element) },
                get selected() { return group.active },                
            });
            const group = namedGroups[name] = {name, elements, active,  activeClass, callback, API};
            return API;

        },
    });
})();

/** Use ****************************************************************************
 Selects element from named group
 radioGroups.select(groupName, element)  
 
 Create a new group. Will replace a group if name is same.
 group = radioGroups.select(name, callback, activeClassName, ...elements)

 Arguments
     name: Unique name of group
     callback: Called when selection changed, null or undefined if not used
     activeClassName: Name of class to toggle
     ...elements: Selectable elements
 Returns a group object
     group.selected (read, write) To select or get the current selected item.
     group.name (read only) name of group
***********************************************************************************/
.active {
  color: red;
  cursor: default;
}
.activeTwo {
  background: #AFA;
  cursor: default;
}
li {
  cursor: pointer;
}
<div id="info">Select items</div>
<div id="special-nav-one">
  <ul>
    <li class="active">A: one</li>
    <li>A: two</li>
    <li>A: three</li>
    <li>A: four</li>
  </ul>
</div>
<div id="special-nav-two">
  <ul>
    <li>B: one</li>
    <li>B: two</li>
    <li>B: three</li>
    <li>B: four</li>
    <li>B: five</li>
  </ul>
</div>

If you plan to add and remove items from the lists you will need to extend the radioGroups interfaces so that you can do so safely.

\$\endgroup\$
  • \$\begingroup\$ I always overlook the fact of creating objects in JS. This is a beautiful and detailed answer, I thank you. Definitely makes me rethink my designs going forward. I come back from a heavy react background and I've become very used to the ES6 syntax, but I rarely used JS without a framework/library. \$\endgroup\$ – Robert Ochinski Jun 12 at 20:55
2
\$\begingroup\$

Another approach could be with hidden radio inputs and :checked CSS selector :

input { display: none }
input:checked ~ * { color: red }
<label><input type="radio" name="li" checked><li>1</li></label>
<label><input type="radio" name="li"><li>2</li></label>
<label><input type="radio" name="li"><li>3</li></label>
<label><input type="radio" name="li"><li>4</li></label>

\$\endgroup\$
1
\$\begingroup\$

You could use some newer methods like spread syntax in array and forEach loop on node list for this. You could also write separate function to get sibling nodes and to toggle class based on sibling nodes.

This way you can toggle class on one element and also toggle based on other element nodes.

const li = document.querySelectorAll('li');

function siblings(elem) {
  const nodes = [...elem.parentNode.children]
  return nodes.filter(node => node !== elem)
}

function toggleClass(elem, cls) {
  elem.classList.toggle(cls);
  siblings(elem).forEach(node => {
    node.classList.remove(cls)
  })
}

li.forEach(el => {
  el.addEventListener('click', function() {
    toggleClass(this, 'active')
  })
})
.active {
  color: red;
}

li {
  cursor: pointer;
  user-select: none;
}
<div id="special-nav">
  <ul>
    <li class="active"> 1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
</div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.