Zdarzenia

Zdarzenia to czynności, które dzieją się w przeglądarce. Może je wywoływać użytkownik, ale i każdy element na stronie. Przykładowo klikając na przycisk na stronie, wywołujemy zdarzenie click. Wybierając za pomocą klawisza tab kolejny element w formularzu, wywołujemy zdarzenie focus. Opuszczając taki element, wywołujemy blur. Obrazek się wgrał? Wywołuje zdarzenie load. Formularz się wysyła? Wywoływane jest zdarzenie submit. Takich zdarzeń jest oczywiście o wiele, wiele więcej, a te powyższe to tylko malutkie przykłady.

Typ zdarzenia:Opis:
blurodpalane, gdy obiekt przestał być aktywny (np. input)
changeodpalane, gdy obiekt zmienił swoją zawartość (np. pole tekstowe)
clickodpalane, gdy obiekt został kliknięty (np. input)
dblclickodpalane, gdy podwójnie klikniemy na obiekt (np. input)
focusodpalane, gdy obiekt stał się wybrany (np. pole tekstowe)
keydownodpalane, gdy został naciśnięty klawisz na klawiaturze
inputpodobne do powyższego, ale odpalane synchronicznie w czasie trzymania klawisza (np. przytrzymanie klawisza A w polu tekstowym)
keyUpodpalane gdy puścimy klawisz na klawiaturze
loadodpalane, gdy obiekt został załadowany (np. cała strona)
mouseoverodpalane, gdy kursor znalazł się na obiekcie
mouseoutodpalane, gdy kursor opuścił obiekt
contextmenuodpalane, gdy kliknięto prawym klawiszem myszki i pojawiło się menu kontekstowe
wheelodpalane, gry kręcimy kółeczkiem myszki
resizeodpalane, rozmiar okna przeglądarki jest zmieniany
selectodpalane, gdy zawartość obiektu została zaznaczona
submitodpalane, gdy formularz został wysłany
unloadodpalane, gdy użytkownik opuszcza dana stronę
animationstartodpalane, gdy animacja css się zacznie
animationendodpalane, gdy animacja css się zacznie
animationiterationodpalane, gdy animacja css zrobi jedną iterację
transitionstartodpalane, gdy transition css się zacznie
transitionendodpalane, gdy transition css się zacznie
transitionrunodpalane, gdy transition css się zacznie (odpalane przed opóźnieniem transition)

Powyższa lista zawiera tylko najczęściej używane zdarzenia. Takich zdarzeń jest o wiele, wiele więcej

DOMContentLoaded

Wspominałem to już w dziale o DOM, ale warto to sobie przypomnieć raz jeszcze.
Za chwilę zaczniemy podpinać zdarzenia do elementów na stronie. Żeby takie podpinanie mogło zadziałać, te elementy muszą być już dostępne dla skryptu.
Oznacza to, że zanim zaczniemy cokolwiek podpinać, musimy się upewnić, że został już wczytany html i zostało stworzone drzewo dokumentu.

Aby mieć pewność, że elementy już istnieją użyjemy jednej z dwóch metod: wstawimy nasz skrypt na końcu strony (najlepiej tuż przed tagiem </body>) lub wykryjemy czy dokument został już w całości wczytany. Najlepiej jednak stosować obie metody naraz - czyli umieszczać skrypty na końcu dokumentu a dodatkowo stosować poniższe sprawdzenie.

Aby wykryć czy dokument został wczytany, skorzystamy ze zdarzenia DOMContentLoaded:


document.addEventListener("DOMContentLoaded", function(event) {

    console.log("DOM został wczytany");
    console.log("Tutaj dopiero wyłapujemy elementy");

});

Najlepiej w ogóle połączyć obie techniki i wrzucać skrypty na koniec body i umieszczać w nich powyższe wykrywanie.

