DOU 2.0: Новий красивий слайдер замість старого некрасивого

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Реактивність без бібліотек, анімація скролу без transform, компонент без фреймворку? Вам не здалося. Я зробив слайдер, який працює без жодної зовнішньої залежності.

Все — чистий JavaScript, Custom Elements, Shadow DOM і трішки магії з нативним CSS-скролом. Зараз я працюю над одним дуже таємним пет-проєктом, в рамках якого я якось заглянув «під капот» слайдера на головній сторінці DOU. Передати свої відчуття від побаченого мені буде важко — різко перестало вистачати повітря, відмовляли ноги, слабли руки, паморочилась голова. І так, на мою особисту думку там усе настільки жахливо.

Однак я є палким прихильником однієї поговірки про пусті балачки та транспортацію мішків. І це якраз той випадок, коли я цілком можу взятися за мішки. Тому прийняв рішення буквально узяти ситуацію до своїх рук та розробити мінімалістичне рішення, від якого я не буду відчувати усі вищенаведені симптоми. Хотілося створити компонент, який можна просто вставити на сторінку і він працюватиме (майже скрізь, але про це пізніше).

Ідея проста: мінімум коду, максимум нативних можливостей, цікавих рішень і, звісно, ніяких фреймворків. Щоб усе це не перетворилося на набір хаотичних експериментів, я вирішив виписати для себе мінімальні вимоги, аби не заплутатися в тому, чого хочу досягти в результаті.

Вимоги

Перед тим, як братися до роботи, варто зрозуміти, що саме ми намагаємось зробити. Я розібрав існуючий слайдер і окреслив мінімальну поведінку, яку треба відтворити.

Функціональні вимоги

  • Відображення слайдів: Слайдер має відображати набір слайдів. Слайдом може бути будь-який елемент, вкладений
    у компонент слайдера.
  • Автоматичне перемикання слайдів: Слайдер має автоматично перемикати слайди через заданий часовий інтервал;
  • Ручне керування: Користувач повинен мати можливість перемикати слайди вручну (наприклад, за допомогою кнопок «вперед» та «назад», індикаторів або жестів);
  • Синхронізація з прокруткою: Під час самостійного переміщення між слайдами за допомогою прокрутки активний слайд має оновлюватися відповідно до позиції прокрутки;
  • Призупинення автоматичного перемикання під час взаємодії: Автоматичне перемикання має зупинятися, коли користувач взаємодіє зі слайдером (наприклад, при фокусуванні чи наведенні курсора), та відновлюватися після завершення взаємодії;

Технічні вимоги

  • Жоднісіньких залежностей: Бо такий шлях. Ніяких бібліотек чи фреймворків, тільки чистий JavaScript;
  • Custom Element: По-перше, давно пора. По-друге, це дає можливість створити компонент з повною ізоляцією логіки та стилів;
  • Лише функціональна стилізація: Компонент не повинен мати власних презентаційних стилів. Усе, що він робить — забезпечує функціональність;
  • Якомога менше коду: Особистий принцип. Чим менше коду, тим менше помилок і складності. І звісно, ніяких езотеричних (для мене) рішень — усе повинно бути зрозумілим і логічним;
  • A11y без додаткових зусиль: Доступність вирішується за замовчуванням через стандартні HTML-елементи та нативні API. Усе, що стосується доступності, перевіряється через автоматичні підказки ШІ та базові інструменти перевірки, без додаткових витрат часу на ручне налаштування або експерименти.
  • Підтримка хоча б Chrome, Edge та Firefox: Підтримка Safari суперечить наступному принципу «мінімально необхідних зусиль», тож звиняйте;

Критерії здорового ґлузду

А ще, аби не втратити розум під час написання компоненту, я вирішив дотримуватись кількох простих принципів:

  • Мінімально необхідні зусилля: Досягнути поставлених вимог з найменшими витратами часу й коду.
  • Жодних костилів: Якщо щось вимагає костильного підходу — я цього не роблю.
  • Реалістичні дедлайни: Якщо якась ідея займає більше одного вільного вечора без помітного прогресу — я її відкидаю.

Загалом, я вирішив, що цей компонент повинен бути якомога простішим.

Приклад використання

На сторінці цей слайдер виглядає і працює наступним чином:

А ось так просто його додавати до розмітки:

<dou-slider interval="3000">
  <div class="slide">Слайд 1</div>
  <div class="slide">Слайд 2</div>
  <div class="slide">Слайд 3</div>

  <span slot="prev" class="icon"></span>
  <span slot="next" class="icon"></span>
</dou-slider>

Поклали слайди, за бажанням замінили іконки, вказали інтервал перемикання. А як це все функціонує, ми з вами розбиратимемо далі, і почати я пропоную зі знайомства з кодом самого компонента DouSlider.

Custom Element

Якщо ви ще раптом не знайомі з Custom Elements — це таке API, що дозволяє вам створювати власні HTML-елементи. Саме елементи, не теги. Теги ви і так можете писати, які лишень в голову прийде, бравзер просто сприйматиме їх як найпростіші елементи, а от Custom Elements дозволяє ще й визначати їхню поведінку. І, буду відвертим, там такий бездонний колодязь можливостей, що я дізнавався дійсно нові для себе речі буквально нещодавно, під час роботи над слайдером.

class DouSlider extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }
}

customElements.define("dou-slider", DouSlider);

Найперше ми оголошуємо клас DouSlider, наслідуючись від HTMLElement та глобально реєструємо клас компонента в державному реєстрі кастомних елементів (сподіваюсь, інформацію звідти скоро можна буде побачити в Резерв+). До речі, скасувати реєстрацію уже неможливо. Ще зверніть увагу — імʼя класу взагалі не обовʼязково бути схожим на імʼя тегу, під яким реєструється компонент. Ми просто кажемо бравзеру, що як він зустріне ось такий тег, то це не діти бавились, а треба використати кастомний клас.

В конструкторі нічого надзвичайного не відбувається — викликаємо super() для забезпечення наслідування від батьківського класу, а ще приєднуємо ShadowDOM, який потрібен для приховання нутрощів нашого компоненту від зовнішнього світу.

Ініціалізація компонента

Наступний важливий крок — створити усю необхідну нам начинку елемента, а також оживити цю начинку за допомогою інтерактивности. І для цього є чудовий lifecycle method connectedCallback, який викликається після того, як наш елемент потрапляє до DOM. Насправді, якщо вашому елементу не треба нічого знати про навколишній світ, то цю ініціалізацію можна провести і в конструкторі, який викликається до вставки в DOM. Але я обрав свій шлях самурая і вирішив використати connectedCallback для більшої семантичності.

connectedCallback() {
  this.#render();
  this.#setupTimer();
  this.#currentIndex = new ReactiveCurrentIndex(this.shadowRoot.getElementById("scrollMarkerGroup"));
  this.#registerEventHandlers();
}

