Events

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:
clickodpalane, gdy element został kliknięty (np. input)
changeodpalane, gdy opuściliśmy element, i zmienił on swoją zawartość (np. pole tekstowe)
mouseoverodpalane, gdy kursor znalazł się na elemencie
mouseoutodpalane, gdy kursor opuścił element
mouseenterodpalane, gdy kursor znalazł się na elemencie
mouseleaveodpalane, gdy kursor opuścił element
dblclickodpalane, gdy podwójnie klikniemy na element (np. input)
submitodpalane, gdy formularz jest wysyłany
resizeodpalane, gdy rozmiar okna przeglądarki jest zmieniany
focusodpalane, gdy element stał się wybrany (np. pole tekstowe, link, button, element z tabindex)
blurodpalane, gdy element przestał być aktywny (np. opuściliśmy input)
keydownodpalane, gdy został naciśnięty klawisz na klawiaturze
keyupodpalane gdy puścimy klawisz na klawiaturze
inputpodobne do powyższego, ale odpalane synchronicznie w czasie trzymania klawisza (np. przytrzymanie klawisza A w polu tekstowym)
loadodpalane, gdy obiekt został załadowany (np. cała strona, pojedyncza grafika)
contextmenuodpalane, gdy kliknięto prawym klawiszem myszki i pojawiło się menu kontekstowe
wheelodpalane, gry kręcimy kółeczkiem myszki
selectodpalane, gdy zawartość obiektu została zaznaczona
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 trzech metod: wstawimy nasz skrypt na końcu strony (najlepiej tuż przed tagiem </body>), do naszego skryptu dodamy atrybut defer lub wykryjemy czy dokument został już w całości wczytany.

Jeżeli pracujemy sami nad naszą prywatną stroną mamy raczej kontrolę kto co kiedy odpala. Ale sytuacja zmienia się gdy piszemy skrypty, które będą używane przez innych, albo pracujemy w zespole. Dlatego właśnie zawsze dobrą praktyką jest sprawdzać, czy dany dokument jest już gotowy do użycia w JavaScript.

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");

});

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 opóźnienia, bo dla przykładu nasze dynamicznie generowane za pomocą JavaScript menu odpali się dopiero po wczytaniu dużych grafik, które użyliśmy gdzieś poniżej na stronie.
Jeżeli więc twój skrypt ma tylko działać na elementach, a nie czekać na wczytanie całych grafik, zawsze 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 JavaScript 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 JavaScript, zmiennych funkcji itp. W powyższym typie zdarzeń nic takiego nie ma miejsca. Wszystko jest totalnie globalne. To błąd.

Hah! Krzykniesz. Więc cały AngularJavaScript jest zły, bo przcież tam też są tak deklarowane zdarzenia. A i w Vue dzieją się podobne rzeczy.

No nie do końca. Faktycznie zdarzenia tam są podpinane inline, ale działają też mechanizmy, które powodują że zdarzenia takie nie są totalnie globalne i działają w zakresie danych komponentów.

Zdarzenie jako właściwość obiektu

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

Jeżeli wyłapiesz ze strony jakiś element i wypiszesz go w konsoli:


const h2 = document.querySelector('h2');
console.dir(h2);

Zauważysz tam masę właściwości i metod. Wśród nich będzie cała masa właściwości zaczynających się od "on" - np. onclick, onmouseover, onload itp. Służą one właśnie do podpinania funkcji pod dane zdarzenia.


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

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

element.onclick = showText;

element.onmouseover = function() {
    console.log('Najechano na przycisk');
}

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

Jeżeli chcemy podpinać zdarzenia do wielu elementów równocześnie, musimy zastosować pętlę:


const p = document.querySelectorAll('p'); //kilka p na stronie

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

//lub

[].forEach.call(p, function(el) {
    el.classList.add('mark');
})

//lub w ES6