W bardzo wielu skryptach zamiast DOMContentLoaded używane jest zdarzenie load dla obiektu window. Jest to często błąd, wynikający z niewiedzy autora skryptu. Event load dla window jest odpalany, gdy wszystkie elementy na stronie zostaną załadowane - nie tylko drzewo dom, ale także i grafiki. Bardzo często będzie to powodować mocno zauważalne błędy, bo np nasze dynamicznie generowane za pomocą JS menu odpali się dopiero po wczytaniu dużych grafik.
Jeżeli więc twój skrypt ma tylko działać na elementach, a nie czekać na wczytanie całych grafik, to używaj zdarzenia DOMContentLoaded.

Rejestrowanie zdarzenia bezpośrednio w kodzie HTML

Aby zdarzenie było dostępne dla danego obiektu, musimy je dla niego zarejestrować. Istnieje kilka metod na rejestrację zdarzenia dla obiektu.

Pierwsza metoda - najgorsza - to zdarzenia deklarowane inline (jako atrybut elementu).
Polega ona na określeniu zdarzenia wewnątrz znacznika:


<a href="jakasStrona.html" onclick="alert('Kliknąłeś')"> kliknij </a>

Ten model rejestrowania zdarzeń jest zły z kilku powodów. Po pierwsze miesza JS z kodem HTML (czemu to złe?).
Po drugie pozbawia nas kontekstu. Jeżeli rejestrujesz zdarzenia ze skryptów, często masz dostęp do właściwości konkretnego obiektu JS, zmiennych funkcji itp. W powyższym typie zdarzeń nic takiego nie ma miejsca. Wszystko jest totalnie globalne. To błąd.

Zdarzenie jako właściwość obiektu

Pierwsza, starsza metoda przypisywania zdarzeń polega na ustawieniu zdarzenia jako właściwość danego obiektu:


function showText() {
    console.log('Kliknięto przycisk na stronie');
}

const element = document.querySelector('#przycisk');
element.onclick = showText;

Zauważyłeś, że przy podpinaniu funkcji do zdarzeń pomijamy nawiasy? Robimy tak dlatego, ponieważ nie chcemy odpalać funkcji, a tylko ją podpiąć pod dane zdarzenie.

Do zdarzenia możemy też podpinać funkcję anonimową:


document.querySelector('#przycisk').onclick = function() {
    console.log('Kliknięto przycisk na stronie');
}

Kolejny przykład pokazuje jak możemy przypisywać zdarzenia do wielu obiektów. Wystarczy zastosować pętlę:


const p = document.querySelectorAll('p');

for (let i=0; i<p.length; i++) {
    p[i].onclick = function () {
        this.classList.add('mark');
    }
}

Od tej pory gdy klikniemy na którykolwiek akapit na stronie, dodamy mu klasę mark, która zrobi z nim dziwy niewidy.

Aby usunąć wcześniej przypisane zdarzenie, wystarczy pod daną właściwość podstawić null:


    obiekt.onclick = null;

Problem z tradycyjnym modelem polega na tym, że do jednego elementu możemy podpiąć tylko jedną funkcję dla jednego rodzaju zdarzenia. Musisz wiedzieć, że do jednego elementu możemy podpiąć kilka funkcji. Wtedy będą one wszystkie odpalane na click. Możemy to obejść przez zastosowanie funkcji zbiorczych (...sam je tak nazwałem):


document.querySelector('#przycisk').onclick = function() {
    printData();
    changeApperance();
    slide();
}

Problem z powyższym kodem jest taki, że nie ma łatwej możliwości usunięcia wybranej funkcji. Powiedzmy, że po jakimś czasie chciałbym usunąć z tego kodu funkcję slide(), tak by dla kolejnych kliknięć odpalały się tylko printData() i changeApperance(). No i musiałbym nieźle kombinować.


document.querySelector('#przycisk').onclick = function() {
    printData();
    changeApperance();
    slide();
}