Першим ділом я викликаю метод #render(), аби вставити усю базову розмітку в shadowRoot:

#render() {
  this.shadowRoot.innerHTML = carouselTemplate;
}

Чому приватний метод? Як на мене, це доволі логічно — якщо я вже ховаю всі нутрощі компонента, то й логіку варто тримати за замком. Приватні методи саме для цього й придумали.

Але є цікавинка — якщо бавитися з DevTools, то усі приватні поля можна мало того, що бачити, їх ще можна й перезаписувати. В звичайному ж контексті ви будете ловити SyntaxError при спробі навіть прочитати приватне поле.

Що відображає компонент

Розмітка компонента

Я знаю, що тут усі неймовірні JavaScript-гуру, тому HTML та CSS нікого не цікавлять, але для мене це не аргументи. Тому хочете ви того, чи ні, але я поясню зараз, шо відбувається в шаблоні компонента:

<header id=controls part=controls>
  <button part=button data-action="${ButtonActions.Prev}" aria-label="Previous Slide" aria-controls="scrollview">
    <slot name=prev></slot>
  </button>
  <form id="scrollMarkerGroup" part="scroll-marker-group" aria-label="Carousel Indicators"></form>
  <button part=button data-action="${ButtonActions.Next}" aria-label="Next Slide" aria-controls="scrollview">
    <slot name=next></slot>
  </button>
</header>

<div id=scrollview role="region" aria-label="Image Slider" tabindex="0">
  <slot id=scrollviewContent></slot>
</div>

<template id=scrollMarkerTemplate>
  <input type=radio id=scrollMarker name=targetCurrentIndex part="scroll-marker"/>
</template>

Є header, що міститиме елементи керування слайдером: кнопки вперед/назад, а також маркери, які а) відображають поточний активний слайд і б) перемикають активний слайд.

І є <div id=scrollview> — область, в якій будуть знаходитися слайди, і яка відповідатиме за плавне і красиве перемикання слайдів. Ні, не за допомогою transform. Так, можна навіть без CSS.

Ну а <template id=scrollMarkerTemplate> — це шаблончик для тих самих маркерів, які згодом генеруються динамічно, в залежності від кількості вставлених слайдів.

Зараз я хотів би звернути вашу увагу на декілька цікавих моментів, які допоможуть спростити код в майбутньому. По-перше, я використовую data-action на кнопках. Якщо ви памʼятаєте мою статтю про делегування подій, то одразу впізнали цей підхід, якщо ж ні — то обовʼязково прочитайте. А якщо коротко — я можу обробляти різноманітні actions з будь-яких елементів за допомогою одного обробника подій.

По-друге, я використовую як іменовані, так і неіменовані слоти. В неіменований слот буде поміщено увесь контент, який ви помістите всередину свого елемента:

<dou-slider>
  <div class="slide">1</div>
  <div class="slide">2</div>
  <div class="slide">3</div>
</dou-slider>

і який при цьому не матиме встановленого значення атрибуту slot. Тобто це місце вставки вашого контенту за замовчуванням. А от для того, аби вставити щось у визначене місце, треба користуватись іменованими слотами:

<slot name="foo"></slot>
…
<x-component>
  <div slot="foo"></div>
</x-component>

І от у моєму випадку, якщо я покладу в елемент будь-який контент, він поміститься одразу в слот без імені. А для того, аби підкласти іконки для кнопок, мені достатньо буде вказати їм відповідні слоти:

<dou-slider>
  <div class="slide"></div>
  <div class="slide"></div>

  <div class="icon" slot="prev"></div>
  <div class="icon" slot="next"></div>
</dou-slider>

А далі зверніть увагу на атрибути part на деяких елементах. Вони дають можливість стилізувати певні елементи всередині ShadowDOM, який, як відомо, дуже не любить зовнішні стилі. Окрім custom properties, вони ж CSS-змінні, які він з радістю пропускає. Стилізацію ззовні ми розберемо аж в кінці статті, тому або майте терпіння, або біс із ним, тим читанням, ось вам посилання на цю частину.

template дає певний імперативний API для швидкого клонування елементів та вставки їх в DOM. Звідти братиму шаблон для маркерів, які, як бачите, є радіоінпутами.

Функціональні стилі

:where(*) { margin: 0; padding: 0; box-sizing: border-box; }
:host { display: flex; flex-direction: column; }

button { background: none; }
button ::slotted(*) { pointer-events: none; }

#scrollview {
  display: flex;
  flex: 1;
  overflow-x: scroll;
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
}

#scrollview ::slotted(*) {
  scroll-snap-align: center;
  flex: 0 0 100%;
}

Тепер швиденько розберемося, що відбувається в стилях. І це, насправді, дуже важливо, бо по факту функціонал саме слайдера у мене базується на нативному скролі, а не на тому ж transform. Зупинюся на #scrollview, бо скидання стилів на початку і так зрозумілі. Тако от, нас цікавлять наступні стилі, які безпосередньо впливають на те, як працює скрол:

overflow-x: scroll;

Очевидно, задаємо слайдеру стандартну поведінку зі скролом по горизонталі.

scrollbar-width: none;

О, а це вже цікаво. В такий спосіб я прибираю скролбар повністю, бо в нашому випадку ми обійдемося саморобними маркерами. І так, це працює в Firefox.

scroll-snap-type: x mandatory;

Ось так ми описуємо поведінку, яка забезпечить «паркування» слайдів по завершенні скролу. Тобто, якщо ми проскролимо так, що буде видно два слайди, які умовно припарковані ні там, ні сям, цей контейнер автоматично «доскролить» таким чином, аби слайд, який стирчить у вʼюпорті найбільше, зайняв основну позицію. В нашому випадку — аби було видно тільки його.

scroll-behavior: smooth;

Ця властивість забезпечує ту саму плавність «доскролу», та й скролу взагалі. Саме вона дозволила мені відмовитися від звичного підходу з transform. Єдиний недолік — неможливо встановити ані швидкість, ані часову функцію
анімації. Але у цьому випадку цим можна абсолютно знехтувати.

overscroll-behavior-x: contain;

І на завершення — маленький лайвхак, який блокуватиме (не в Safari) скрол на батьківському контейнері, якщо в цьому контейнері ви доскролились до ручки краю. Найбільша проблема, яку мені вдалося цим вирішити, це блокування жесту на трекпаді, який переходить назад по історії бравзера, який неодмінно буде тригеритись, якщо скролити слайдер, знаходячись на першому слайді. Далі йде два важливих правила для слайдів:

#scrollview ::slotted(*) {
  scroll-snap-align: center;
  flex: 0 0 100%;
}

