data-атрибуты и dataset

изменение≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Кнопке нужно знать, какой товар она покупает; строке списка — какой комментарий она удаляет. Прятать это в class или в глобальный массив не нужно: в HTML есть отдельное пространство имён data-*, а у элемента — свойство dataset.

Разделы темы:·

У HTML фиксированный словарь атрибутов: href, src, disabled — за каждым закреплён смысл, и браузер знает, что с ним делать. Своё product-id туда не впишешь: валидатор поругается, а завтра спецификация может занять это имя под что-то своё — и разметка внезапно начнёт что-то значить.

Поэтому в HTML выделили целое пространство имён: всё, что начинается с data-, принадлежит вам. Браузер такие атрибуты не трогает — не рисует, не меняет поведение, просто хранит. Зато их видно в DevTools, по ним работают CSS-селекторы вроде [data-state="open"], и из JavaScript к ним есть короткий вход.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

dataset — не копия и не снимок. Это живая обёртка (объект типа DOMStringMap) над всеми data--атрибутами элемента: читаете свойство — читаете атрибут, пишете свойство — переписываете атрибут. В обе стороны и без синхронизации руками.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отсутствующий ключ даёт undefined — не пустую строку и не null, который вернул бы getAttribute. Новое свойство добавляет атрибут в конец списка, а delete убирает его из разметки целиком.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В разметке имена принято писать через дефис, а в JavaScript дефис в имени свойства не наберёшь. Браузер переводит одно в другое по простому правилу: приставка data- отбрасывается, а дефис вместе со следующей строчной буквой превращается в заглавную. Обратный путь такой же: заглавная буква в свойстве становится дефисом и строчной буквой в атрибуте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
В разметкеВ JavaScriptЧто важно
data-id="7"el.dataset.idзначение — строка "7", не число
data-user-id="7"el.dataset.userIdдефис со строчной буквой схлопывается
data-order-42="ok"el.dataset["order-42"]перед цифрой дефис остаётся
атрибута нетel.dataset.nopeundefined, а не пустая строка
data-page-u-r-l="/home"el.dataset.pageURL = "/home"обратный путь: заглавная → дефис

Дефис исчезает, только если сразу за ним идёт строчная латинская буква. Перед цифрой он остаётся, и ключ приходится брать в скобках. А обратный перевод ничего не знает про аббревиатуры: каждая заглавная буква станет отдельным дефисом, и pageURL превратится в data-page-u-r-l.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

Одна деталь: event.target — это самый глубокий элемент под курсором, например span внутри пункта. У него своего data-id нет, поэтому от цели поднимаются вверх через closest до ближайшего элемента с нужным атрибутом.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Если вы пишете class="user-42", остановитесь: это data-атрибут, который стесняется своего имени.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В атрибуте живёт только текст. Число придётся пропустить через Number, флаг — сравнить со строкой, объект — сложить через JSON.stringify и разобрать через JSON.parse. Последнее уместно для маленькой конфигурации в пару полей; всё, что крупнее, в разметке только мешает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

И помните, где эти данные лежат: в открытой разметке, которую видит любой посетитель через DevTools и просмотр исходного кода. Токены, чужие адреса и цены, которым нельзя доверять на клиенте, туда не кладут. Зато data-state отлично заменяет служебные классы: селектор [data-state="open"] в CSS и одна строчка в JavaScript.

Если объект нужен только коду и никогда — стилям, разметке и другим разработчикам, храните его рядом в Map или WeakMap с элементом в роли ключа. Разметка остаётся чистой, значения не превращаются в строки, а WeakMap отпустит запись, когда на элемент не останется сильных ссылок и его заберёт сборщик мусора — удаление из документа само по себе ещё ничего не освобождает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

  • Атрибуты, начинающиеся с data-, официально отданы разработчику: браузер их хранит, но никак не интерпретирует.
  • dataset — живая двусторонняя обёртка над этими атрибутами: запись в свойство меняет разметку, изменение атрибута видно в свойстве.
  • Имена переводятся автоматически: data-user-id ↔ dataset.userId; перед цифрой дефис остаётся, а заглавные буквы при записи разворачиваются в дефисы.
  • Значения всегда строки: число получают через Number, объект — через JSON.stringify и JSON.parse. Отсутствующий ключ даёт undefined.
  • Классический приём — хранить id прямо в элементе и доставать его в делегировании через event.target.closest("[data-id]"); для тяжёлых объектов лучше WeakMap.

1/3В разметке есть <div data-user-id="7">. Что вернёт el.dataset.userId?

2/3Выполнили el.dataset.pageURL = "/home". Какой атрибут появится в разметке?

3/3Слушатель висит на списке, у пунктов есть data-id, а внутри пункта — span с текстом. Кликнули по span. Что вернёт event.target.dataset.id?

Сумма корзины из разметки

В каждом пункте списка лежат цена и количество. Посчитайте итог, читая их через dataset. Не забудьте, что оба значения — строки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Перебираем пункты и приводим оба значения к числу — иначе + склеит строки.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Короче — через reduce: [...items].reduce((sum, item) => sum + Number(item.dataset.price) * Number(item.dataset.qty), 0).

Переключатель состояния

У кнопки есть data-state со значением off. Повесьте обработчик клика, который переключает состояние между off и on. Три клика подряд должны оставить кнопку включённой.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Читаем текущее значение из dataset и записываем противоположное — атрибут в разметке обновится сам.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Дальше внешний вид можно задать одним правилом CSS вместо возни с классами: селектор [data-state="on"] сработает сразу после присваивания.