[...p].forEach(function(el) {
    el.classList.add('mark');
}

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

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


    element.onclick = null;

Problem z tym modelem podpinania zdarzeń polega na tym, że do jednego elementu możemy podpiąć tylko jedną funkcję dla jednego rodzaju zdarzenia.

Normalnie dla pojedynczego elementu możemy podpiąć kilka funkcji nasłuchujących dane zdarzenie. Wtedy będą one wszystkie odpalane np. na pojedyncze kliknięcie. 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 dłubania w kodzie
//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 wskazuje jak mają się zachowywać dane zdarzenia. Domyślnie jest ustawiony na false i w znacznej większości przypadków można go pominąć.


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.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 2 parametry: nazwę zdarzenia i nazwe funkcji którą chcemy wyrejestrować:


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ć. Na dziewczynach wrażenia może nie zrobicie, ale na kumplach programistach jak najbardziej. ...A może i koleżankom programistkom zadrży powieka?

A co z przekazywaniem do takich funkcji parametrów? Są na to różne rozwiązania.
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 pobierać z jakiegoś miejsca - a przynajmniej w jakiś sposób powiązać z danym elementem.
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 dataset, dzięki czemu takie dane mogą być generowane na przykład przez CMS:


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

//normalnie taki atrybut data-text byłby generowany przez CMS w HTML
//dla przykładu wygeneruję go ręcznie
element.dataset.text = 'To jest tekst do wypisania';

element.addEventListener('click', function() {
    console.log(e.target.dataset.text);
});

Można też dla przykładu zastosować bind, który pozwala przekazywać nie tylko nowe this, ale i parametry do danej funkcji:


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

const element = document.querySelector('guzik');
element.addEventListener('click', showSomething.bind(element, "ala ma kota");

Wywoływanie kliknięcia

Javascript udostępnia nam kilka gotowych metod dla elementów na stronie, które w prosty sposób pozwalają wywoływać dane eventy - tak jakby to realnie zrobił użytkownik:


//klikamy na element
element.click();

//opuszczamy element
element.blur();

//aktywuje dany element
element.focus();

//wysyłamy dany formularz
form.submit();

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żdej podpiętej funkcji do zdarzenia 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.color = 'red'; //this wskazuje na kliknięty element
}

const elem = document.querySelector('.btn1');

elem.addEventListener('click', changeColor);

elem.addEventListener('mouseover', function() {
    this.innerText = "Włala!"; //zmieniamy tekst w elemencie
})

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


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

const elem = document.querySelector('.btn1');

elem.addEventListener('click', changeColor);

elem.addEventListener('mouseover', function(e) {
    e.target.innerText = "Włala!"; //zmieniamy tekst w elemencie
})

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. Ten this zostawmy na razie w spokoju.

Jeżeli w pierwszym parametrze przekażemy jakąś funkcję (najczęściej anonimową), forEach wstawi do jej pierwszego parametru dany element z tablicy. Do kolejnych atrybutów tej funkcji JavaScript 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, JavaScript 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 JavaScript
    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 key zdarzenia.


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

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

    textF.addEventListener('keyup', function(e) {
        console.log('Klawisz: ' + e.key);
    });

});

Możemy też skorzystać z dodatkowych właściwości:

e.altKeye Czy klawisz alt jest naciśnięty
e.ctrlKey Czy klawisz Ctrl jest naciśnięty
e.shiftKey Czy klawisz Shift jest naciśnięty
e.keyCode Zwraca kod klawisza. Przydatne przy sprawdzaniu zakresów klawiszy - np klawisz to liczba

const textarea = document.querySelector('#keyTest')
textarea.addEventListener('keyup', function(e) {
    const keys = [];

    if (e.shiftKey) {
        keys.push("shift");
    }
    if (e.altKey) {
        keys.push("alt");
    }
    if (e.ctrlKey) {
        keys.push("ctrl");
    }
    keys.push(e.key);

    console.log("Naciśnięte klawisze: " + keys.join());

    if (e.keyCode >= 48 && e.keyCode <= 57) {
        console.log('Klawisz to cyfra');
    }
});

Możemy to wykorzystać na przykład do stworzenia pola numerycznego. Normalnie pole typu input:number pozwala wpisywać litery z niektórych systemów liczbowych (np. e)

Spróbuj wpisać w poniższe pola typu number literę e:

Powyższy przykład to tylko prosta implementacja. Pamiętaj, że wartość pola może się w nim zmienić nie tylko pod wpływem wpisywania. Możemy taką nową wartość wkleić ze schowka, lub zmienić ją za pomocą skryptu.

Z wklejeniem możemy sobie poradzić obsługując zdarzenie paste, które odpalane jest gdy do danego pola zostanie coś wklejone:


