Клик

Динамические элементы и события с помощью обработчика клика и касаний

3 минуты чтения · 5 решений

Содержание

О модуле

С этого модуля начинается JavaScript. Ховер из предыдущего модуля реагировал на присутствие курсора, но это непроизвольное действие. Клик всегда осознанный, и именно поэтому он открывает совершенно другой уровень интерактивности. Зритель больше не наблюдает за плакатом, он взаимодействует с ним.

В JavaScript клик ловится через метод addEventListener. Вы указываете элемент, тип события click и функцию, которая выполнится в момент нажатия. Внутри этой функции доступен объект события event с информацией о том, где и как произошёл клик. Дальше всё зависит от того, что вы решите сделать с этой информацией.

Переключение свойств

Самый базовый сценарий взаимодействия с кликом. Нажал на объект — он изменился. Нажал ещё раз — вернулся в исходное состояние. За это отвечает метод classList.toggle(), который добавляет CSS-класс, если его нет, и убирает, если есть.

Вся визуальная работа остаётся в CSS. JavaScript знает только одно: класс active нужно включить или выключить. Какие именно свойства при этом изменятся, определяется в стилях. Это позволяет менять поведение без правки кода, просто переписав CSS-правило для .active.

В примере ниже четыре объекта реагируют на клик по-разному, хотя JavaScript-код для каждого из них одинаковый. Разница целиком в CSS: один меняет фон, другой масштабируется, третий скругляет углы, четвёртый поворачивается.

Объяснение

Метод querySelectorAll находит все элементы с классом toggle-box внутри превью-зоны. Цикл forEach вешает на каждый из них обработчик клика. Внутри обработчика classList.toggle('active') переключает класс. CSS-правило transition: all 0.3s ease на элементе обеспечивает плавность перехода между состояниями.

фон
скейл
радиус
поворот

<div class="toggle-box toggle-bg">фон</div>
<div class="toggle-box toggle-scale">скейл</div>
<div class="toggle-box toggle-radius">радиус</div>
<div class="toggle-box toggle-rotate">поворот</div>
      

.toggle-box {
  transition: all 0.3s ease;
  cursor: pointer;
}
 
