Lekcja 11: Zdarzenia (Events)

Zdarzenia to akcje lub wystąpienia, które dzieją się w systemie, z którym pracujesz – system informuje Cię o nich, abyś mógł na nie w jakiś sposób zareagować. W kontekście przeglądarki internetowej, zdarzenia to akcje wykonywane przez użytkownika (np. kliknięcie myszą, naciśnięcie klawisza) lub przez samą przeglądarkę (np. zakończenie ładowania strony).

Obsługa zdarzeń pozwala JavaScriptowi reagować na te akcje, czyniąc strony interaktywnymi.

Model obsługi zdarzeń

Istnieje kilka sposobów przypisywania obsługi zdarzeń do elementów HTML:

1. Atrybuty HTML `on*` (przestarzałe, niezalecane)

Można umieścić kod JavaScript bezpośrednio w atrybutach HTML, takich jak `onclick`, `onmouseover` itp.

<!-- NIE ZALECANE -->
<button onclick="alert("Kliknięto przycisk!"); console.log("Klik!");">Kliknij mnie</button>

Ten sposób miesza HTML z JavaScriptem, utrudnia zarządzanie kodem i jest uważany za złą praktykę.

2. Właściwości DOM `on*`

Można przypisać funkcję JavaScript do właściwości elementu DOM odpowiadającej zdarzeniu (np. `element.onclick`, `element.onmouseover`).

<button id="myButton">Kliknij mnie (DOM property)</button>
let myButton = document.getElementById("myButton");

if (myButton) {
    // Przypisanie funkcji obsługi zdarzenia
    myButton.onclick = function() {
        console.log("Kliknięto przycisk (DOM property)!");
        alert("Klik!");
    };

    // Można przypisać tylko JEDNĄ funkcję obsługi dla danego zdarzenia w ten sposób.
    // Poniższe nadpisze poprzednią funkcję:
    // myButton.onclick = function() { console.log("Inna obsługa kliknięcia"); };
}

Ten sposób jest lepszy niż atrybuty HTML, ale nadal ma ograniczenie do jednej funkcji obsługi na zdarzenie dla danego elementu.

3. Metoda `addEventListener()` (zalecana)

Jest to nowoczesny i najbardziej elastyczny sposób obsługi zdarzeń. Pozwala na dodanie wielu funkcji obsługi (listenerów) dla tego samego zdarzenia na jednym elemencie.

element.addEventListener(typZdarzenia, funkcjaObslugi, [opcjeLubUseCapture]);
<button id="listenerButton">Kliknij mnie (Listener)</button>
let listenerButton = document.getElementById("listenerButton");

function handleClick1() {
    console.log("Listener 1: Kliknięto przycisk!");
}

function handleClick2(event) {
    console.log("Listener 2: Kliknięto przycisk!");
    console.log("Typ zdarzenia:", event.type); // "click"
    console.log("Element docelowy:", event.target); // Przycisk, który został kliknięty
    // event.preventDefault(); // Można zapobiec domyślnej akcji (np. wysłaniu formularza)
    // event.stopPropagation(); // Można zatrzymać propagację zdarzenia (bąbelkowanie)
}

if (listenerButton) {
    // Dodanie pierwszego listenera
    listenerButton.addEventListener("click", handleClick1);

    // Dodanie drugiego listenera dla tego samego zdarzenia
    listenerButton.addEventListener("click", handleClick2);

    // Dodanie listenera, który wykona się tylko raz
    listenerButton.addEventListener("mouseover", function() {
        console.log("Najechano myszką (tylko raz)");
    }, { once: true });
}

Usuwanie listenerów (`removeEventListener()`)

Aby usunąć listener dodany za pomocą `addEventListener`, musisz przekazać dokładnie tę samą funkcję, która została użyta przy dodawaniu.

// Aby móc usunąć listener, funkcja obsługi nie może być anonimowa
if (listenerButton) {
    // Usunięcie pierwszego listenera
    // listenerButton.removeEventListener("click", handleClick1);
}

Obiekt zdarzenia (Event Object)

Funkcja obsługi zdarzenia automatycznie otrzymuje jako argument obiekt zdarzenia (często nazywany event, evt lub e). Zawiera on szczegółowe informacje o zdarzeniu.

Najważniejsze właściwości obiektu zdarzenia:

Popularne typy zdarzeń

Propagacja zdarzeń (Event Propagation)

Gdy zdarzenie wystąpi na elemencie, przechodzi przez dwie fazy propagacji w drzewie DOM:

  1. Faza przechwytywania (Capturing Phase): Zdarzenie "schodzi" w dół drzewa od window do elementu docelowego. Listenery dodane z opcją capture: true są wywoływane w tej fazie.
  2. Faza bąbelkowania (Bubbling Phase): Zdarzenie "wędruje" w górę drzewa od elementu docelowego do window. Jest to domyślna faza, w której wywoływane są listenery (capture: false lub brak opcji).

Można zatrzymać propagację za pomocą event.stopPropagation().

