Kontynuujemy naukę obiektowego podejścia do pisania kodu JavaScript, wykonując hover effect z kolorem na każdej literze tagu <h2> z pewnym opóźnieniem. Piszemy klasę JS, która obiekt o podanej klasie CSS zamieni na jednoliterowe elementy <span>, które z odpowiednim opóźnieniem będą wykonywały efekt zmiany koloru.

Początkowa templatka – HTML, CSS, JavaScript

Nasz HTML jest bardzo prosty, mamy w zasadzie pustą templatkę z elementem <h1> z odpowiednią klasą nadaną (przyda się później) oraz podpiętymi kodami CSS i JS:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="./test5.css">
</head>
<body>
    <h1 class="each-letter-effect">My heading</h1>
    <script src="./test5.js"></script>
</body>
</html>

JavaScript na razie pusty, zaś w CSS mamy reset stylów, wycentrowanie elementu w pionie i poziomie oraz podstawowy hover:

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    height: 100vh;
    background-color: #ACE1AF;
    display: flex;
    justify-content: center;
    align-items: center;
}

h1 {
    text-transform: uppercase;
    transition: all .5s;
}
h1:hover {
    color: #004526;
}

Na razie nasz efekt działa na całym elemencie h1, czego nie chcemy – każda litera ma się animować osobno. Tym zajmiemy się w naszym JavaScript a potem dostosujemy CSS. Do dzieła.

Piszemy klasę EachLetterEffect – zarys naszego mini-projektu

Nasza klasa w formie takiego szkieletu będzie wyglądać mniej więcej tak, i choć może to nas przytłaczać na początku – nie powinno, to łatwiejsze, niż się wydaje:

class EachLetterEffect {
    constructor(){
        //łapie elementy z klasą .each-letter-effect
        //przechodzi po każdym pętlą for..of
        //prekazuje do turnContentToSpans, które przejmuje pałeczkę
    }

    turnContentToSpans(heading){
        //przyjmuje heading
        //zamienia każdą literę na listę jednoliterowych elementów
        //czyści innerHTML 
        //dodaje fragment ospanowanych liter 
        //przekazując tablicę liter do 
        //createSpansFromArray
    }

    createSpansFromArr(arr){
        //przyjmuje tablicę jednoliterowych elementów
        //tworzy document fragment
        //przechodzi w pętli map po tablicy
        //dodaje do fragmentu span
        //utworzony przez funkcję createSpanElement
        //zwraca fragment
    }

    createSpanElement(letter, i){
        //przyjmuje literę oraz indeks
        //tworzy span element
        //dodaje textContent - literę
        //dodaje transition-delay - indeks * 40ms
        //zwraca span
    }
}

const effect = new EachLetterEffect();

Chodzi o to, aby nasza klasa w konstruktorze złapała wszystkie elementy o klasie CSS, jaką chcemy i przekazała do funkcji, która obrobi te elementy, zamieniając ich treść na jednoliterowe spany.

Piszemy konstruktor i główną funkcję naszej klasy

Okej, wiemy mniej więcej, co chcemy zrobić, pora napisać konstruktor:

constructor(){
   const headings = document.querySelectorAll('.each-letter-effect');
   for (const heading of headings) {
         this.turnContentToSpans(heading);
     }
 }

Łapiemy wszystkie elementy o szukanej klasie CSS i przechodząc po nich w pętli przekazujemy funkcji głównej, która ma za zadanie obrobić je, zamienić treść na jednoliterowe elementy <span>.

Teraz nasza funkcja, a w zasadzie metoda:

turnContentToSpans(heading){
   const lettersArr = heading.textContent.split('');
   heading.innerHTML = '';
   heading.append(this.createSpansFromArr(lettersArr));
}

Przyjmuje heading przekazany w pętli. Z textContent pobiera litery i zamienia na tablicę jednoliterową przez split. Czyści innerHTML i dodaje fragment HTML ze spanami utworzony przez funkcję pomocniczą, której tablicę liter przekazuje.