//gdzieś po godzinie
//ależ ten kod jest niemądry...
document.querySelector('#przycisk').onclick = function() {
    printData();
    changeApperance();
}

Nowy model rejestracji zdarzeń

Problemów takich nie mamy korzystając z "nowego" modelu rejestrowania zdarzeń opierającego się na metodzie addEventListener().

Przyjmuje ona 3 argumenty: typ zdarzenia, funkcja callback do wywołania, oraz trzeci opcjonalny argument, który włącza lub wyłącza (true/false) propagację zdarzeń.


const element = document.querySelector('.btn');

//rejestrujemy 3 zdarzenia click dla elementu
element.addEventListener('click', showMe);
element.addEventListener('click', showSomething)
element.addEventListener('click', function() {
    this.style.setProperty('color', 'red');
});

Od tej pory po pojedynczym kliknięciu zostaną wywołane wszystkie trzy funkcje.

Do wyrejestrowania funkcji z danego zdarzenia służy metoda element.removeEventListener(), która przyjmuje dokładnie takie same argumenty jakie były użyte do zarejestrowania danego zdarzenie za pomocą addEventListener().


element.removeEventListener('click', showMe);
element.removeEventListener('click', showSomething);

Niestety jedna z naszych funkcji jest funkcją anonimową - nie ma nazwy i nie jest podstawiona pod żadną zmienną. Nie jesteśmy więc w stanie przekazać dla removeEventListener drugiego parametru, tym samym nie jesteśmy w stanie wyrejestrować tej funkcji.

Co więc zrobić? Nie bałaganić i nie iść na łatwiznę. Prawdziwie ładny kod powinien unikać funkcji anonimowych, a stosować funkcje z nazwami - zarówno przy podpinaniu ich jak i odpinaniu.

Ok. Wiem. To jest Javascript. Przy małych skryptach używanie funkcji anonimowych jest i wygodne i zwyczajnie się sprawdza. Warto przynajmniej o takich problemach jak powyższe wiedzieć. Czasami uratuje wam to życie. Na dziewczynach wrażenia może nie zrobicie, ale na kumplach programistach jak najbardziej. ...A może i koleżankom programistkom zadrży powieka?

Co jednak z przekazywaniem do takich funkcji parametrów? Jest na to bardzo proste rozwiązanie. Możemy użyć funkcji anonimowej, która odpali naszą funkcję z parametrami:


function showSomething(data) {
    alert(data);
}

const element = document.querySelector('#guzik');
element.addEventListener('click', function() {
    const text = "Ala ma kota";
    showSomething(text);
});

Ale nie zawsze się do sprawdzi, bo często będziemy chcieli taki text dynamicznie skądś pobierać.
Pamiętaj że parametry nie powinny być wyssane z palca. Powinny być brane skądś - np z danego obiektu. W poniższym przykładzie użyłem do tego obiekt dataset:


function showSomething(data) {
    console.log(e.target.dataset.text);
}

const element = document.querySelector('guzik');
element.dataset.text = 'To jest tekst do wypisania';

element.addEventListener('click', showSomething);

Wywoływanie eventu

Aby wywołać za pomocą JS kliknięcie na element,wystarczy że uzyjemy metody click() dla danego elementu:


//klikamy na element
element.click();

Przykład użycia

Na zakończenie tych wywodów kawałek kodu, który demonstruje użycie powyższych metod:


<div>
    <button class="button button-primary button-big" id="buttonTest" type="button">Wpierw podepnij zdarzenie!</button>
    <button class="button" id="buttonTestAdd" type="button">Podepnij zdarzenie</button>
    <button class="button" id="buttonTestRemove" type="button">Odepnij zdarzenie</button>
    <button class="button" id="buttonTestClick" type="button">Kliknij na dużym przycisku</button>
</div>

//1. Pobieram elementy
const buttonTest = document.querySelector('#buttonTest');
      buttonTest.dataset.text = 'To jest tekst do wypisania';