Тут scroll-snap-align: center задає правило паркування слайду, тобто він має паркуватися посередині свого батька зі скролом, а flex: 0 0 100% змушує кожен слайд займати всю ширину контейнера, лишаючись при цьому вирівняними по горизонталі в рядочок. Ще не знудились? Далі буде жвавіше, бо ми переходимо аж до другого рядочка connectedCallback, на якому ініціалізується...

Таймер

Цей таймер використовується для того, аби запустити перехід до наступного слайду, щойно поточний слайд завершить «паркуватися». Чому не інтервал? Інтервалом в цьому випадку набагато важче оперувати, особливо коли потрібно мати тонкий контроль над перезапуском. Я детальніше далі розберу конкретні кейси.

Один з них — можливість зупиняти і перезапускати таймер при наведенні курсора на слайд, аби призупинити цикл прокрутки і дати можливість користувачу спокійно прийняти рішення, тиснути йому в посилання, чи ні. Для керування ж таймером в контексті слайдера я виділив кілька окремих методів:

#suspendTimer() {
  this.#timer.disable();
}

#resumeTimer() {
  this.#timer.enable();
}

Якщо подивитесь до методу Timer::disable, то побачите, що він не ставить сам таймер на павзу, а зупиняє його повністю, одночасно забороняючи його перезапуск. А Timer::enable цю заборону знімає, при цьому запускаючи таймер заново. Тобто технічно це suspend і resume, просто без збереження поточного прогресу відліку часу. Сподіваюся, мета існування саме такої реалізації таймера вам зрозуміла і прийнятна, бо саме час перейти до третього рядочка connectedCallback, де починається справжня магія.

ReactiveCurrentIndex

this.#currentIndex = new ReactiveCurrentIndex(
  this.shadowRoot.getElementById("scrollMarkerGroup")
);

Тут пропоную трохи зупинитися і розібратися, що відбувається в класі ReactiveCurrentIndex.

А відбувається тут нахабне використання особливостей поведінки HTML Form Elements для забезпечення тієї самої «реактивности». Якщо конкретніше: комбінація можливості перемикати активний input[type=radio] та можливість звертатися до елементів форми всередині form за іменем. Дивіться, в чому фокус. Маючи посилання на елемент форми і набір радіо-інпутів під одним іменем, ми маємо доступ до цікавого обʼєкту типу RadioNodeList:

<input type=radio name=targetCurrentIndex value=0 …>
<input type=radio name=targetCurrentIndex value=1 …>
form.elements.targetCurrentIndex // <-- Ось.

який виглядає як звичайний NodeList, але збоку у нього теліпається value, в якому, очевидно, лежатиме значення атрибута value активного радіо-інпута. Тобто, звернувшись в будь-який момент до цього обʼєкта, я матиму актуальне значення.

А тепер питання — коли зазвичай мені потрібно дізнатися поточне значення? Правильно, коли воно щойно змінилося (рєактівщина, ммм...) А як дізнатися, коли якесь значення всередині форми
змінилося? Знову правильно — повісити listener на подію change. Ну або внаглу втулити
onchange. В цьому випадку це абсолютно припустимо, бо ця форма більш ні для чого мені не потрібна.

А тепер до справді чорної магії: якщо встановити вручну значення value у RadioNodeList, то відповідний радіо-інпут автоматично оновить свій стан! Угумсь, він стане checked. Візуально теж. Але є одне але — при цьому подію change не буде створено. А нашо вона нам? А для того, щоб завершити повний цикл реактивності.

Приходьте послухати Сергія на DOU Day👇

Дивіться самі. У мене є сутність ReactiveCurrentIndex, за допомогою якої я можу реагувати на зміну
її value, викликаючи якийсь колбек.

Ця сутність одночасно вміє слідкувати за активним станом форми, який синхронізується з UI. Тобто я очікую, що натиснувши на радіо-інпут, мій код зможе зреагувати на зміну поточного індексу, а якщо я присвою значення value ззовні, то активний індикатор автоматично оновиться. От тому в сетері value окрім того, що я оновлюю значення активного інпуту, я ще й «стріляю» подію change, бо саме на ній висить зовнішня підписка.

Інтерактивність

this.#registerEventHandlers();

Отут буде дуже багато чого цікавого, і це те місце, де все сходиться докупи.

Обробка кнопок

…
#registerEventHandlers() {
  this.shadowRoot.getElementById("controls").onclick = (event) => {
    if (!event.target.dataset?.action) {
      return;
    }
  this.#handleButtonClick(event.target.dataset.action);
};

…
#handleButtonClick(action) {
  switch (action) {
    case ButtonActions.Next:
      this.showNextSlide();
        break
    case ButtonActions.Prev:
      this.showPrevSlide();
        break;
    default:
      return;
  }
}

Ось воно, священне делегування подій! Я вішаю лише один обробник кліків, і обробляю в ньому лише події від елементів з атрибутом data-action. А далі, абсолютно абстрагуючись від того, звідки цей action прилетів, обробляю його.

Зупинка та перезапуск таймера при взаємодії

this.shadowRoot.onfocusin = () => {
  this.suspendTimer();
};

this.shadowRoot.onfocusout = () => {
  this.resumeTimer();
};

…
this.#scrollviewRef.onmouseenter = () => {
  this.suspendTimer();
};

this.#scrollviewRef.onmouseleave = () => {
  this.resumeTimer();
};

В цих обробниках подій я втілюю якраз вимогу зупиняти таймер коли користувач взаємодіє зі слайдом — в даному випадку при наведенні курсора, або ж коли якийсь елемент всередині слайду отримує фокус. І, так, я був дуже приємно здивований тим, що підхід з використанням нативного скролу для слайдер виявився напрочуд доступним. Наприклад, слайдер сам прокручується на наступний слайд, якщо «табити» по активним елементам всередині слайдів. В моєму випадку це посилання на статтю.

І реагувати на фокус всередині слайд стало можливим завдяки подіям focusin та focusout, які спливають, на відміну від focus. Що важливо — focusin це не подія «фокус всередині», а «отримання фокусу елементом». А focusout це коли фокус «переходить» з іншого елемента. І виявляється, що ця подія ще й містить посилання на елемент, який втрачає фокус.

Підписка на зміну стану поточного індексу активного слайда

this.#currentIndex.onvaluechange = () => {
  this.#scrollToCurrentSlide();
};

Ось тут я «підписуюсь» на зміну стану в моєму штучному обʼєкті CurrentIndexState, в якому
onvaluechange викликається явно:

// class CurrentIndexState
constructor(formRef) {
  this.#formRef = formRef;
  this.#formRef.onchange = () => {
    this.onvaluechange?.(this.value);
  };
}

Оновлення маркерів

this.#scrollviewRef.onslotchange = () => {
  this.#slideCountCache = null;
  this.#renderMarkers();
};

