Последовательное добавление классов

const $cells = document.getElementsByClassName('cell'); // тут блоки которым добавляются классы 
const $startButton = document.getElementById('start');  // кнопка активации
$startButton.addEventListener('click', () => {
  for (let elem of $cells) {
    elem.classList.add('cell__anim');
  }
});

Задумка в том, что при нажатии на кнопку, нужно добавить класс cell__anim всем блокам $cells последовательно, т.е. друг за другом, через определенный промежуток времени.
Пытался пропихнуть setInterval внутрь функции, но все классы все равно добавлялись одновременно.

P.S.: чистый JS


Ответы (2 шт):

Автор решения: HTO HOT

Через Promise нужно

const $cells = document.getElementsByClassName('cell') // тут блоки которым добавляются классы 
const $startButton = document.getElementById('start') // кнопка активации
$startButton.addEventListener('click', async()=> { /// async
        for (let elem of $cells){
            await new Promise(resolve=>{ /// ждём исполнения функции resolve
                setTimeout(()=>{
                    elem.classList.add('cell__anim')
                    resolve()
                },300) /// Промежуток
                
            })
            
        }
})
.cell__anim{
                color: red;
            }
<ul>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
            <li class="cell">TEST</li>
        </ul>
        <button id="start">START</button>

→ Ссылка
Автор решения: yar85

Через Promise не нужно, хоть и можно :)
Почему считаю что в данном случае промис не нужен - потому что обертка в объект обещания тут не выполняет какой-либо полезной работы... То есть, обертка в промис особо сильно не вредит в любом случае, но эта конкретная задача вполне успешно решается и без такой обертки:

generateElements();

const $cells = document.querySelectorAll('.cell');
const $startButton = document.getElementById('start');
$startButton.addEventListener('click', () => {
  $cells.forEach((cell, idx) => setTimeout(() => {
    cell.classList.add('cell__anim');
  }, 100 * idx));
}, { once: true });


function generateElements() {
  document.body.insertAdjacentHTML('beforeend',
    `<section>
      ${Array.from({ length: 50 }, (_, i) => `<div class="cell">${i + 1}</div>`).join('')}
    </section>`
  );
}
section { display: flex; flex-wrap: wrap; margin: 1rem 0 0; padding: 1px; }
.cell { flex: 0 0 1.2em; line-height: 1.2; margin: -1px 0 0 -1px; padding: 0.5em; text-align: center; border: 1px solid #ccc; transition: background-color 0.3s ease; }
.cell.cell__anim { background-color: #ded; }
<button id="start">Start</button>

Тут просто устанавливается отдельный таймаут для каждого элемента, с последовательным увеличением значения задержки.


А вот и одним setInterval'ом:

generateElements();

const $cells = document.querySelectorAll('.cell');
const $startButton = document.getElementById('start');
$startButton.addEventListener('click', () => {
  let curIdx = -1;
  const interval = setInterval(() => {
    const cell = $cells[++curIdx];
    if (!cell) return void(clearInterval(interval));
    cell.classList.add('cell__anim');
  }, 100);
}, { once: true });


function generateElements() {
  document.body.insertAdjacentHTML('beforeend',
    `<section>
      ${Array.from({ length: 50 }, (_, i) => `<div class="cell">${i + 1}</div>`).join('')}
    </section>`
  );
}
section { display: flex; flex-wrap: wrap; margin: 1rem 0 0; padding: 1px; }
.cell { flex: 0 0 1.2em; line-height: 1.2; margin: -1px 0 0 -1px; padding: 0.5em; text-align: center; border: 1px solid #ccc; transition: background-color 0.3s ease; }
.cell.cell__anim { background-color: #ded; }
<button id="start">Start</button>

На каждой итерации последовательно увеличиваем индекс элемента в коллекции (пока не выйдем за ее границы) и добавляем класс элементу по текущему индексу.


В обоих случаях используется метод querySelectorAll, который возвращает коллекцию элементов в порядке их следования в документе.

Использование опции once метода addEventListener просто для упрощения - так слушатель будет автоматически удален после первого события (т.е., с этой опцией не нужно самостоятельно удалять слушатель или использовать переменную-флажок "уже запущено").



Бонусное решение - вариант применения промисов, который считаю единственно оправданным в данном случае:

generateElements();

const $cells = document.querySelectorAll('.cell');
const $startButton = document.getElementById('start');
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
$startButton.addEventListener('click', async () => {
  for (const cell of $cells) {
    cell.classList.add('cell__anim');
    await sleep(100);
  }
}, { once: true });


function generateElements() {
  document.body.insertAdjacentHTML('beforeend',
    `<section>
      ${Array.from({ length: 50 }, (_, i) => `<div class="cell">${i + 1}</div>`).join('')}
    </section>`
  );
}
section { display: flex; flex-wrap: wrap; margin: 1rem 0 0; padding: 1px; }
.cell { flex: 0 0 1.2em; line-height: 1.2; margin: -1px 0 0 -1px; padding: 0.5em; text-align: center; border: 1px solid #ccc; transition: background-color 0.3s ease; }
.cell.cell__anim { background-color: #ded; }
<button id="start">Start</button>

Плюс в значительном улучшении читаемости кода, а минус в создании лишних объектов. Оправдано ли это - вопрос личных предпочтений и решаемых задач... я использую "опциональную" промисификацию только когда одновременно выполняются три условия:

  1. если присутствуют подзадачи где накладные расходы на нее несущественны (с малым кол-вом итераций);
  2. если обертка в промис является достаточно общей, generic (когда проект большой и можно переиспользовать промисифицированный код в нескольких подзадачах, не изменяя его);
  3. когда это оправдано реальной практической пользой (не для красоты и не для "тимлид, посмотри, я умею в промисы", а для пользы дела).
→ Ссылка