numerFix.addEventListener('paste', function(e) {
    var clipboardData, pastedData;

    const clipboardData = e.clipboardData || window.clipboardData;
    const pastedData = clipboardData.getData('Text').toUpperCase();

    if(pastedData.indexOf('E') !== -1) {
        e.stopPropagation();
        e.preventDefault();
    }
};

Co do zmian za pomocą skrypu to już zależnie od sytuacji.

Zachowanie eventów

Przeanalizujmy prosty przykład. Mamy html, w którym znajduje się przycisk w divie.
Do obydwu elementów podpinamy zdarzenie click.


<html>
    <body>
        <div class="parent">
            <button class="button">Kliknij mnie i sprawdź w konsoli</button>
        </div>
        <p>
            <span></span>
        </p>
    </body>
</html>

const div = document.querySelector('.parent');
const btn = document.querySelector('.parent .button');

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

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

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

Powyższa sytuacja wynika z zachowania się eventów. Jeżeli event jest odpalany, przeglądarki uruchamiają dla niego 2 fazy. Pierwsza z nich - tak zwana event capturing - polega na tym, że dany event wędruje od góry dokumentu w głąb, aż dojdzie do elementu, który wywołał dany event.

faza capture eventów

Gdy event dojdzie do elementu, który go wywołał jest dla niego odpalany.

Kolejną fazą jest faza event bubbling, w której to event wraca w górę dokumentu, aż dojdzie do elementu, który znajduje się na samej górze drzewa.

faza bubbling eventów

Nasze zdarzenie click odpaliliśmy z pominięciem 3 parametru, więc nasze zdarzenie biegnie w górę dokumentu. Zostaje więc odpalone dla przycisku, a następnie biegnie do początku hierarchii natrafiając po drodze na kolejne elementy (w tym div), które mają swoje nasłuchy na dany typ eventu.

W nowoczesnych przeglądarkach domyślnie wszystkie eventy są odpalane w fazie bubbling, czyli zachowują się jak bąbelki w wodzie - biegną od elementu które je wywołał w górę dokumentu. Za zmianę tego zachowania odpowiada trzeci parametr podawany podczas podmianania eventu:


btn.addEventLitener("click", function() {...}, true); //event capturing
btn.addEventLitener("click", function() {...}); //event bubling

Tylko w wyjątkowych sytuacjach będziesz chciał zmienić to zachowanie, dlatego w większości przypadków parametr ten będzie pomijany.

Możliwe, że w tym momencie możesz być nieco zmieszany. Zamiast odpalić event na danym elemencie, dodatkowo leci on po wszystkich jego rodzicach? Znowu jakieś udziwnienia?
Osobiście lubię przyrównywać to do narysowanego w zeszycie przycisku. Gdy palcem naciśniemy na taki rysunek, przyciśniemy i rysunek, ale i kartkę na którym został narysowany, a i wszystkie karki, które leżą poniżej.

Przykład z naciśnięciem przycisku

Zatrzymanie propagacji

Aby przerwać powyższą wędrówkę, skorzystamy z metody e.stopPropagation().
Metoda ta blokuje wędrówkę danego typu eventu:


const div = document.querySelector('.parent');
const btn = document.querySelector('.parrent .button');

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

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

Od tej pory kliknięcie na przycisk wywoła zdarzenie tylko dla tego przycisku. Wędrówka tego zdarzenia zostanie zablokowana:

Ważne jest to, że jeżeli chcemy całkowicie zablokować przedostanie się danego typu eventu w górę, metodę stopPropagation musimy wywołać w 1 funkcji nasłuchującej.


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

btn.addEventListener('click', function(e) {
    e.stopPropagation(); //powyższa funkcja już puściła event w górę
    console.log('Kliknięto przycisk');
});

Istnieje też metoda stopImmediatePropagation(), która także blokuje wędrówkę eventu danego typu. Różni się ona od stopPropagation tym, że poza blokadą wędrówki dodatkowo zatrzyma dla danego elementu wywołanie kolejnych funkcji nasłuchujących dany event.

Porównajmy więc 2 przykłady. Pierwszy z nich będzie korzystał z stopPropagation, a drugi z stopImmediatePropagation.

W przykładzie poniżej element .grand-parent ma włączone nasłuchiwanie click. Element .parent ma podpięte 2 nasłuchiwania click. W pierwszym z nich odpala e.stopPropagation().


<div class="grand-parent">
        dziadek
    <div class="parent">
        rodzic
        <button class="button" type="button">stopPropagation</button>
    </div>
</div>

const grandParent = document.querySelector('.grand-parent');
const parent = document.querySelector('.grand-parent .parent');
const btn = document.querySelector('.grand-parent .button');

grandParent.addEventListener('click', function(e) {
    console.log('nasłuchiwanie klika na dziadku'); //będzie zablokowane
});

parent.addEventListener('click', function(e) {
    //zatrzymujemy wędrówkę eventu danego typu w górę dokumentu - nie dojdzie on do dziadka
    //nie zatrzymujemy wykonania kodu z tej funkcji
    //nie zatrzymujemy wykonania pozostałych nasłuchów dla tego elementu
    e.stopPropagation();
    console.log('1 nasłuchiwanie klika w rodzicu');
});

parent.addEventListener('click', function(e) {
    console.log('2 nasłuchiwanie klika w rodzicu');
});

btn.addEventListener('click', function(e) {
    console.log('klik na buttonie');
});
dziadek
rodzic

I wersja 2 - z stopImmediatePropagation:


<div class="grand-parent">
        dziadek
    <div class="parent">
        rodzic
        <button class="button" id="buttonImidiatePropagation3" type="button">stopImmediatePropagation</button>
    </div>
</div>

const grandParent = document.querySelector('.grand-parent');
const parent = document.querySelector('.grand-parent .parent');
const btn = document.querySelector('.grand-parent .button');

grandParent.addEventListener('click', function(e) {
    console.log('nasłuchiwanie klika na dziadku'); //będzie zablokowane
});

parent.addEventListener('click', function(e) {
    //zatrzymujemy wędrówkę eventu danego typu w górę dokumentu - nie dojdzie on do dziadka
    //nie zatrzymujemy wykonania kodu z tej funkcji
    //zatrzymujemy pozostałe nasłuchy eventu danego typu dla tego elementu
    e.stopImmediatePropagation();
    console.log('1 nasłuchiwanie klika w rodzicu');
});

parent.addEventListener('click', function(e) {
    console.log('2 nasłuchiwanie klika w rodzicu');
});

btn.addEventListener('click', function(e) {
    console.log('klik na buttonie');
});
dziadek
rodzic

Element nasłuchujący i ten, na którym odpalono zdarzenie

Jak widziałeś powyżej element który nasłuchuje zdarzenie wcale nie musi być tym, na którym dane zdarzenie zostało wywołane. Możemy dla przykładu nasłuchiwać zdarzenia click dla div, a zostanie ono zapoczątkowane w buttonie, który jest w tym divie.

Właściwość e.target wskazuje na element, na którym dane zdarzenie się wydarzyło, a właściwość e.currentTarget na element, który nasłuchuje dane zdarzenie.


<div class="parent" id="parentTarget">
    <button class="button" type="button">Test targeta</button>
</div>

const parent = document.querySelector('.parent');
parent.addEventListener('click', function(e) {
    console.log('e.target: ', e.target);
    console.log('e.currentTarget: ', e.currentTarget);
})

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');

    for (const el of delBtns) {
        el.addEventListener('click', function() {
            const element = this.parentElement;
            element.parentElement.removeChild(element);
        });
    }
});