.toggle-bg.active { background: #9eff70; color: black; }
.toggle-scale.active { transform: scale(1.4); }
.toggle-radius.active { border-radius: 50%; }
.toggle-rotate.active { transform: rotate(45deg); }
      

root.querySelectorAll('.toggle-box').forEach(box => {
  box.addEventListener('click', () => {
    box.classList.toggle('active')
  })
})
/* classList.toggle — включает класс,
   если его нет, и выключает, если есть.
   CSS делает всю визуальную работу. */
      

Обратите внимание, что JavaScript здесь минимален. Он выполняет роль выключателя, а вся анимация и визуальное оформление живут в CSS. Это разделение обязанностей, к которому стоит стремиться: JS управляет состоянием, CSS отвечает за внешний вид.

classList.toggle — это швейцарский нож для интерактивности в веб-плакатах. Один вызов заменяет конструкцию из if/else с classList.add и classList.remove. Практически любой эффект по клику можно свести к переключению одного CSS-класса.

Воздействие на другой элемент

В предыдущем паттерне элемент менял сам себя. Здесь клик происходит на кнопке, а визуальный эффект применяется к соседнему объекту. Это разделение триггера и цели, которое открывает гораздо больше возможностей для построения интерактивных сцен.

Кнопка остаётся кнопкой. Квадрат рядом с ней трансформируется: меняет цвет, скругляется, поворачивается и увеличивается. Повторный клик возвращает всё обратно. JavaScript находит оба элемента через querySelector и связывает их: событие на одном, эффект на другом.

Объяснение

Функция находит два элемента внутри превью-зоны: кнопку trigger-btn и целевой блок target-box. Обработчик клика вешается на кнопку, но classList.toggle('morphed') вызывается на целевом блоке. CSS-класс .morphed описывает трансформацию: смена фона, скругление, поворот и масштабирование одновременно.

Кубическая функция cubic-bezier(0.34, 1.56, 0.64, 1) в transition создаёт эффект «перелёта» — объект слегка проскакивает конечное состояние и пружинисто возвращается.


<button class="trigger">нажми</button>
<div class="target"></div>
      

.target {
  transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
 
.target.morphed {
  background: #9eff70;
  border-radius: 50%;
  transform: rotate(180deg) scale(1.2);
}
      

const btn = root.querySelector('.trigger-btn')
const target = root.querySelector('.target-box')
 
btn.addEventListener('click', () => {
  target.classList.toggle('morphed')
})
/* Клик на кнопке, а меняется квадрат.
   Элемент-триггер и элемент-цель
   могут быть любыми. */
      

Циклическое переключение

Клик не обязан быть бинарным «включил — выключил». Объект может проходить через несколько состояний по кругу. Каждое состояние выглядит по-разному, а JavaScript считает номер текущего состояния и записывает его в data-atribute элемента.

CSS подхватывает значение data-state через селектор атрибутов [data-state="1"] и применяет соответствующее визуальное оформление. В результате JavaScript хранит только число, а CSS решает, как это число выглядит.

Объяснение

Текущее состояние хранится в HTML-атрибуте data-state. При каждом клике JavaScript считывает его через dataset.state, увеличивает на единицу и записывает обратно. Оператор остатка % (модуло) зацикливает счётчик: при totalStates = 4 последовательность идёт 0, 1, 2, 3, 0, 1, 2, 3...

CSS-селекторы вида [data-state="1"] реагируют на изменение атрибута и автоматически применяют нужные стили. JavaScript не знает ничего о цветах, формах или трансформациях. Он просто переключает число.

0

<div class="cycle-box" data-state="0">0</div>
      

.cycle-box { transition: all 0.4s ease; }
 
.cycle-box[data-state="1"] {
  background: #9eff70;
  border-radius: 50%;
}
.cycle-box[data-state="2"] {
  background: #3898e2;
  transform: rotate(45deg);
}
.cycle-box[data-state="3"] {
  background: #febbed;
  transform: scale(0.7);
  border-radius: 50%;
}
      

const box = root.querySelector('.cycle-box')
const totalStates = 4
 
box.addEventListener('click', () => {
  let state = parseInt(box.dataset.state)
  state = (state + 1) % totalStates
  box.dataset.state = state
  box.textContent = state
})
/* dataset.state хранит текущее состояние.
   Оператор % зацикливает: 0 → 1 → 2 → 3 → 0.
   CSS-селектор [data-state="N"] задаёт
   визуальное оформление каждого состояния. */
      

Паттерн с data-атрибутами и CSS-селекторами атрибутов удобен тем, что состояние видно прямо в HTML при инспекции. Откройте DevTools, найдите элемент, и вы сразу увидите data-state="2". Это упрощает отладку по сравнению с подходом через множество CSS-классов.

Создание элементов

До сих пор JavaScript работал с элементами, которые уже существовали в разметке. Здесь при каждом клике создаётся новый элемент, которого раньше не было. Кликнул в любую точку превью-зоны — в этом месте появился цветной круг, который расширился и затух. Кликнул ещё раз — ещё один круг. Каждый живёт отдельно и удаляется после завершения анимации.

Это мощный приём для веб-плакатов, потому что он позволяет зрителю буквально рисовать на экране. Следы от кликов, брызги, частицы, вспышки — всё это строится на одном и том же принципе: создать элемент, позиционировать его, добавить анимацию, удалить после завершения.

Объяснение

Метод document.createElement('div') создаёт новый элемент. Координаты клика пересчитываются из глобальных (clientX/Y) в локальные через getBoundingClientRect(), чтобы круг появлялся точно под курсором, а не где-то в углу страницы.

Цвет выбирается случайно из массива через Math.random(). Элемент добавляется в DOM через root.appendChild(dot), после чего CSS-анимация @keyframes автоматически запускается. Когда анимация завершается, событие animationend срабатывает и dot.remove() удаляет элемент из DOM, чтобы страница не засорялась десятками невидимых div.

Свойство pointer-events: none на создаваемых элементах важно: без него клики по расходящимся кругам будут перехватываться ими, а не проваливаться сквозь них к превью-зоне.

Кликайте в любое место

<div class="area">
  <div class="hint">Кликайте в любое место</div>
</div>
      

.spawned {
  position: absolute;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  animation: fade 1.5s ease forwards;
}
 
@keyframes fade {
  0%   { transform: translate(-50%,-50%) scale(0); opacity: 1; }
  100% { transform: translate(-50%,-50%) scale(3); opacity: 0; }
}
      

const colors = ['#fe3904','#9eff70','#82e1f1','#febbed','#3898e2']
 
root.addEventListener('click', (e) => {
  const rect = root.getBoundingClientRect()
  const dot = document.createElement('div')
  dot.className = 'spawned'
  dot.style.left = (e.clientX - rect.left) + 'px'
  dot.style.top = (e.clientY - rect.top) + 'px'
  dot.style.background = colors[Math.floor(Math.random() * colors.length)]
  root.appendChild(dot)
  dot.addEventListener('animationend', () => dot.remove())
})
/* createElement создаёт элемент на лету.
   getBoundingClientRect пересчитывает координаты
   относительно превью-зоны, а не окна.
   animationend удаляет элемент после анимации,
   чтобы DOM не засорялся. */
      

Если не удалять созданные элементы после анимации, DOM будет расти с каждым кликом. На сотне кликов это незаметно, но на тысяче страница начнёт тормозить. Всегда подписывайтесь на animationend и вызывайте remove(), если элемент больше не нужен.

Работа с коллекцией

Пятнадцать ячеек в сетке. Клик по ячейке выделяет её, повторный клик снимает выделение. Счётчик внизу показывает, сколько ячеек выбрано. Этот паттерн демонстрирует работу с группой однотипных элементов, где каждый элемент независим, но все вместе они формируют общее состояние.

Приём часто встречается в веб-плакатах с интерактивными сетками, мозаиками, пиксель-артом.

Объяснение

querySelectorAll находит все ячейки. forEach вешает на каждую обработчик. При клике classList.toggle переключает состояние конкретной ячейки. Затем querySelectorAll('.cell.selected').length пересчитывает общее количество выбранных ячеек по всей сетке и обновляет текст счётчика.

Обратите внимание, что пересчёт происходит заново при каждом клике, а не хранится в отдельной переменной. Это менее оптимальный подход с точки зрения производительности, но более надёжный: количество всегда соответствует реальному состоянию DOM, без риска рассинхрона между переменной и визуалом.

Выбрано: 0

<div class="cell"></div>
<!-- ...15 ячеек -->
<div class="counter">Выбрано: 0</div>
      

.cell {
  background: #f0f0f0;
  transition: all 0.2s ease;
  cursor: pointer;
}
 
.cell.selected {
  background: #fe3904;
  transform: scale(0.85);
  border-radius: 50%;
}
      

const cells = root.querySelectorAll('.cell')
const counter = root.querySelector('.counter')
 
cells.forEach(cell => {
  cell.addEventListener('click', () > {
    cell.classList.toggle('selected')
    const count = root.querySelectorAll('.cell.selected').length
    counter.textContent = `Выбрано: ${count}`
  })
})
/* querySelectorAll возвращает все элементы.
   forEach вешает обработчик на каждый.
   После каждого клика пересчитываем
   количество выбранных через .length. */
      

Чтение data-атрибутов

HTML-элементы могут хранить произвольные данные в атрибутах data. JavaScript читает эти данные через свойство dataset и использует их для визуальных изменений. В примере ниже каждый цветной квадрат в палитре хранит свой цвет и название в data-color и data-name. При клике эти значения извлекаются и применяются к соседнему блоку.

Этот подход позволяет хранить все данные в HTML, а JavaScript делает только одно: читает и применяет. Чтобы добавить новый цвет в палитру, достаточно добавить один div в разметку с нужными атрибутами, JS-код менять не придётся.

Объяснение

Атрибут data-color="#fe3904" в HTML становится доступен в JavaScript как element.dataset.color. Аналогично data-name="красный" становится element.dataset.name. Это стандартный механизм HTML5 для хранения пользовательских данных на элементах.

При клике на квадрат палитры JavaScript считывает его dataset.color и dataset.name, затем применяет цвет к фону блока .canvas и обновляет текстовые подписи. Отдельно проверяется яркость цвета, чтобы подпись оставалась читаемой: на светлых фонах текст становится чёрным.

Класс .active-swatch управляет рамкой вокруг текущего выбранного цвета. forEach снимает рамку со всех квадратов, затем classList.add ставит её на кликнутый.

красный
#fe3904

<div class="swatch"
  data-color="#fe3904"
  data-name="красный"
  style="background:#fe3904"></div>
<!-- ...остальные цвета -->
 
<div class="canvas">
  <div class="canvas-label"></div>
  <div class="canvas-value"></div>
</div>
      

.canvas {
  transition: background-color 0.4s ease;
}
 
.swatch {
  cursor: pointer;
  border: 3px solid transparent;
}
 
.swatch.active-swatch {
  border-color: black;
}
      

const swatches = root.querySelectorAll('.swatch')
const canvas = root.querySelector('.canvas')
const label = root.querySelector('.canvas-label')
const value = root.querySelector('.canvas-value')
 
swatches.forEach(sw => {
  sw.addEventListener('click', () => {
    const color = sw.dataset.color
    const name = sw.dataset.name
 
    canvas.style.backgroundColor = color
    label.textContent = name
    value.textContent = color
 
    const bright = ['#9eff70','#82e1f1','#febbed']
    label.style.color = bright.includes(color) ? 'black' : 'white'
 
    swatches.forEach(s => s.classList.remove('active-swatch'))
    sw.classList.add('active-swatch')
  })
})
/* dataset.color и dataset.name —
   данные из HTML-атрибутов data-*.
   Клик читает их и применяет к canvas.
   Никакого хардкода в JS —
   все значения хранятся в разметке. */
      

Паттерн с data-атрибутами отлично работает для интерактивных палитр, фильтров, переключателей тем. Вся конфигурация живёт в HTML, JavaScript остаётся универсальным. Когда дизайнер хочет поменять цвета или добавить новые, он правит только разметку, не заглядывая в скрипт.