const buttonAdd =  document.querySelector('#buttonTestAdd');
const buttonRemove =  document.querySelector('#buttonTestRemove');
const buttonClick =  document.querySelector('#buttonTestClick');


//2. Podpinam eventy
function showText() {
    alert(this.dataset.text);
}

function addTestEvent() {
    buttonTest.innerText = 'No dobra. Można klikać!';
    buttonTest.addEventListener('click', showText);
}

function removeTestEvent() {
    buttonTest.innerText = 'Wpierw podepnij zdarzenie!';
    buttonTest.removeEventListener('click', showText);
}

function clickOnBtn() {
    buttonTest.click();
}

document.addEventListener("DOMContentLoaded", function() {
    buttonAdd.addEventListener('click', addTestEvent);
    buttonRemove.addEventListener('click', removeTestEvent);
    buttonClick.addEventListener('click', clickOnBtn);
});



Ten element

Standardowo, jeżeli nie zmienimy kontekstu, w każdym podpiętym zdarzeniu możemy skorzystać ze słowa this, które wskazuje na element, który wywołał dane zdarzenie.
Przykładowo jeżeli podpinamy zdarzenie click pod jakiś button, to we wnętrzu funkcji danego zdarzenia this będzie wskazywać na dany button:


function changeColor() {
    this.style.setProperty('color', 'red'); //this wskazuje na kliknięty element
}

document.addEventListener("DOMContentLoaded", function() {

    const el1 = document.querySelector('.btn1');
    el1.element.addEventListener('click', changeColor);

});

Mogą zdarzyć się przypadki, że kontekst dla this zostanie zmieniony (np. poprzez tak zwana funkcję strzałkową, lub bind).
Wtedy do danego elementu możemy odwołać się poprzez e.target:


function changeColor(e) {
    e.target.style.color = '#CC0000';
}

document.addEventListener("DOMContentLoaded", function() {
    const el1 = document.querySelector('.btn1');
    el1.element.addEventListener('click', changeColor);

});

Skąd ten e? A właśnie go poznamy...

Wkraczamy w głąb zdarzenia

Pamiętasz rozdział o tabelach i pętlę forEach? Jest to metoda, która wymaga 2 atrybutów: funkcji i opcjonalnego parametru dla this. This zostawmy na razie w spokoju.

Jeżeli w pierwszym parametrze przekażemy jakąś funkcję (najczęściej anonimową), forEach wstawi do jej pierwszego parametru iterowany element. Do kolejnych atrybutów tej funkcji JS wstawi licznik i tablicę po której iterujemy:


[1,2,3,4].forEach(function(el, i, arr) {
    console.log(el); //wypisze kolejno 1, 2, 3...
    console.log(i); //wypisze licznik pętli - 1, 2, 3...
    console.log(arr); //wypisze tablicę po której iterujemy [1,2,3,4]
})

//nie musimy wcale tych atrybutów obsługiwać, tylko to co potrzebujemy
[1,2,3,4].forEach(function(el) {
    console.log(el); //wypisze kolejno 1, 2, 3...
});

[1,2,3,4].forEach(function() {
    console.log('x') //wypisze 'x' 4 razy
});

Gdybyśmy chcieli rozpisać kod metody forEach, jej wnętrze wyglądało by teoretycznie tak:


Array.prototype.forEach = function(fn, newThis) {
    for (let i=0; i<this.length; i++) {
        fn(this[i], i, this).bind(newThis);
    }
}

Jak widzisz funkcja, która jest przekazywana przez nas trafia do pierwszego parametru forEach, a potem w pętli jest wywoływana wraz z odpowiednimi parametrami.

Czemu właściwie taki odskok od eventów?
Gdy wywoływany jest każdy event na stronie, JS podobnie jak przy forEach wstawia do pierwszego parametru funkcji którą w evencie podpinamy coś.
Tym czymś jest dokładnie obiekt, który zawiera olbrzymią dawkę informacji o danym zdarzeniu - pozycję kliknięcia, pozycję scrolla, typ zdarzenia, element który wywołał zdarzenie, element który nasłuchuje zdarzenie itp.