…
#renderMarkers() {
  const markerGroup = this.shadowRoot.getElementById("scrollMarkerGroup");
  const markerTemplate = this.shadowRoot.getElementById("scrollMarkerTemplate");
  const fragment = document.createDocumentFragment();
  for (let index = 0; index < this.#slideCount; index++) {
    const input = markerTemplate.cloneNode(true).content.firstElementChild;
    input.value = index;
    input.checked = index === 0;
    fragment.append(input);
  }
  markerGroup.replaceChildren(fragment);
}

Що відбувається? По-перше, я підписуюсь на подію slotchange, яку викидає будь-який слот при зміні свого вмісту. Я не можу згенерувати маркери в іншому місці, бо ні під час виконання конструктора, ні в самому connectedCallback у мене ще немає доступу до вмісту слота. Тому треба підписатися, щоб слот сам сказав — «я маю новий вміст».

Ну а тоді я отримую кількість слайдів, переданих в компонент, і від того створюю відповідну кількість маркерів. this.#slideCount є геттером, і ми розглянемо, чому я обрав такий підхід, дещо пізніше. Самі ж маркери створюю доволі просто: за допомогою template створюю новий інстанс радіо-інпуту, вставляю його в documentFragment, а потім одним махом заміняю усі дочірні елементи в markerGroup (це, якраз, ота форма, з якою я і роблю реактивну магію). Ніяких innerHTML = '', ніякого while і видалення нащадків по одному. Раз — і готово!

Автоматичне перемикання слайдів

this.#scrollviewRef.onscrollend = ({ target: { scrollLeft } }) => {
  this.#currentIndex.value = Math.round(scrollLeft / this.#scrollviewWidthCache);
  this.#timer.restart();
};

Що Chrome (і його похідні), що Firefox нині підтримують подію scrollend (але не Safari), про яку, гадаю, просто мріяло безліч поколінь веброзробників. Ця подія транслюється, як не дивно, саме по завершенні скролу, і це якраз те місце, в якому я хотів би перезапустити таймер. Що я і роблю. Чому я одночасно оновлюю #currentIndex.value? Бо scrollend може відбутися і без участі таймеру — коли ми запускаємо наступний слайд кнопкою, або ж явно прокруткою, наприклад тачпадом.

В такому випадку необхідно оновити поточний індекс.Тобто індекс ми оновлюємо на випадок, якщо скрол запустили ми самі, а таймер перезапускаємо на випадок, якщо перемикання відбулося автоматично. І в обох випадках в логіці присутні перевірки, щоб не запускати зайвого коду. Наприклад, якщо ми самі скролимо слайди, таймер не запуститься, бо буде в цей час заблокований. А оновлення індексу не запустить обробник події у випадку, якщо це проста синхронізація, і внутрішнє значення, та значення, яке ми присвоюємо, співпадають.

Оновлення кешу ширини контейнера

#scrollviewResizeObserver = new ResizeObserver(([entry]) => {
  this.#scrollviewWidthCache = entry.contentRect.width;
});

…
this.#scrollviewResizeObserver.observe(this.#scrollviewRef);

Аби слайдеру не ставало хижо при зміні його розмірів, я додав ResizeObserver, який відслідковує зміни розміру елементу scrollvew. В двох словах — це як onresize, але для окремого елемента. Як ви могли бачити вище, ця ширина використовується для обчислення поточного індексу слайду наприкінці скролу. І аби не смикати зайвий раз DOM-елемент, я це значення кешую.

Деініціалізація

Я довго вагався, чи додавати це взагалі в код, бо, по факту, особливих протікань памʼяті тут не передбачається, адже очікується, що слайдер існуватиме увесь час існування сторінки, і піде в небуття разом із нею. Але для наглядности я вирішив усе таки дещо підчистити:

disconnectedCallback() {
  this.#scrollviewResizeObserver.disconnect();
  this.#scrollviewResizeObserver = null;
  this.#timer.disable();
}

В disconnectedCallback я вибірково очищаю ResizeObserver та зупиняю таймер. Ще, можливо, пасує очистити мій реактивний стейт. Сама суть цього колбеку полягає в тому, що він викликається перед видаленням нашого кастомного елемента з DOM, і якщо передбачається, що сторінка існуватиме і далі, то це якраз чудове місце, в якому можна влаштувати генеральне прибирання памʼяті.

Як працює скрол

Для реалізації найголовнішої фічі слайдера, а саме безпосреденьо скролу, або ж прокрутки, я обрав нативний скрол замість transform. В інших розділах я побіжно згадуватиму ті чи інші переваги, які вдалось отримати, а зараз окреслю в загальному. Так от, нативний скрол дає мені вбудовані можливості як для запуску, так і для відслідковування прокрутки. Той самий метод scrollTo чи подія scrollend.

А ще, працюючи над скролом, часто із легким сумом згадував метод scrollIntoView, бо його аналог на рівні контейнера ще більше спростив би логіку прокрутки. Для запуску скролу я маю ось такий метод, в якому явно задаю позицію, до якої треба проскролити, знаючи поточний індекс та ширину контейнера:

#scrollToCurrentSlide() { 
  this.#scrollviewRef.scrollTo({
    left: this.#currentIndex.value * this.#scrollviewWidthCache
  });
}

Викликаю я його, як ви вже могли бачити, в одному-єдиному місці:

this.#currentIndex.onvaluechange = () => {
  this.#scrollToCurrentSlide();
};

А от змінюю значення #currentIndex.value я в кількох місцях, покладаючись на саморобну реактивність:

this.#scrollviewRef.onscrollend = ({ target: { scrollLeft } }) => {
  this.#currentIndex.value = Math.round(scrollLeft / this.#scrollviewWidthCache);
  this.#timer.restart();
};

…
showNextSlide() {
  this.#currentIndex.value = (this.#currentIndex.value + 1) % this.#slideCount;
}

showPrevSlide() {
  this.#currentIndex.value = (this.#currentIndex.value + this.#slideCount - 1) % this.#slideCount;
}

До речі, оці чудернацькі розрахунки це ніщо інше, як «зациклювання» перемикання слайдів. Тобто, дійшовши до останнього, ми переключимось на перший. А якщо будемо відмотувати назад, то з першого перескочимо на останній слайд.

Гетер, сетер, два кеша

Кешування DOM-елементів

Аби мінімізувати кількість запитів до DOM-елементів, я вирішив скористатися такою прекрасною функціональністю JavaScript-обʼєктів, а в моєму випадку класів, як гетери та сетери. Наприклад, ось кешування посилання на scrollview:

#scrollviewRefCache = null;
get #scrollviewRef() {
  return (this.#scrollviewRefCache ??= this.shadowRoot.getElementById("scrollview"));
}

