如何在 HTML 元素上循环遍历类

Avatar of Chris Coyier
Chris Coyier

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

假设您有三个 HTML 类,并且 DOM 元素一次只能拥有其中一个类。

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

现在您的工作是轮换它们。 也就是说,在 HTML 元素上循环遍历类。 当某些事件发生时,如果元素上具有 state-1 类,则删除 state-1 并添加 state-2 类。 如果它具有 state-2 类,则删除该类并添加 state-3 类。 在最后一个状态,删除它,并循环回 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.

值得注意的是,我们在这里讨论的是 3 个或更多个类。 DOM 具有 .classList.toggle() 函数,甚至一个函数可以将条件作为第二个参数,但它主要用于两个类的开/关情况,而不是在类之间循环。

原因? 有很多原因。 更改类名为您提供了重新设置 DOM 中样式的强大功能,并且像这样的状态管理是现代 Web 开发的基石。 但具体地说,在我的情况下,我想做 FLIP 动画,在那里我会更改布局并触发不同状态之间的补间动画。

小心 现有类! 我看到了一些覆盖 .className 的想法,这对 DOM 元素上可能存在的其他类不友好。 所有这些都是以这种方式在类之间循环的“安全”选择。

因为这是编程,所以有很多方法可以完成此操作。 让我们覆盖其中很多方法——为了好玩。在 Twitter 上发布了这个问题,因此这些解决方案中的许多来自参与该讨论的人们。

一个冗长的 if/else 语句来在类之间循环

这就是我最初循环遍历类的方法。 那是我的大脑的工作方式。 只需为要发生的事件编写非常具体的说明即可。

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");
}

我不介意这里的冗长,因为对我来说,它是非常清楚的,并且很容易返回到这段代码并“对其进行推理”,就像他们所说的那样。 您可能会认为冗长是一个问题——当然有一种方法可以用更少的代码在类之间循环。 但一个更大的问题是它不是很有扩展性。 没有配置的迹象(例如轻松更改类的名称)或简单的方法来添加类到派对或删除它们。

我们可以使用常量,至少

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);
}

但这并没有太大区别或更好。

从旧类中删除正则表达式,增加状态,然后重新添加

这一个 来自 Tab Atkins。 由于我们知道类的格式,state-N,我们可以查找它,去掉数字,使用三元运算符增加它(但不要超过最高状态),然后添加/删除类作为在它们之间循环的一种方式。

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}`);

查找类的索引,然后删除/添加

许多在类之间循环的技术围绕着预先设置一个类数组。 这充当在类之间循环的配置,我认为这是一种聪明的方法。 一旦有了它,就可以找到相关的类来添加和删除它们。 这一个 来自 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 有一个让添加/删除技巧更简短的好主意。 事实证明它是一样的……

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

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

Mayank 有一个类似的想法,通过在数组中查找类来在类之间循环,只是不是使用 classList.contains(),而是检查当前在 DOM 元素上的类与数组中的类。

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);

这种变体是最常见的想法。 这是 Jhey 的,以及 这是 Mike Wagz 的,它设置了向前和向后移动的函数。

级联替换语句

说到 replace API,Chris Calo 有一个巧妙的想法,您可以使用 or 运算符将它们链接起来,并依靠它返回 true/false(如果它有效或无效)。 因此,您执行所有三个操作,其中一个操作将起作用!

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

Nicolò Ribaudo 得出 了相同的结论。

只循环遍历类编号

如果您预先配置了 1,您可以循环遍历类 1-3,并根据它添加/删除它们。 这 来自 Timothy Leverett,他在同一推文中列出了另一个类似选项。

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

使用 data-* 属性代替

数据属性 具有相同的特异性,因此我不反对这一点。 它们在状态处理方面可能实际上更清晰,但更好的是,它们有 一个特殊的 API 使得它们很容易操作。 Munawwar Firoz 有一个想法 可以将其缩减为一行代码

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

一个数据属性状态机

您可以 依赖 David Khourshid 为您准备好一个状态机。

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

您几乎肯定需要一个函数

给自己一点抽象,对吧? 许多想法都以这种方式编写代码,但到目前为止,我已经将其移出以专注于想法本身。 在这里,我会保留该函数。 这一个 来自 Andrea Giammarchi,其中预先设置了一个在类之间循环的唯一函数,然后您根据需要调用它。

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);

我从 Kyle Simpson 那里听到了同样的想法,几乎是一字不差。

其他?

在我 原始推文 的回复中还有更多想法,但据我所知,它们是我上面已经分享过的内容的变体。 如果我错过了你的想法,我深感歉意! 请随时在下面的评论中再次分享您的想法。 我看到没有人使用 switch 语句——这可能是一个可能性!

David Desandro 甚至录制了视频,这很棒,因为它逐渐将概念抽象化,直到它简洁但仍然可读且更灵活。

这里有一个演示 Pen,其中包含每个示例的所有代码。 它们是编号的,因此要测试另一个示例,请注释掉未注释的那个,并取消注释另一个示例。