element.document.addEventListener('click', function(even) {

    /*
    dzięki temu, że do naszej funkcji JS
    automatycznie przekazał obiekt z informacjami
    o evencie, mamy dostęp do właściwości zdarzenia
    */

    console.log(even);

});

Poniżej skupimy się na kilku przykładowych właściwościach. W praktyce wszystko zależy od sytuacji...

Typ zdarzenia

Jakiego typu jest dane zdarzenie? Aby się tego dowiedzieć, wystarczy odczytać właściwość type zdarzenia:


const btn = document.querySelector('#uberButton');

btn.addEventListener('click', function(e) {
    console.log('Typ zdarzenia: ' + e.type);
});

Wstrzymanie domyślnej akcji

Większość elementów na stronie wykonuje domyślne akcje. Linki przenoszą w jakieś miejsca, formularz się wysyłają, buttony się wciskają itp.

Po podpięciu zdarzeń pod obiekt będą ono wywoływane na początku, jednak zaraz po nich wykonana zostanie domyślna czynność.

Aby zapobiec wykonaniu domyślnej akcji skorzystamy z metody e.preventDefault():


link.addEventListener('click', function(e) {
    e.preventDefault();

    alert('Ten link nigdzie nie przeniesie.');
});

Niektórych zdarzeń nie da się w ten sposób zatrzymać (np. load), o czym mówi nam właściwość e.cancelable.

Który klawisz został naciśnięty

Wartość naciśniętego klawisza jest przechowywana w właściwości keyCode zdarzenia. Wystarczy ją odczytać i przesłać do metody String.fromCharCode() aby uzyskać naciśnięty klawisz.


document.addEventListener("DOMContentLoaded", function() {

    const textF = document.querySelector('#textField');
    const textarea = document.querySelector('#textarea');

    textF.addEventListener('keydown', function(e) {
        textarea.value += 'Klawisz: ' + String.fromCharCode(e.keyCode) + ', ';
    });

});

Zatrzymanie propagacji

Po odpaleniu zdarzenia, domyślnie przechodzi ono po obiektach od danego elementu do góry dokumentu - (tak jak bąbelki lecą z ust ryby w górę).

Spójrzmy na poniższy przykład:


<div id="exampleDiv">
    <a id="exampleLink" href="">Kliknij mnie</a>
</div>

const div = document.querySelector('#exampleDiv');
const a = document.querySelector('#exampleLink');

div.addEventListener('click', function(e) {
    alert('Kliknięto div');
});

a.addEventListener('click', function(e) {
    alert('Kliknięto przycisk');
});

Jeżeli teraz klikniemy na przycisk, zostanie wyświetlony komunikat "Kliknięto link". Równocześnie jednak zostanie wyświetlony komunikat dla div!.

Dzieje się tak dlatego, że wszystkie zdarzenia zachowują się jak bąbelki. Po wywołaniu biegną one do góry hierarchii dokumentu.
Nasze zdarzenie click dla przycisku zostaje odpalone, a następnie biegnie do początku hierarchii natrafiając po drodze na kolejny nasłuch wywołany przez div.

Aby przerwać tą wędrówkę oraz kolejne nasłuchy, skorzystamy z metody e.stopPropagation():


const div = document.querySelector('#div');
const btn = document.querySelector('#btn');

div.addEventListener('click', function(e) {
    alert('Kliknięto div');
});

btn.addEventListener('click', function(e) {
    e.stopPropagation();
    alert('Kliknięto przycisk');
});

Od tej pory kliknięcie na link wywoła zdarzenie tylko dla tego linku.

Metoda stopPropagation wywoływana w funkcji podpiętej do danego eventu zatrzymuje aktualny event (np pojedynczy click).