Здавалось би, яка різниця, напряму чи ось так? Але різниця є. Не в цьому прикладі, звичайно, тут втрати будуть фактично не помітні, але на складніших випадках, якщо часто запитувати елементи через DOM API, падіння швидкодії може бути відчутним. Ви помітили оцей прикол з ??=? Розберемось з ним трошкти згодом, тримайте в голові.

Кешування значень властивостей DOM-елементів

Аби мати доступ до актуальної кількості слайдів всередині слота, я використав доволі хитромудру конструкцію, але знову ж таки, вийшло доволі «реактивно»:

#slideCountCache = null;
get #slideCount() {
  return (this.#slideCountCache ??= this.shadowRoot.getElementById("scrollviewContent").assignedElements().length);
}

Спробую пояснити. Отже, #slideCount — це суто гетер, який, по факту, виконується щоразу, як до нього звертається якийсь код. Тому їх бажано якось або мемоїзувати, або кешувати в інший спосіб. У мене в ролі кеша виступає #slideCountCache. В ньому зберігається довжина колекції з assignedElements слота, в якому знаходяться слайди. Мені потрібно знати точну кількість слайдів в будь-який момент часу, тому і з кешем тут трошки хитро. Давайте поки розберемось з ??=. Якщо коротко, цей оператор заміняє ось таку конструкцію:

get #slideCount() {
  if (this.#slideCountCache === null || this.#slideCountCache === undefined) {
    this.#slideCountCache = this.shadowRoot.getElementById("scrollviewContent").assignedElements().length
  }

  return this.#slideCountCache;
}

Тож оновлення кеша відбудеться лише в тому випадку, коли #slideCountCache буде null.
І я це роблю вручну ось в цьому місці:

this.#scrollviewRef.onslotchange = () => {
  this.#slideCountCache = null; // <-- в цьому місці
  this.#renderMarkers();
};

Тобто я очікую, що при slotchange кількість слайдів може змінитися, тому інвалідую кеш. А при наступному звертання до #slideCount цей кеш оновиться, і гетер вертатиме кешоване значення. Ось так.

Кешування інстансів

В методі #setupTimer ??= використовується для лінивої ініціалізації обʼєкта таймера, що дозволяє мені створити його лише раз, а при усіх послідовних викликах користуватися уже готовим посиланням на інстанс.

const timer = (this.#timer ??= new Timer(this.interval, () => this.showNextSlide()));

Публічне API компонента

Звичайно ж, наглухо зашитий слайдер нікому не цікавий, тож я вирішив деякий його функціонал винести в публічне API, а саме налаштування тривалості перемикання слайдів і методи для цього перемикання.

Керування перемиканням

Дуже просто. Назовні стирчать два публічні методи: showNextSlide та showPrevSlide, які виконують рівно те, як називаються.

document.querySelector('dou-slider').showNextSlide();
document.querySelector('dou-slider').showPrevSlide();

Оновлення внутрішнього стану за допомогою атрибутів

У Custom Elements є чудова вбудована можливість слідкувати за зміною атрибутів DOM-елементу, і реагувати на неї всередині нашого коду. Для початку треба явно вказати, за якими атрибутами ми хочемо слідкувати:

const Attributes = {
  Interval: "interval",
};

…
static get observedAttributes() {
  return [Attributes.Interval];
}

І далі використати ще один lifecycle callback attributeChangedCallback:

attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue === newValue) {
    return;
  }
  
  if (name === Attributes.Interval) {
    this.#intervalCache = null;
    this.#setupTimer();
  }
}

Перевірку на спробу присвоїти старе значення треба робити руками, це вам не фреймворки, де за вас усе подумали. Конкретно в цьому прикладі я скидаю кеш для interval, значення якого оновиться уже всередині метода #setupTimer при спробі його прочитати. А #setupTimer в свою чергу просто оновить значення duration та перезапустить таймер.

Тобто змінювати періодичність можна в будь-який момент, просто змінивши значення атрибуту interval елемента dou-slider! Але і це ще не все. Ви помітили, що interval, на відміну від інших, публічний? Так, я дозволяю змінювати його значення і через JavaScript! Але не просто так, а з цікавим фокусом:

get interval() {
  return (this.#intervalCache ??= DouSlider.getPositiveInterval(parseInt(this.getAttribute(Attributes.Interval))));
}

set interval(interval) {
  this.#intervalCache = null;
  this.setAttribute(Attributes.Interval, DouSlider.getPositiveInterval(interval));
}

Гетер має виглядати уже знайомо для вас — кеш через окреме поле і nullish coalescing assignment, все просто. А от в сетері я скидаю цей кеш, і оновлюю атрибут. Таким чином і поле і атрибут синхронізовані. Подібна поведінка в стандартних елементах є, до прикладу, у атрибуту id. О, а ще зробив невеличкий статичний метод для валідації значення інтервалу.

По-перше, атрибути завжди мають рядковий тип (string), це вам не реакт, нагадую, а по-друге, порушити просторово-часовий континуум мені не дуже хочеться, тож треба слідкувати, аби не можна було присвоїти відʼємні значення інтервалу:

static getPositiveInterval(interval) {
  const num = parseInt(interval);
  return Number.isInteger(num) ? Math.max(num, 0) : 0;
}

Ну і, звичайно, #intervalCache має значення 0 за замовчуванням, тобто слайдер не перемикатиме слайди, якщо ми не вкажемо явний інтервал через атрибут або властивість елемента. І тепер можна оновлювати інтервал не лише через атрибут, а й прямим присвоєнням:

document.querySelector('dou-slider').interval = 1000;

Що можна покращити

Ну, як мінімум можна додати якийсь кастомний івент на кшталт slidechange, в якому передаватиметься поточний індекс слайда. В такий спосіб, разом з публічними методами showNextSlide, showPrevSlide, можливістю задавати значення індекса через публічний сетер interval та можливістю реагувати на внутрішню зміну цього індекса можна дозволити створювати власні елементи керування слайдером поза компонентом. Звичайно, тоді ще доведеться вводити якийсь атрибут для вимкнення «нативних» кнопок, шось типу controls="none". Очевидно, я цього не робив, бо такої задачі не стояло.

Шо по доступності

Відверто, я в цій темі не те, шоб дуб дерево хвойне, але експертности відчутно бракує. Тому я покладався на вбудовані можливості для tab-навігації, і разочок попросив ШІ додати необхідні aria-атрибути. І цього, на мою думку, для цього компонента цілком достатньо. Тим паче, що завдяки використанню суто нативних елементів я отримав кілька дуже корисних побічних ефектів. Отже, завдяки використанню форми і радіо-інпутів є можливість перемикати слайди стрілками з клавіатури, якщо один з маркерів є в фокусі. Вверх та вліво перемикатимуть назад, вниз і вправо, відповідно — вперед.

І, помітьте, в коді взагалі немає обробників подій з клавіатури. Також і сам скрол-контейнер підтримує вбудовану навігацію з кнопок. Ну як навігацію. Скролити кнопками можна було від початку часів, а тут просто за рахунок scroll-snap відбувається автоматична парковка. Єдиний нюанс — якщо натиснути стрілку ще раз після того, як запустилась прокрутка, то вона буквально встане на павзу, без паркування. Але наступне натискання відновить анімацію.

А наступне — зупинить. Також працює tab-навігація по кнопках, і по активним елементам всередині слайдера. Я вже раніше згадував, що при отриманні фокусу всередині слайду, контейнер сам підскролить , щоб елемент з фокусом було видно.

Зовнішні стилі

Як і обіцяв, аж наприкінці статті розповім про стилізацію вебкомпонентів за допомогою ::part. Взагалі, ззовні ваш Custom Element можна обмазати стилями в два способи: за допомогою CSS-змінних та тих самих part. Різниця полягає в тому, що CSS-змінні дають можливість лише точкової зміни певних стилів, що аж надто добре накладається на ідею ізоляції стилів. А ::part() працюють як ’мілкі’ селектори, якими можна зловити уже цілий елемент, але не його нащадків.

<!-- Shadow DOM -->
<div part="styled">
  <div class="child">

custom-element::part(styled) {
  /* Стилі спрацюють */
}

custom-element::part(styled) .child {
  /* Нє */
}

Nested CSS синтаксис ::part() теж не підтримує, хоча дозволяє метчити ті самі псевдоелементи:

custom-element::part(styled)::after {
  /* Стилі спрацюють */
}

custom-element::part(styled) {
  &::after {
    /* Нє */
  }
}

Повні кастомні стилі для кнопок і маркерів виглядають ось так:

Тут особливо цікавинок немає, про змінні розповім в окремому матеріалі, єдине, що варте окремої згадки, це те, що appearance: none скрізь працює чудово, але...

«Не в Safari» ©

Я цей браузер не люблю давно й палко. В ньому або шось не робе геть, або робе з такими багами, шо на голову не налізає. Так і тут. До прикладу, тікет щодо імплементації івента scrollend відкрито ще 2019 року.

appearance: none на радіо-інпутах працює дуже дивно, і оновлює відображення маркерів тільки при наведенні на них курсора. Той же overscroll-behavior працює досить дивно — якщо скролити на першому слайді повільно й ніжно, ніби пестячи спляче кошеня, то оверскрол блокується, як і очікується.

Якщо ж рука ваша смикнеться і ви скрольнете дещо швидше за помираючого слимака, то Safari чхатиме на наші бажання, і оверскрол спрацює тільки так.

Тому, згідно правила «якщо щось потребує костиля — я того не роблю», підтримку Safari було виключено повністю. В ньому можна й старий слайдер крутити, мені не жалко.

Післямова

Отже, що ми маємо в результаті? Повністю автономний, реактивний і доступний слайдер, який не потребує жодних сторонніх залежностей, працює на чистому JavaScript, використовує максимум нативних браузерних API та забезпечує високу продуктивність без зайвих компромісів. Так, це не ідеальне рішення. Воно навряд чи виграє нагороди за дизайн чи гнучкість. Але під час роботи над ним я відкрив для себе кілька цікавих підходів, які давно хотів спробувати, отримав кілька осяянь — наприклад, щодо використання нативного скролу замість transform чи оті реактивні трюки з формами й радіоінпутами.

Я не просто побачив нові можливості в добре відомих API, а й примусив їх працювати разом так, як цього не очікував. Писав, видаляв і переписував код — і, по ходу, видалив я того коду крепко більше, ніж лишилося в результаті.

Вихідний код також доступний у публічному репозиторії на GitHub. Форкайте, бавтесь, додавайте поліфіли для Safari, якщо не цінуєте свій час, бггг.

Якщо цей матеріал змусив когось із вас переглянути свої підходи або побачити бравзери з нового боку — значить, усе було недарма. Бо моя мета була не просто створити компонент, а показати, що нативні можливості можуть бути не менш цікавими й потужними, ніж готові бібліотеки чи фреймворки. А тепер — до коментів! Я знаю, що ви вже готові розказати, де я не правий. Чекаю на ваші виважені зауваження.

І, звісно, командо DOU — зі святом вас! 20 років тримати спільноту — це неймовірно потужно. Від мене — цей маленький, але щирий подарунок. Буду надзвичайно втішений, якщо вам пасуватиме таки використати цей слайдер на сайті. Тоді вже можна буде і сумісність із Safari доробити. Але це буде вже тема для зовсім іншої статті. Цьом вам у лобіка!

Подякувати за цю титанічну працю ви можете, закинувши дві гривні на РЕБ, або ж долучившись до ініціативи «Незамовлений Гепі Міл»

P.S. Подаруйте хтось сайту нормальний редактор для дописів, будь-ласочка, а то у мене склалось враження, що я на форматування тексту витратив ледь не стільки ж, скільки на саме написання, майте бога в серці.

Список рекомендованої літератури

  1. Custom Elements
  2. Shadow DOM
  3. Як працює скрол в бравзерах
  4. Стилізація Shadow DOM за допомогою part
  5. CSS scroll-snap

👍ПодобаєтьсяСподобалось51
До обраногоВ обраному21
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Ефективна результативна спільнодія починається із здатності до взаємоповаги.

Неввічлива поведінка унеможливлює взаємоповагу та, відповідно, здатність до взаємодопомоги, що, своєю чергою, унеможливлює будь-яку взаємоузгоджену спільнодію.

Тобто без взаємоповаги є неможливим таке явище як синергія спільнодії, оскільки відсутність взаємоповаги унеможливлює взаємодопомогу.

Рівень складності задач у сучасному ІТ досяг позначки, коли вартісний результат можуть показати лише злагоджені команди досвідчених талановитих експертів. Саме з цієї причини софт-скіли є такими важливими для сучасного ІТ.

Тому здатність до взаємоповаги є ключевою необхідною умовою успіху у сучасному ІТ, адже без неї ніколи не буде нічого путящого за жодних обставин.

Коментар видалено автором допису.

Коментар видалено автором допису.

Цей пост для того, щоб привернути увагу до проблеми.

На момент написання поточного тексту в статті було лише 61 коментар. Шістдесят один. Це в технічній статті, які так вимагає спільнота. Обговорення будь-якої іншої нетехнічної теми миттєво набирає тисячу постів.

Ця стаття нецікава? Не в тренді? Не сказав би. Чи є ще що обговорити? Так. Але автор ніяк не стимулює це обговорення. А спільнота або не хоче думати, або не хоче забивати собі голову задачами складнішими за примітивне формошльопство.

Така поведінка аудиторії не стимулює авторів писати для вас бодай що. Це виглядає як марна справа, як мітання бісеру перед свинями.

З точки зору учасника бесіди відповіді на кшталт

Дякую, цікаві зауваження

розглядаються виключно в контексті неагресивного посилання подалі. Де уточнюючі запитання? Де результати тестування, посилання на дослідження, та й просто альтернативна думка?

Хочете навчитися чомусь більшому? Експериментуйте, ставте амбітні цілі, як мінімум навчіться правильно ставити запитання. Тоді може й буде живе комʼюніті в нас.

ну може просто не варто починати спілкування з фраз на кшталт «що розробник умовно отупішав» у відповідь на фразу стосовно особисто мене. Я не ображаюсь, просто склалося враження, що у співбесідника не стоїть мети зі мною поспілкуватися на тему допису, а стоїть мета висвітлювати виключно якісь недоліки статті і моєї персони. Можливо я неправий, але враження склалося саме таке.

А зауваження дійсно були цікаві, але в більшості, на мою думку, недоречні в плані. Це як виставляти претензії виробнику Мівіни, що їхня локшина не виготовляється за традиційними китайськими рецептами. Якось так.

Я з величезним задоволенням вступатиму з вами в дискусії, особливо технічні, особливо якщо вони відповідатимуть обговоренню, і якщо я матиму на це час і натхнення. Ну і, якщо не буду бачити тонкі натяки на тупих розробників.

ну може просто не варто починати спілкування з фраз на кшталт «що розробник умовно отупішав» у відповідь на фразу стосовно особисто мене

Я не починав спілкування з цієї фрази. Та я пояснив далі по тексту, що саме я вважаю за умовно отупішавшого розробника. Пан вважає себе таким? Не думаю...

а стоїть мета висвітлювати виключно якісь недоліки статті і моєї персони.

Стояла б така мета — я б почав обливати брудом все підряд не підбираючи слів. Але, я окремо написав ті речі, які я персонально вважаю важливими. Це вельми конкретний перелік, ні? То ми будемо спілкуватися на технічні теми, чи ні? Не цікаво вже?

але в більшості, на мою думку, недоречні в плані

То можна обговорити. Мій досвід сформований абсолютно різними задачами, починаючи з примітивних сайтів, закінчуючи складними SaaS. Це продукти B2B рівня, які живуть, розвиваються та підтримуються більше 10 років. Купа аудитів, в тому числі безпекових. Для вас мій перелік не доречний, а для мене — це показник зрілості та рівня задач, з якими стикався розробник.

і якщо я матиму на це час і натхнення.

На цьому розмову можна закінчувати.

На цьому розмову можна закінчувати.

Підтримую. Гарного дня.

Ага, скотиняка ще та...

Тепер трохи технічних консьорнів.

  1. Чому transform краще за scroll? Тому що він може прискорюватися за допомогою GPU та менше їсти батарейку. Але не слід забувати встановлювати will-change, звісно ж.
  2. Чому бажано використовувати window.requestAnimationFrame() замість setTimeout або setInterval? Тому що два останніх не гарантують взагалі темп виконання коду та можуть візуально створювати «смикання» або відсутність плавності.
  3. Чому використання innerHTML треба уникати. Тому що це прямий шлях до XSS.
  4. Чому тег <style> в коді темплейта треба теж унікати? Тому що він не проходить по CSP правилу style-src self.
Десь так.

Не технічні консьорни та зауваження

  1. Можливість для розширення фукнціональності та модифікацій вкрай низька. Все через темплейт, який зашитий в коді, та ще й як константа. Тобто, треба робити форк з патчами, якщо захочеться щось свого в стилі чи поведінку додати. Чому б одразу не використати <template>, де всередині будуть декілька інших темплейтів? Віддайте право модифікацій розробникам-користувачам. Але, знову ж таки, ніякого innerHTML.
  2. Зовнішнє API доволі примітивне, в реальному житті хочеться мати більше контролю та як мінімум можливість перемикання на конкретний слайд.
  3. А що буде, якщо вбудувати слайдер в сторінку, де RTL напрямок є основним?
А що буде, якщо вбудувати слайдер в сторінку, де RTL напрямок є основним?

Як тільки ДОУ вийде на ринок, де

RTL напрямок є основним

, то ДОУ заплатить автору за доробку компонента)