Delegacja zdarzeń (Event Delegation)

Jest to technika polegająca na dodaniu jednego listenera do wspólnego elementu nadrzędnego (np. listy ul) zamiast dodawania wielu listenerów do każdego elementu podrzędnego (np. każdego li). W funkcji obsługi sprawdzamy event.target, aby określić, który element podrzędny wywołał zdarzenie.

Zalety delegacji:

<ul id="parentList">
    <li data-id="1">Element 1</li>
    <li data-id="2">Element 2</li>
    <li data-id="3">Element 3</li>
</ul>
<button id="addButton">Dodaj element</button>
let parentList = document.getElementById("parentList");
let addButton = document.getElementById("addButton");
let counter = 4;

if (parentList) {
    parentList.addEventListener("click", function(event) {
        // Sprawdź, czy kliknięto na element LI
        if (event.target && event.target.nodeName === "LI") {
            let itemId = event.target.getAttribute("data-id");
            console.log("Kliknięto element listy o ID:", itemId);
            event.target.style.textDecoration = "line-through"; // Przykład akcji
        }
    });
}

if (addButton) {
    addButton.addEventListener("click", function() {
        let newItem = document.createElement("li");
        newItem.textContent = `Element ${counter}`;
        newItem.setAttribute("data-id", counter);
        if (parentList) {
            parentList.appendChild(newItem);
        }
        counter++;
        // Nowy element LI będzie automatycznie obsługiwany przez listener na UL!
    });
}

Zadanie praktyczne

Stwórz przycisk HTML. Użyj `addEventListener`, aby po kliknięciu przycisku w konsoli pojawił się komunikat "Przycisk został kliknięty!", a tekst na przycisku zmienił się na "Kliknięto!".

Pokaż rozwiązanie

HTML:

<button id="interactiveButton">Kliknij mnie</button>

JavaScript:

document.addEventListener("DOMContentLoaded", () => {
    let button = document.getElementById("interactiveButton");

    if (button) {
        button.addEventListener("click", function(event) {
            console.log("Przycisk został kliknięty!");
            // event.target odnosi się do przycisku
            event.target.textContent = "Kliknięto!";

            // Opcjonalnie: zapobiegnij dalszym kliknięciom lub zmień styl
            // event.target.disabled = true;
        });
    }
});

Zadanie do samodzielnego wykonania

Stwórz pole tekstowe <input type="text" id="myInput"> oraz paragraf <p id="output"></p>. Napisz skrypt, który będzie nasłuchiwał zdarzenia input na polu tekstowym. Przy każdej zmianie wartości w polu tekstowym, tekst wpisany przez użytkownika powinien pojawić się wewnątrz paragrafu output.

FAQ - Zdarzenia

Dlaczego `addEventListener` jest lepsze niż `onclick`?

`addEventListener` pozwala na dodanie wielu funkcji obsługi dla tego samego zdarzenia, oferuje większą kontrolę nad fazą propagacji (capture/bubble) i jest standardowym, nowoczesnym podejściem. Właściwości `on*` pozwalają tylko na jedną funkcję obsługi i są mniej elastyczne.

Co to jest `this` w funkcji obsługi zdarzenia?

W funkcji obsługi dodanej przez `addEventListener` (jeśli nie jest to funkcja strzałkowa), `this` zazwyczaj wskazuje na element, do którego listener jest przypisany (czyli `event.currentTarget`). W przypadku funkcji strzałkowych, `this` zachowuje wartość z kontekstu, w którym funkcja strzałkowa została zdefiniowana.

Kiedy używać `event.preventDefault()`?

Używaj `event.preventDefault()`, gdy chcesz anulować domyślną akcję przeglądarki dla danego zdarzenia. Typowe przypadki to zatrzymanie wysyłania formularza (przy walidacji po stronie klienta) lub zapobieganie przejściu do nowego URL po kliknięciu linku.

Kiedy używać `event.stopPropagation()`?

Używaj `event.stopPropagation()`, gdy chcesz zapobiec dalszemu "bąbelkowaniu" zdarzenia w górę drzewa DOM. Może to być przydatne, aby uniknąć wywołania listenerów na elementach nadrzędnych, ale należy używać tego ostrożnie, aby nie zakłócić oczekiwanego działania innych części aplikacji.

Jaka jest różnica między `event.target` a `event.currentTarget`?

`event.target` to element, który pierwotnie wywołał zdarzenie (np. konkretny element `li` wewnątrz `ul`). `event.currentTarget` to element, do którego aktualnie przypisany jest listener (np. element `ul`, jeśli używamy delegacji zdarzeń).

Czy mogę symulować zdarzenia w JavaScript?

Tak, można tworzyć i wysyłać syntetyczne zdarzenia za pomocą konstruktora `Event` (lub bardziej specyficznych, jak `MouseEvent`, `KeyboardEvent`) i metody `element.dispatchEvent(event)`. Jest to przydatne np. w testach automatycznych.