Istnieje też metoda stopImmediatePropagation(), która po wywołaniu zatrzyma wszystkie następne eventy danego typu podpięte do tego elementu:


<div class="parent">
    <button class="button" id="buttonPropagation" type="button">stopPropagation</button>
</div>
<div class="parent">
    <button class="button" id="buttonImidiatePropagation" type="button">stopImmediatePropagation</button>
</div>

//podpinamy eventy dla parentów
const divs = document.querySelector('.parent');

[].forEach.call(divs, function(div) {
    div.addEventListener('click', function(e) {
        e.stopPropagation();
        console.log('Kliknięto div'); //to sie nidgy nie wywoła, bo blokuja dzieci
    });
});

//podpinamy eventy dla przyciskow
//dla jednego stopPropagation(), a dla drugiego stopImmediatePropagation()

const btn = document.querySelector('#buttonPropagation');
btn.addEventListener('click', function(e) {
    e.stopPropagation(); //zatrzyma ten event, ale nie zatrzyma poniższego
    console.log('Kliknięto przycisk - pierwszy event')
});
btn.addEventListener('click', function(e) {
    e.stopPropagation(); //zatrzyma ten event, ale nie zatrzyma powyższego
    console.log('Kliknięto przycisk - drugi event')
});

const btn2 = document.querySelector('#buttonImidiatePropagation');
btn2.addEventListener('click', function(e) {
    e.stopImmediatePropagation(); //zatrzyma ten event i poniższy
    console.log('Kliknięto przycisk - pierwszy event')
});
btn2.addEventListener('click', function() { //ten się nigdy nie wywoła
    console.log('Kliknięto przycisk - drugi event')
});

Sprawdź w konsoli wynik:

Problem z eventami i dynamicznymi elementami

Wyobraź sobie, że mamy listę elementów, które są do niej wstawiane dynamicznie:


<div class="elements-list">
    <!-- tutaj trafią nowe elementy -->
</div>

<div class="add-element-bar">
    <button type="button" class="btn add-element">
        Dodaj element
    </button>
</div>


document.addEventListener('DOMContentLoaded', function() {
    let counter = 0;
    const addBtn = document.querySelector('.add-element');
    const list = document.querySelector('.elements-list');

    addBtn.addEventListener('click', function() {
        counter++;

        //tworzę element
        const el = document.createElement('div');
        el.classList.add('element');
        el.innerText = "To jest element " + counter;

        //dodaje element do listy
        list.appendChild(el);
    });

});

Teraz chcielibyśmy do każdego takiego elementu dodać przycisk usuwania, który po kliknięciu usunie taki element:


document.addEventListener('DOMContentLoaded', function() {

    const addBtn = document.querySelector('.add-element');
    const list = document.querySelector('.elements-list');

    addBtn.addEventListener('click', function() {
        ...

        const del = document.createElement('button');
        del.innerText = "Usuń";
        del.classList.add('delete');
        el.appendChild(del);

        ...
    });

});

Podepnijmy pod niego zdarzenie click, które usunie element:


document.addEventListener('DOMContentLoaded', function() {

    const addBtn = document.querySelector('.add-element');
    const list = document.querySelector('.elements-list');

    addBtn.addEventListener('click', function() {
        ...
        ...
    });

    const delBtns = document.querySelectorAll('.delete');

    [].forEach.call(delBtns, function(el) {
        el.addEventListener('click', function() {
            const element = this.parentElement;
            element.parentElement.removeChild(element);
        })
    });
});
Sprawdź na stronie

Dodajemy nowy element, klikamy na przycisk usuń i co? I nie działa.

Kod na 100% jest poprawny, błędów w konsoli nie ma. Więc czemu to coś nie działa?

Zwróć uwagę, kiedy i jak podpinamy eventy dla przycisków .delete

Robimy to tuż pod podpięciem eventu pod główny przycisk dodający nowy element. Wyszukujemy wszystkie przyciski .delete w liście elementów i podpinamy im...