Перекладачі вже вбудовані в браузери. Не проблема просто взяти та переключити відображення на RTL навіть для україномовного ресурсу.

Сергій, цікаво а на що більше пішло часу: на написання коду чи статті?))

Коду, очевидно. Статтю як таку я написав за два дні. А от з кодом бавився шось два чи три тижні у вільний час ) Тим паче, шо кілька разів переписував наніц рішення після того, як нові ідеї в голову приходили )

Реалістичні дедлайни: Якщо якась ідея займає більше одного вільного вечора без помітного прогресу — я її відкидаю.
А от з кодом бавився шось два чи три тижні у вільний час

Так мова про фічу, або конкретну реалізацію, а не весь код загалом. Це швидше в тему моєї майбутньої доповіді про Good Enough Code — якщо я бачу, шо витрачений час перевищує вигоду, тоді на мороз.

Ну і плюс ці тижні включають в себе і шліфовку і неробство, ну )

Що тільки не вигадають розбробники, аби не вчити CSS...
Це не повний аналог того, що зробив автор, але його довести до розуму буде дуже легко.

Коментар видалено автором допису.

Подивився ближче. Воно не контролюється ззовні, стан залежить на target, тобто два екземпляри вже не покладеш. Взагалі в моїй реалізації використані ті самі підходи з CSS.

Будемо відверті, я не та людина, якій можна шпиняти за небажання вчити CSS )

стан залежить на target, тобто два екземпляри вже не покладеш.

Та все там можна нормально розрулити. Це ж ми дивимося на демонстрацію можливостей, а не на кінцевий продукт.

Будемо відверті, я не та людина, якій можна шпиняти за небажання вчити CSS )

Останні тенденції кажуть про те, що розробник умовно отупішав. На все почали дивитися через призму JS. Там, де можна просто вирішити все CSS та додати трохи JS, народ бібліотеки цілі пише, бо не в курсі, що браузер давно вже вміє щось прямо з коробки. Немає претензій до тебе, є акцент саме на цьому факті.

Ще можна дорікнути мені, що я не зробив ось так: developer.chrome.com/blog/carousels-with-css

Сам дуже давно прийшов до висновку що слайдер який зроблений через нативний скролл, це дуже зручна річ, якщо треба швидкий і простий, без тони зайвого коду, слайдер. А для мобілки це взагалі топ. Тому при створенні АБ тестів для сайтів, тільки його і використовую

А маєш якусь реалізацію? Цікаво було б глянути.

Як окремий компонент ніколи його не зберігав, оскільки працюю з АБ тестами, і зазвичай просто чистим джава скріптом накидував на блок слайдеру лише необхідний функціонал. Але я ввечері знайду якогось клієнта, де робив найбільш повний слайдер, і закину в репозиторій. Самому буде цікаво наскільки іншим підійде такий варіант