Sprawdź działanie powyższego kodu

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.contains('.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

Istnieje kilka metod, które możemy wykorzystać do symulowania zachowania elementów:


element.click(); //kliknęliśmy w element
element.select(); //zaznaczamy element (tekst w inpucie)
element.focus(); //wybieramy element (jak za pomocą klawiatury)
element.blur(); //opuszczamy element
form.submit(); //wysyłamy formularz
form.reset(); //resetujemy formularz

Dla eventów dla których nie mamy gotowych metod możemy stworzyć własny event. Aby odpalić takie eventy, musimy się posłużyć konstruktorem Event:


const img = document.querySelector('img');


const event = new Event("mousemove", {
    'bubbles'    : true, //czy zdarzenie ma iść w górę dokumentu
    'cancelable' : true  //czy można je zatrzymać
});
img.dispatchEvent(event);

//lub w skróconej wersji

img.dispatchEvent(new Event("mousemove"));

Pokazana powyżej metoda submit() nie działa do końca tak jak może nam się to wydawać. Metoda submit() odpala wysyłkę formularza programistyczną, natomiast event submit odpalany jest gdy formularz zostanie wysłany ręcznie poprzez klawisz enter (dla formularza z 1 polem) lub poprzez naciśnięcie klawisza submit w formularzu (1). Co więcej użycie tej metody spowoduje, że nie zadziała htmlowa walidacja formularzy. Nie ja to wymyślałem...


    const form = document.querySelector(".form");

    //to nigdy nie odpali
    form.addEventListener("submit", function(e) {
        e.preventDefault();
        console.log("walidacja i wysyłka");
    });

    form.submit()
    

Aby to naprawić musimy stworzyć i odpalić event submit:


    const form = document.querySelector(".form");

    //tutaj wszystko ok
    form.addEventListener("submit", function(e) {
        e.preventDefault();
        console.log("walidacja i wysyłka");
    });

    form.dispatchEvent(new Event("submit"));
    

Tak naprawdę nie musimy się ograniczać do eventów, które są domyślnie dostępne, bo możemy też robić nasze własne:


//nasłuchujemy na nasz event - np oznaczający wczytanie naszych danych
document.addEventListener('dataWasLoaded', 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('dataWasLoaded');
    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)
});