Ale zaraz zaraz - przecież w tym momencie jeszcze nie ma w tej liście żadnych przycisków .delete. Dopiero po dodaniu nowego elementu powstanie pierwszy przycisk .delete. Nasz kod podpinający przyciski usuwania działa dobrze, problem w tym, że selektor znajdujący takie przyciski nie ma co jeszcze znaleźć...

Jak rozwiązać taki problem?

Jednym z rozwiązań jest podpinanie eventów po stworzeniu danego przycisku:


addBtn.addEventListener('click', function() {
    counter++;
    const el = document.createElement('div');
    el.classList.add('element');
    el.innerText = "To jest element " + counter;

    const del = document.createElement('button');
    del.innerText = "Usuń";
    del.classList.add('delete');
    del.addEventListener('click', function() {
        const element = this.parentElement;
        element.parentElement.removeChild(element);
    });
    el.appendChild(del);

    list.appendChild(el);
});

I to rozwiązanie zadziała - zresztą sprawdź tutaj

Problem z tym rozwiązaniem jest jeden. Wyobraź sobie, że takich elementów mamy powiedzmy 100000. Czyli 100000 razy podpinamy event click. Zamiast jednego eventu mamy ich o wiele, wiele więcej. Pamiętasz prototyp i po co go stosowaliśmy?

Przy eventach występuje ta sama zasada. Skoro istnieje (a istnieje) sposób by zamiast 100000 eventów użyć tylko jednego, to czemu by z niego nie skorzysać?

Rozwiązanie jest bardzo proste: zamiast podpinać się bezpośrednio pod dane elementy (.delete), podepniemy się pod rodzica i za pomocą e.target w evencie będziemy sprawdzać jaki element wywołał dany event:


list.addEventListener('click', function(e) {
    //e.target - ten który kliknął
    //e.currentTarget - ten który nasłuchuje

    if (e.target.className.contain('.delete')) { //tutaj nie tylko klasę możemy spradzać
        const element = e.target.parentElement;
        element.parentElement.removeChild(element);
    }
});

Dzięki temu nie tylko ograniczyliśmy liczbę eventów do jednego. Zyskaliśmy także to, że nasz event działa dla elementów, które dopiero zostaną dodane. W końcu elementem nasłuchującym jest element .list, który istnieje od samego początku.

To co - sprawdzamy?

Customowe eventy

Tak naprawdę nie musimy się ograniczać do eventów, które są domyślnie dostępne. W javascripcie możemy także tworzyć własne eventy.


//nasłuchujemy na nasz event - np oznaczający wczytanie naszych danych
document.addEventListener('ourDataWasLoaded', function() {
    document.querySelector('#loading').style.setProperty('display', 'none');
});

//dla uproszczenia przykładu
//poniżej zakładamy, że wczytaliśmy dane
fetch('...url...').then(function() {
    const event = new Event('ourDataWasLoaded');
    elem.dispatchEvent(event);
})

To nie wszystko. Aby móc wraz z eventem przesyłać dodatkowe dane, użyjemy metody CustomEvent(eventName, detail, bubbles, cancelable).

Pierwszym parametrem jest nazwa naszego eventu. Do drugiego przekazujemy dodatkowe dane w postaci obiektu, który ma właściwość detail. Dane te zostaną wysłane wraz z eventem.
Będą one potem dostępne pod właściwością e.detail, która zostanie przekazany do funkcji nasłuchującej. Ostatnie parametry włączenie odwrotnego przepływu eventów (z góry dokumentu na dół) i włączenie / wyłączenie propagacji.

Ogólna więc postać CustomEventu wygląda tak:


const event = new CustomEvent('ourDataWasLoaded', {
    detail: { ourData: ob },
    bubbles: true, //idąc w góre dokumentu, event będzie odpalany dla elementów (jeżeli mają nasłuch)
	cancelable: false //czy można przerwać za pomocą e.stopPropagation()
});