Лише функціональна стилізація: Компонент не повинен мати власних презентаційних стилів. Усе, що він робить — забезпечує функціональність;

— дай тобі боже здоров’ячка міцного за це!

"

Лише функціональна стилізація: Компонент не повинен мати власних презентаційних стилів. Усе, що він робить — забезпечує функціональність;

" — дай тобі боже здоров’ячка міцного за це!

Дуже крута стаття! Тепер хочу зробити все теж саме що і ти, тільки на Angular 🙃

Поб’ємося об заклад, у кого рішення займе менше байт?)))

Ахахаха, я програю цей спір )

Але, цей, спробуй просто заради досліду імплементувати оцей фокус з формою на RxJS. Воно має лягати прямо чітко.

Забемкався скролити аби лайк вліпити 🫰️️️️️️

сподіваюсь, воно вартувало того )

А є усе це десь зібране аби самому потицяти?

UPD: знайшов, звіняйте

дякую за зауваження, підсвітив абзац

Не побачив результат:

Новий красивий слайдер замість старого некрасивого

тоді раджу прочитати статтю спочатку

P.S. Подаруйте хтось сайту нормальний редактор для дописів, будь-ласочка, а то у мене склалось враження, що я на форматування тексту витратив ледь не стільки ж, скільки на саме написання, майте бога в серці.

Сергію, а тут ми наче поправили, нє? Чи все одно незручно?

Ой, Владо ) Тут можна говорити і говорити ) За markdown величезна подяка, але він зі старим редактором не дуже дружить. А старий редактор дуже дивно обробляє HTML. Саме тому мені довелось відмовитися від ідеї додати зміст статті, бо редактор вперто не давав його сховати під details.

Я би радив обдумати повну відмову від старого редактора і повний перехід на маркдаун. Це перше. Друге мало би вирішитись за рахунок першого, але про всяк ще варто переглянути як додаткова HTML-розмітка парситься. Бо через це часто доводиться відмовлятися від цікавих рішень. А деякі рішення можна буде й запропонувати «з коробки», той самий автозміст статті.

пишеш все в Notion, а потім ctrl-C -> ctrl-V, я так зазвичай роблю :)

Це поки тобі щось прикольне не прийде в голову зробити )

Не розумію, як в тебе виходить робити таку довгу технічну статтю такою цікавою!
Прочитала на одному подиху і отримала професійне задоволення!
Дякую =)

Ну прям аж такою цікавою ) Дякую) Щось новеньке дізналася?

Передати свої відчуття від побаченого мені буде важко — різко перестало вистачати повітря, відмовляли ноги, слабли руки, паморочилась голова.

У мене кожен раз таке коли заходжу, але не від слайдера, а від того, що у плашки по центру нема часу анімації. Ну хто так робить, секунда ж діла:

.b-head li.mini-header a {
   transition: .3s;
}

Ще не дочитав, але вже хочу уточнити. Спочатку на Доу перейшов з мобільного, і не зрозумів де взагалі той слайдер. Тому питання: на мобілках також має слайдитись? Чи його й раніше не було?

Моя версія ніби мала б скролитись. За поточну не впевнений. Треба глянуть

Чистий матеріал гарна подача. Якщо мені ще доводилося частенько використовувати кастомні івенти то кастомні елементи залишаються скринькой пандори. Порадувало побачити фрагмент не часто зустрічається хоча як на мене зручна річ щоб закинуть зразу сформовану розмітку.
В загальному цікава трудомістка стаття. Як сказано в цитаті «це вам не мішки язиком транспорувати»))))

fragment взагалі дуже класна практика, через нього можна й повне оновлення вмісту елементу робить, усілякі вставки-видаляння, а потім одним махом ставити назад в DOM

Код дійсно чистий та дуже зрозумілий, а за сафарі окремий респект! Скільки він мені крові попив... Також недавно познайомився з Custom Elements y своєму пет проекті де спочатку у мене був не реакт, і це мені трішки спростило та прискорило моє рішення))

Дякую, дуже приємно, що стаття сподобалась. Може, якісь аспекти хотілось би дізнатися детальніше?

Монументальна праця! І про Custom Elements, і про цікаві прийоми в CSS, і про композицію, і про недосконалість світу :) Коротше, можна не просто читати, але й перечитувати ;)

Буду цьому надзвичайно радий )

Але не розкрита основна інтрига — чи замінили DOU вже страшний та жахливий слайдер, на модерний та красивий?

Ну для мене це теж інтрига )

Класна стаття і слайдер гарний, дякую тобі! Але ми вже давно носимо ідею взагалі відмовитися від слайдера)

Дякую ) Так а чому ж? Як на мене, він працює і свою функцію виконує.

Якщо глянути аналітику, то відвідувачі не дуже клікають далі 2-3 статті. Думаємо якось інакше організувати цю верхню частину головної на десктопах. Але не придумали ще ,як само)

Так може там і крутити 2-3 статті?)
До речі, сьогодні уві сні ще побачив слайдер зі статтями на кшталт сторіз, з вертикальними прев’ю і видно кілька відразу. Мій слайдер до такого цілком готовий ;)

Цікаві кейси з використанням ResizeObserver, ReactiveCurrentIndex, Timer і загалом ООП підходу. Вражає, як красіво можна управляти станом слайдера.

Дякую, що ти ділитися корисними ідеями, стаття вийшла дуже потужною! Так цікаво, що навіть заварив чай, шкода тільки, що не п’ятниця! 😅 ✌️

Ну та ще завтра перечитаєш ) Дякую!

Передати свої відчуття від побаченого мені буде важко — різко перестало вистачати повітря, відмовляли ноги, слабли руки, паморочилась голова

Бідненький:-(

Я взагалі дуже вразливий

видалив я того коду крепко більше

може замінити «крєпко» на «значно»?..

взагалі стаття дуже ОК, бо прірва девелоперів знають все про хукі життєвого циклу у реакт, вью, ангуляр, — при тому не маючи гадки, що це все базується на connectedCallback(), disconnectedCallback(), adoptedCallback() та attributeChangedCallback()

Це не «крєпко», це «крепко» ) я так говорю )

І дуже дякую за відгук, дуже втішений, що сподобалось

та не лише сподобалось, а я ще лінку зберіг, щоб давати своїм студентам як базовий матеріал щодо CustomElement на одному рівні з матеріалами MDN )))

Підготовка, а ще більше верстка статті зайняли досить багато часу, тому можуть буть якісь косяки. Якщо ви помітите якісь розбіжності в коді в статті та репозиторії, або десь пояснення не відповідають сніпету — пишіть, виправлю якнайшвидше.

Підписатись на коментарі