Виджеты интерфейса на ванильном JavaScript
Vanilla JS - виджеты без фреймворка
Фреймворки (React, Vue) прячут повторяющуюся работу с интерфейсом. Но под капотом те же идеи: состояние, разметка, события, очистка при уничтожении. На «голом» JavaScript это видно явно.
Здесь разбираем один законченный виджет — промо-карусель (баннер со сменой слайдов). Разметка и стили — в типовых элементах интерфейса (CSS); классы ES6 — в работе с объектами и прототипами; DOM — в работе с HTML; события — в событиях браузера.
Из чего состоит виджет
| Часть | Где живёт | Что делает |
|---|---|---|
| Разметка | HTML | Слайды, кнопки, контейнер для точек |
| Оформление | CSS | Ширина, наложение слайдов, transition, @keyframes |
| Поведение | JS-класс | Смена слайда, таймер, обработчики |
JavaScript не рисует баннер с нуля — он переключает атрибуты и классы, а браузер плавно меняет opacity благодаря CSS. Это важный принцип: состояние в JS, отображение в CSS.
Паттерн — constructor → init → destroy
В JavaScript нет встроенного метода init(), как в некоторых фреймворках. Это соглашение:
constructor— сохранить ссылки на DOM и начальные числа (индекс, задержка).init()— подписаться на события, создать индикаторы, запустить автопрокрутку.destroy()— снять таймер и обработчики, чтобы не было утечек памяти.
Зачем не всё в конструкторе:
- Конструктор не может быть
async— если понадобится загрузка данных,initудобнее. - Иногда объект создают, а
init()вызывают позже (например, когда блок вставили в страницу). - Тестам проще подменить
init, не трогая создание экземпляра.
Класс PromoCarousel — обзор
Ниже — учебная реализация (в репозитории энциклопедии она же лежит в src/components/shared/bannerCarouselEngine.js и питает интерактив на странице CSS).
const DEFAULT_AUTOPLAY_MS = 5000;
class PromoCarousel {
constructor({ root, autoplayDelay = DEFAULT_AUTOPLAY_MS, onSlideChange } = {}) {
if (!root) {
throw new Error('PromoCarousel: нужен корневой элемент');
}
this.root = root;
this.slides = [...root.querySelectorAll('[data-carousel-slide]')];
this.prevBtn = root.querySelector('[data-carousel-prev]');
this.nextBtn = root.querySelector('[data-carousel-next]');
this.indicatorsHost = root.querySelector('[data-carousel-indicators]');
this.currentIndex = 0;
this.slideCount = this.slides.length;
this.autoplayDelay = autoplayDelay;
this.autoplayInterval = null;
this.onSlideChange = onSlideChange;
this.indicators = [];
if (this.slideCount === 0) {
return;
}
this.init();
}
init() {
this.createIndicators();
this.showSlide(this.currentIndex);
this.startAutoplay();
this.bindControls();
this.bindHoverPause();
}
// ... методы ниже разобраны по блокам
}
Конструктор — что сохраняем в this
Каждый вызов document.querySelector — обход дерева DOM. Для виджета с тремя слайдами это дёшево, но привычка кэшировать узлы в конструкторе переносится и на большие интерфейсы.
this.slides— массив изquerySelectorAll(spread[...]делает обычный массив).this.currentIndex— номер видимого слайда (0 … n−1).this.autoplayInterval— id таймера; без него нельзя остановить автопрокрутку.
throw new Error, если нет root, — раннее сообщение об ошибке вместо тихого undefined дальше по коду.
init() — порядок имеет значение
init() {
this.createIndicators();
this.showSlide(this.currentIndex);
this.startAutoplay();
this.bindControls();
this.bindHoverPause();
}
- Индикаторы — сначала создаём точки (если контейнер пустой в HTML).
- showSlide — синхронизируем видимый слайд и активную точку.
- startAutoplay — только после корректного первого кадра.
- Обработчики — в конце, когда DOM в согласованном состоянии.
createIndicators() — не дублировать HTML
Если слайдов может быть 3 или 10, вручную писать десять кнопок в разметке неудобно. Скрипт создаёт кнопки в цикле:
createIndicators() {
if (!this.indicatorsHost) return;
this.indicatorsHost.innerHTML = '';
this.indicators = [];
for (let i = 0; i < this.slideCount; i++) {
const dot = document.createElement('button');
dot.type = 'button';
dot.setAttribute('data-carousel-dot', '');
dot.setAttribute('aria-label', `Слайд ${i + 1}`);
dot.addEventListener('click', () => {
this.goToSlide(i);
this.resetAutoplay();
});
this.indicatorsHost.appendChild(dot);
this.indicators.push(dot);
}
}
Почему стрелочная функция в click: () => { this.goToSlide(i) } сохраняет правильный this (экземпляр класса). Обычная function потребовала бы .bind(this).
Почему resetAutoplay после клика: пользователь осознанно выбрал слайд; через 200 мс не должно срабатывать автоматическое «догоняющее» переключение от старого таймера.
showSlide(index) — связка с CSS
showSlide(index) {
this.slides.forEach((slide, i) => {
const active = i === index;
slide.dataset.active = active ? 'true' : 'false';
slide.setAttribute('aria-hidden', active ? 'false' : 'true');
});
this.indicators.forEach((dot, i) => {
const active = i === index;
dot.dataset.active = active ? 'true' : 'false';
if (active) {
dot.setAttribute('aria-current', 'true');
} else {
dot.removeAttribute('aria-current');
}
});
this.onSlideChange?.(index);
}
В CSS завязка на атрибут:
.slide[data-active='true'] {
opacity: 1;
pointer-events: auto;
}
Один источник правды — currentIndex в JS; DOM только отражает его.
Опциональный onSlideChange — колбэк для демо или аналитики («показали слайд 2»).
next(), prev() и «зацикливание»
next() {
this.currentIndex = (this.currentIndex + 1) % this.slideCount;
this.showSlide(this.currentIndex);
}
prev() {
this.currentIndex = (this.currentIndex - 1 + this.slideCount) % this.slideCount;
this.showSlide(this.currentIndex);
}
Оператор % (остаток от деления) даёт индекс в диапазоне 0 … slideCount - 1. После последнего слайда next возвращает 0; перед первым prev — последний.
Формула (index - 1 + n) % n нужна, потому что в JavaScript % с отрицательными числами ведёт себя иначе, чем ожидают с «математическим» модулем.
Автопрокрутка — setInterval и сброс
startAutoplay() {
if (this.slideCount <= 1) return;
if (window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) return;
this.stopAutoplay();
this.autoplayInterval = window.setInterval(() => this.next(), this.autoplayDelay);
}
stopAutoplay() {
if (this.autoplayInterval) {
window.clearInterval(this.autoplayInterval);
this.autoplayInterval = null;
}
}
resetAutoplay() {
this.stopAutoplay();
this.startAutoplay();
}
| Метод | Назначение |
|---|---|
startAutoplay | Запускает новый интервал |
stopAutoplay | Очищает старый id (обязательно перед новым setInterval) |
resetAutoplay | Обёртка «перезапустить с нуля» после клика |
prefers-reduced-motion: у части пользователей в системе включено уменьшение анимации. Автосмена слайдов — тоже движение; её отключают из уважения к настройке.
Пауза при наведении:
bindHoverPause() {
this.root.addEventListener('mouseenter', () => this.stopAutoplay());
this.root.addEventListener('mouseleave', () => this.startAutoplay());
}
Пока курсор над баннером, пользователь читает; ушёл — карусель снова крутится.
Точка входа на странице
document.addEventListener('DOMContentLoaded', () => {
const root = document.querySelector('[data-promo-carousel]');
if (!root) return;
new PromoCarousel({ root });
});
DOMContentLoaded гарантирует, что querySelector найдёт узлы из HTML. Альтернатива — скрипт в конце <body> или атрибут defer у <script src="..."> (см. работа с HTML).
destroy() — зачем очищать
Если виджет убирают из страницы (SPA, динамическая вкладка), таймер и слушатели остаются, если их не снять — утечки памяти и «фантомные» переключения.
destroy() {
this.stopAutoplay();
if (this.indicatorsHost) {
this.indicatorsHost.innerHTML = '';
}
this.indicators = [];
// В продакшене: сохранить ссылки на обработчики и вызвать removeEventListener
}
В учебном коде для кнопок «назад/вперёд» иногда клонируют узел (replaceWith(cloneNode(true))), чтобы сбросить все слушатели разом — грубо, но наглядно.
Связь с классами ES6
Синтаксис class PromoCarousel { } — сахар над прототипами (подробнее в §22). Для UI-компонента это удобно: методы лежат в одном месте, this указывает на экземпляр.
Сравнение с C#/Java:
| Идея | C# | JavaScript (браузер) |
|---|---|---|
| Поля экземпляра | private int _index | this.currentIndex |
| Инициализация | конструктор | constructor + init() |
| Обработчик кнопки | Click += ... | addEventListener('click', ...) |
| Статический helper | static method | отдельная функция или static |
Главная ловушка JS — this зависит от способа вызова, не от места объявления. В обработчиках используйте стрелочные функции или bind в конструкторе.
Что улучшить дальше (без фреймворка)
- Клавиатура —
keydownна корне: стрелки влево/вправо, Escape для паузы. - Свайп —
pointerdown/pointerupи сравнение смещения (см. события). - Intersection Observer — не крутить автопрокрутку, пока баннер не виден во viewport.
- Ленивая загрузка —
loading="lazy"на<img>внутри слайдов. - Готовые библиотеки — Swiper, Glide: имеет смысл, когда нужны миниатюры, виртуальные слайды, глубокая доступность; для одного hero-блока часто хватает своего класса на ~100 строк.
Практика
- Сверстайте баннер по гайду CSS без JS — только первый слайд виден.
- Подключите
PromoCarousel(или свой класс с тем же API). - Добавьте
max-width: 720pxи проверьте на ширине окна 1400px. - В DevTools включите «Reduce motion» — убедитесь, что автопрокрутка не стартует.
- В консоли вызовите
carousel.next()— если сохраните экземпляр вwindow.demoCarousel = new PromoCarousel(...).
Итог
Промо-карусель — хороший тренажёр: HTML (смысл и a11y), CSS (слои и анимация), JS (состояние, таймеры, события). Разделение «CSS рисует переход — JS выбирает слайд» переносится на табы, аккордеоны и модальные окна — те же кирпичи, другая разметка.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Основы JavaScript - стандарт ECMAScript, модель выполнения и базовые конструкции языка. JavaScript — это язык программирования, который позволяет создавать интерактивные веб-страницы, серверные приложения и мобильные программы. Для создания массивов используется литеральная нотация. Конструктор Array не применяется. Как работать с HTML-элементами, как их создавать, менять. Простые приложения на JavaScript - базовые сценарии, структура кода и быстрый старт с практическими примерами. Расширения файлов определяют способ обработки кода средой выполнения или компилятором. История JavaScript - происхождение языка, ключевые этапы развития и влияние на современный веб. Такое именование представляет собой соглашение между разработчиками. Классический JavaScript не обеспечивает реальной приватности через подчеркивания. JavaScript содержит набор зарезервированных слов, которые имеют специальное значение в языке. Эти слова нельзя использовать в качестве идентификаторов для переменных, функций или классов. Встроенные функции JavaScript - ключевые методы массивов, строк и объектов для повседневной разработки. Этот шаблон описывает подключение внешних функций, классов или значений из других файлов. Он используется в начале файла и определяет зависимости текущего модуля. JavaScript используется для создания кроссплатформенных мобильных приложений, которые работают на iOS и Android с использованием единой кодовой базы.Основы JavaScript
Что требуется знать перед началом изучения языка программирования JavaScript
Рекомендации по разработке на JavaScript
Работа с HTML в JavaScript
Простые приложения на JavaScript
Форматы JavaScript
История языка JavaScript
Синтаксис и пунктуация в JavaScript
Ключевые слова языка JavaScript
Встроенные функции JavaScript
Структура и подключение JavaScript-кода
Применение JavaScript в вебе и за его пределами