About

Thursday, January 27, 2022

How to Cycle Through Classes on an HTML Element

Say you have three HTML classes, and a DOM element should only have one of them at a time:

<div class="state-1"></div>
<div class="state-2"></div>
<div class="state-3"></div>

Now your job is to rotate them. That is, cycle through classes on an HTML element. When some event occurs, if the element has state-1 on it, remove state-1 and add state-2. If it has state-2 on it, remove that and add state-3. On the last state, remove it, and cycle back to state-1.

Example of how to Cycle Through Classes on an HTML Element. Here a large <button> with an <svg> inside cycles through state-1, state-2, and state-3 classes, turning from red to yellow to green.

It’s notable that we’re talking about 3+ classes here. The DOM has a .classList.toggle() function, even one that takes a conditional as a second parameter, but that’s primarily useful in a two-class on/off situation, not cycling through classes.

Why? There is a number of reasons. Changing a class name gives you lots of power to re-style things in the DOM, and state management like that is a cornerstone of modern web development. But to be specific, in my case, I was wanting to do FLIP animations where I’d change a layout and trigger a tween animation between the different states.

Careful about existing classes! I saw some ideas that overwrote .className, which isn’t friendly toward other classes that might be on the DOM element. All these are “safe” choices for cycling through classes in that way.

Because this is programming, there are lots of ways to get this done. Let’s cover a bunch of them — for fun. I tweeted about this issue, so many of these solutions are from people who chimed into that discussion.

A verbose if/else statement to cycle through classes

This is what I did at first to cycle through classes. That’s how my brain works. Just write out very specific instructions for exactly what you want to happen:

if (el.classList.contains("state-1")) {
  el.classList.remove("state-1");
  el.classList.add("state-2");
} else if (el.classList.contains("state-2")) {
  el.classList.remove("state-2");
  el.classList.add("state-3");
} else {
  el.classList.remove("state-3");
  el.classList.add("state-1");
}

I don’t mind the verbosity here, because to me it’s super clear what’s going on and will be easy to return to this code and “reason about it,” as they say. You could consider the verbosity a problem — surely there is a way to cycle through classes with less code. But a bigger issue is that it isn’t very extensible. There is no semblance of configuration (e.g. change the names of the classes easily) or simple way to add classes to the party, or remove them.

We could use constants, at least:

const STATE_1 = "state-1";
const STATE_2 = "state-2";
const STATE_3 = "state-3";

if (el.classList.contains(STATE_1)) {
  el.classList.remove(STATE_1);
  el.classList.add(STATE_2);
} else if (el.classList.contains(STATE_2)) {
  el.classList.remove(STATE_2);
  el.classList.add(STATE_3);
} else {
  el.classList.remove(STATE_3);
  el.classList.add(STATE_1);
}

But that’s not wildly different or better.

RegEx off the old class, increment state, then re-add

This one comes from Tab Atkins. Since we know the format of the class, state-N, we can look for that, pluck off the number, use a little ternary to increment it (but not higher than the highest state), then add/remove the classes as a way of cycling through them:

const oldN = +/\bstate-(\d+)\b/.exec(el.getAttribute('class'))[1];
const newN = oldN >= 3 ? 1 : oldN+1;
el.classList.remove(`state-${oldN}`);
el.classList.add(`state-${newN}`);

Find the index of the class, then remove/add

A bunch of techniques to cycle through classes center around setting up an array of classes up front. This acts as configuration for cycling through classes, which I think is a smart way to do it. Once you have that, you can find the relevant classes for adding and removing them. This one is from Christopher Kirk-Nielsen:

const classes = ["state-1", "state-2", "state-3"];
const activeIndex = classes.findIndex((c) => el.classList.contains(c));
const nextIndex = (activeIndex + 1) % classes.length;

el.classList.remove(classes[activeIndex]);
el.classList.add(classes[nextIndex]);

Christopher had a nice idea for making the add/remove technique shorter as well. Turns out it’s the same…

el.classList.remove(classes[activeIndex]);
el.classList.add(classes[nextIndex]);

// Does the same thing.
el.classList.replace(classes[activeIndex], classes[nextIndex]);

Mayank had a similar idea for cycling through classes by finding the class in an array, only rather than using classList.contains(), you check the classes currently on the DOM element with what is in the array.

const states = ["state-1", "state-2", "state-3"];
const current = [...el.classList].find(cls => states.includes(cls));
const next = states[(states.indexOf(current) + 1) % states.length];
el.classList.remove(current);
el.classList.add(next);

Variations of this were the most common idea. Here’s Jhey’s and here’s Mike Wagz which sets up functions for moving forward and backward.

Cascading replace statements

Speaking of that replace API, Chris Calo had a clever idea where you chain them with the or operator and rely on the fact that it returns true/false if it works or doesn’t. So you do all three and one of them will work!

 el.classList.replace("state-1", "state-2") ||
 el.classList.replace("state-2", "state-3") ||
 el.classList.replace("state-3", "state-1");

Nicolò Ribaudo came to the same conclusion.

Just cycle through class numbers

If you pre-configured a 1 upfront, you could cycle through classes 1-3 and add/remove them based on that. This is from Timothy Leverett who lists another similar option in the same tweet.

// Assumes a `let s = 1` upfront
el.classList.remove(`state-${s + 1}`);
s = (s + 1) % 3;
el.classList.add(`state-${s + 1}`);

Use data-* attributes instead

Data attributes have the same specificity power, so I have no issue with this. They might actually be more clear in terms of state handling, but even better, they have a special API that makes them nice to manipulate. Munawwar Firoz had an idea that gets this down to a one-liner:

el.dataset.state = (+el.dataset.state % 3) + 1

A data attribute state machine

You can count on David Khourshid to be ready with a state machine:

const simpleMachine = {
  "1": "2",
  "2": "3",
  "3": "1"
};
el.dataset.state = simpleMachine[el.dataset.state];

You’ll almost surely want a function

Give yourself a little abstraction, right? Many of the ideas wrote code this way, but so far I’ve move it out to focus on the idea itself. Here, I’ll leave the function in. This one is from Andrea Giammarchi in which a unique function for cycling through classes is set up ahead of time, then you call it as needed:

const rotator = (classes) => ({ classList }) => {
  const current = classes.findIndex((cls) => classList.contains(cls));
  classList.remove(...classes);
  classList.add(classes[(current + 1) % classes.length]);
};

const rotate = rotator(["state-1", "state-2", "state-3"]);
rotate(el);

I heard from Kyle Simpson who had this same idea, almost character for character.

Others?

There were more ideas in the replies to my original tweet, but are, best I can tell, variations on what I’ve already shared above. Apologies if I missed yours! Feel free to share your idea again in the comments here. I see nobody used a switch statements — that could be a possibility!

David Desandro went as far as recording a video, which is wonderful as it slowly abstracts the concepts further and further until it’s succinct but still readable and much more flexible:

And here’s a demo Pen with all the code for each example in there. They are numbered, so to test out another one, comment out the one that is uncommented, and uncomment another example:


How to Cycle Through Classes on an HTML Element originally published on CSS-Tricks. You should get the newsletter and become a supporter.



source https://css-tricks.com/cycle-through-classes-html-element/

No comments:

Post a Comment