Na stronie https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent możesz zobaczyć, że takich parametrów jest o wiele więcej, chociaż są rzadziej używane.

Przykładowo możemy stworzyć własny event, który poinformuje inne elementy na stronie że właśnie wczytał dane, oraz wraz z eventem przekaże im te dane:


document.on('ourDataWasLoaded', function(e) {
    document.querySelector('#loading').style.setProperty('display', 'none');
    console.log('parentElement.parentElement: ' , e.detail); //{name : "Marcin", surname: "Nowak"}
});

parentElement.on('ourDataWasLoaded', function(e) {
    console.log('parentElement: ' , e.detail); //{name : "Marcin", surname: "Nowak"}
});

parentElement.parentElement.on('ourDataWasLoaded', function(e) {
    console.log('parentElement.parentElement: ' , e.detail); //{name : "Marcin", surname: "Nowak"}
});

btn.addEventListener('click', function() {
    //po kliknięciu na przycisk wczytujemy dynamicznie dane
    //po wczytaniu wysylamy w gore nasz event, ktory ma także w sobie jakieś dodatkowe dane
    fetch('...url...').then(function() {
        const ob = {
            name : "Marcin",
            surname : "Nowak"
        }
        const event = new CustomEvent('ourDataWasLoaded', {
            detail: { ourData: ob }
        });
        elem.dispatchEvent(event);
    })
})

Powyżej dla CustomEvent podaliśmy tylko jeden parametr

Ale po co to? Musisz wiedzieć, że eventy bardzo często są nie tylko wykorzystywane do wykrywania podstawowych czynności użytkownika, ale też do przesyłania informacji między różnymi oddzielnymi częściami danej strony. Większość dzisiejszych frameworków takich jak Angular, React itp bazują na tej zasadzie. Mamy jakiś komponent, który musi przekazać jakieś dane do innego komponentu. Możemy to robić za pomocą atrybutów (nie chcę tutaj wnikać w szczegóły), ale też możemy wykorzystywać eventy.

Możemy dla przykładu w elemencie document (czy jakimś nadrzędnym kontenerze) nasłuchiwać naszych eventów. Element taki dowie się (przez event) w pewnym momencie, że dla przykładu jakiś element wczytał dane. Wyśle wtedy w dół nowy event, który poinformuje inne elementy na stronie, że dane są już wczytane i można zacząć działać. Dzięki temu wiele komponentów może współgrać ze sobą.

W poniższym przykładzie asynchronicznie (dynamicznie) wczytujemy dane z innego serwera. Jak je wczytamy, odpalamy nasz event listLoaded na jakiś elementach. One czekają na ten event, a jak tylko go usłyszą, wypisują wynik w konsoli. Sprawdź w debugerze czy się udało...


<div id="customEventParent">
    <div id="customEventUserList"></div>
    <div id="customEventSidebarUserList"></div>
</div>

const parentElem = document.querySelector('#customEventParent')
const userList = document.querySelector('#customEventUserList');
const sidebarUserList = document.querySelector('#customEventSidebarUserList');

//jakis element na stronie wczytuje dane
//jak je wczyta wysyla event do dokumentu (w gore)
fetch('https://jsonplaceholder.typicode.com/users')
    .then(resp => resp.json())
    .then(function(resp) {
        const event = new CustomEvent('listLoaded', {
            detail : {
                users : resp
            },
            bubbles: false,
            cancelable: false
        });

        //odpalamy event na danych elementach
        userList.dispatchEvent(event);
        sidebarUserList.dispatchEvent(event);
});


//jakies inne elementy czekaja na odpalenie eventu
userList.addEventListener('listLoaded', function(e) {
    console.log('element: ', e.target.id);
    console.log(e.detail.users)
});
sidebarUserList.addEventListener('listLoaded', function(e) {
    console.log('element: ', e.target.id);
    console.log(e.detail.users)
});