Funkcje pomocnicze – createSpansFromArr oraz createSpan

Pora napisać funkcję createSpansFromArr, która przyjmie tablicę liter i utworzy document fragment pełen elementów <span>, który dodamy w miejsce zawartości tekstowej w funkcji głównej:

createSpansFromArr(arr){
  const fragment = new DocumentFragment();
  arr.map((letter, i) => {
   fragment.append(this.createSpanElement(letter, i));
   });
    return fragment;
}

Tworzymy fragment, który potem zwracamy. Przez map przechodzimy po tablicy liter i dodajemy do fragmentu span, który utworzy druga funkcja pomocnicza, przyjmując literę i indeks (będzie potrzebny do wyliczania opóźnienia animacji).

Druga funkcja pomocnicza, tworząca jeden element span:

createSpanElement(letter, i){
   const span = document.createElement("span");
   span.textContent = letter;
   span.style.transitionDelay = `${i*40}ms`;
   return span;
}

Wielkiej filozofii tu nie ma – przyjmuje literę i indeks, tworzy span, textContent to litera, opóźnienie to indeks razy 40ms, zwraca span. Ten element jest dodawany razem z innymi w pierwszej funkcji pomocniczej do fragmentu, który zwracany jest i przekazywany do naszej głównej funkcji, która podmienia tekst na ospanowane litery.

Teraz nasz <h1> powinien wyglądać tak:

<h1 class="each-letter-effect">
<span style="transition-delay: 0ms;">M</span>
<span style="transition-delay: 40ms;">y</span>
<span style="transition-delay: 80ms;"> </span>
<span style="transition-delay: 120ms;">h</span>
<span style="transition-delay: 160ms;">e</span>
<span style="transition-delay: 200ms;">a</span>
<span style="transition-delay: 240ms;">d</span>
<span style="transition-delay: 280ms;">i</span>
<span style="transition-delay: 320ms;">n</span>
<span style="transition-delay: 360ms;">g</span>
</h1>

Pora nadać odpowiedni efekt w CSS.

h1 {
    text-transform: uppercase;
    
}
h1 span {
    transition: all .5s;
}
h1:hover span {
    color: #004526;
}

Cała klasa oraz CSS – ostatni rzut oka

Nasza klasa w całości, wcale nie jest tak, że kod nieobiektowy byłby czytelniejszy, wręcz przeciwnie:

class EachLetterEffect {
    constructor(){
        const headings = document.querySelectorAll('.each-letter-effect');
        for (const heading of headings) {
            this.turnContentToSpans(heading);
        }
    }

    turnContentToSpans(heading){
        const lettersArr = heading.textContent.split('');
        heading.innerHTML = '';
        heading.append(this.createSpansFromArr(lettersArr));
    }

    createSpansFromArr(arr){
        const fragment = new DocumentFragment();
        arr.map((letter, i) => {
        fragment.append(this.createSpanElement(letter, i));
        });
        return fragment;
    }

    createSpanElement(letter, i){
        const span = document.createElement("span");
        span.textContent = letter;
        span.style.transitionDelay = `${i*40}ms`;
        return span;
    }
}

const effect = new EachLetterEffect();

Zaletą obiektowego podejścia jest to, że teraz możemy sobie ten efekt jedną linijką aktywować. Można też dokonywać w niej zmian, na przykład wprowadzając jakieś argumenty, które przyjmie konstruktor i odpowiednio przekaże (np. więcej kontroli nad opóźnieniem albo możliwość przekazania własnej klasy CSS).

Nasza klasa zamienia treść elementu HTML na szereg elementów <span> zawierających jeden znak i odpowiednie opóźnienie, a taki szereg łatwiej animować, co czynimi w naszym kodzie CSS:

(...)
h1 {
    text-transform: uppercase;
    
}
h1 span {
    transition: all .5s;
}
h1:hover span {
    color: #004526;
}