← Lekcje
JavaScript · lekcja 29

Zdarzenia 4/5:
kliknięcie pola zapisuje ruch

Plan tematu (5 godzin):

  1. Godzina 1: zdarzenie, addEventListener, obiekt zdarzenia
  2. Godzina 2: nie tylko klik - inne zdarzenia
  3. Godzina 3: budujemy planszę 3×3
  4. Godzina 4 (dziś): kliknięcie pola zapisuje ruch
  5. Godzina 5: kto wygrał - sprawdzamy planszę
Rozgrzewka · 3 minuty

Rozgrzewka: plansza z poprzedniej lekcji

  1. Ile elementów ma tablica utworzona zapisem Array(9).fill("")?
  2. Co jest zapisane w każdym elemencie tej tablicy na starcie gry, zanim ktokolwiek kliknie?
  3. Do czego służy atrybut data-index na przycisku planszy?
Część 1 · Nasłuch na wszystkich 9 polach naraz

Pętla for: jeden nasłuch, dziewięć przycisków

Na godzinie 1 podpięliście addEventListener do jednego przycisku. Plansza ma dziewięć przycisków - zamiast pisać dziewięć razy ten sam kod, document.getElementsByClassName("cell") (lekcja 21) zwraca kolekcję wszystkich przycisków naraz, a pętla for przechodzi po niej i do każdego podpina tę samą funkcję.

0 1 2 3 4 5 6 7 8 for (i = 0; i < cells.length; i++) const cells = document.getElementsByClassName("cell"); for (let i = 0; i < cells.length; i++) { cells[i].addEventListener("click", handleMove); } // dziewięć razy ten sam nasłuch, dziewięć razy inny cells[i]

Ciekawostka z Wikipedii: kółko i krzyżyk ma dużo dłuższą historię, niż widać po tej planszy. Podobne gry z trzema polami w rzędzie grano już w starożytnym Egipcie ponad 3000 lat temu, a w Rzymie pod nazwą terni lapilli. W 1952 roku brytyjski informatyk Sandy Douglas zaprogramował ją na komputerze EDSAC jako grę OXO - jedną z pierwszych gier komputerowych w historii.

MDN: Learn_web_development/Core/Scripting/Events, sekcja „Using addEventListener()” (mechanizm z lekcji 66); kolekcja getElementsByClassName: MDN, Web/API/Document/getElementsByClassName, jak w lekcji 51 (deck 51_js_dom_2_slajdy.html)
Część 2 · Które pole kliknięto

event.target.dataset: numer pola z atrybutu HTML

Wszystkie przyciski wywołują tę samą funkcję handleMove - ale każdy ma inny numer pola zapisany w atrybucie data-index. Właściwość dataset elementu (event.target z godziny 1 tego tematu) czyta te atrybuty jako zwykły tekst.

HTML <button class="cell" data-index="4"></button> czwarty przycisk, środek planszy dataset JavaScript event.target.dataset.index === "4" to jest TEKST, nie liczba
MDN, Web/API/HTMLElement/dataset: „The dataset read-only property of the HTMLElement interface provides read/write access to custom data attributes (data-*) on elements. It exposes a map of strings (DOMStringMap) with an entry for each data-* attribute.” - „Właściwość dataset (tylko do odczytu) interfejsu HTMLElement zapewnia dostęp do niestandardowych atrybutów danych (data-*) na elementach. Udostępnia mapę ciągów znaków (DOMStringMap) z jednym wpisem na każdy atrybut data-*.” (tłum. własne); „For example, a data-abc-def attribute corresponds to dataset.abcDef.” - „Na przykład atrybut data-abc-def odpowiada dataset.abcDef.” (tłum. własne)
Część 3 · Sprawdzamy i zapisujemy ruch

handleMove: czy pole jest puste?

function handleMove(event) {
  const index = event.target.dataset.index;
  if (board[index] !== "") {
    return; // pole zajęte, nic nie rób
  }
  board[index] = currentPlayer;
  event.target.textContent = currentPlayer;
}

Kolejność ma znaczenie: najpierw sprawdzacie, czy board[index] jest puste (""). Dopiero gdy jest puste, wpisujecie znak gracza do tablicy board (dane) i do textContent przycisku (to, co widać na ekranie) - dwa różne miejsca, ten sam znak.

Część 4 · Zmieniamy gracza po ruchu

currentPlayer: operator warunkowy przełącza X na O

currentPlayer = currentPlayer === "X" ? "O" : "X";

Ten sam efekt da zapis z if: if (currentPlayer === "X") { currentPlayer = "O"; } else { currentPlayer = "X"; } - operator warunkowy (? :) to krótszy zapis tego samego „jeśli-inaczej” (dział Warunki, lekcje 7-8).

KLIK na pole planszy CZY POLE PUSTE? WPISZ ZNAK board[index] = currentPlayer ZMIEŃ GRACZA X → O, O → X TAK NIE - koniec, bez zmian
Operator warunkowy (ang. conditional (ternary) operator): dział Warunki, lekcja 37 (37_js_warunki_1_slajdy.html)
Egzamin E.14

Tak pytają na egzaminie

1. Funkcja JavaScript powinna być wywołana za każdym razem, gdy użytkownik wpisze dowolny znak do pola edycji. Którego zdarzenia należy użyć?

  1. onload
  2. onclick
  3. onkeydown
  4. onmouseout
CKE, E.14, czerwiec 2017, zadanie 32
Ćwiczenie przy komputerze · 20 minut

Ćwiczenie: kliknięcie pola zapisuje ruch

  1. Otwórz tic-tac-toe.html i tic-tac-toe.js z poprzedniej lekcji (plansza z dziewięcioma przyciskami klasy cell, każdy z data-index od 0 do 8, tablica let board = Array(9).fill("")).
  2. Dodaj zmienną let currentPlayer = "X"; - kto zaczyna.
  3. Pętlą for po document.getElementsByClassName("cell") podepnij addEventListener("click", handleMove) do każdego z dziewięciu przycisków.
  4. Napisz funkcję handleMove(event): odczytaj event.target.dataset.index, sprawdź, czy board[index] === "".
  5. Jeśli pole jest puste: wpisz currentPlayer do board[index] i do event.target.textContent, potem przełącz gracza operatorem warunkowym.
  6. Sprawdź w przeglądarce: kliknij kilka pól po kolei - znaki mają się zmieniać X, O, X, O; kliknięcie zajętego pola ma nic nie robić.
  7. Dla chętnych: zapisz zmiany w swoim repozytorium - commit „Dodaję obsługę kliknięcia pola”.
Cały plik
// tic-tac-toe.js (dopisane do pliku z poprzedniej lekcji)
let board = Array(9).fill("");
let currentPlayer = "X";
const cells = document.getElementsByClassName("cell");
for (let i = 0; i < cells.length; i++) {
  cells[i].addEventListener("click", handleMove);
}
function handleMove(event) {
  const index = event.target.dataset.index;
  if (board[index] !== "") {
    return;
  }
  board[index] = currentPlayer;
  event.target.textContent = currentPlayer;
  currentPlayer = currentPlayer === "X" ? "O" : "X";
}
Kod sprawdzony node --check. Logika bez DOM sprawdzona w Node.js: ruch na pole 4 zapisuje X, kolejny ruch na to samo pole jest odrzucany, gracz przełącza się X → O → X; board["2"] działa tak samo jak board[2] - dataset.index zwraca tekst.
Na koniec

Sprawdź się

  1. Dlaczego pętla for jest wygodniejsza niż podpinanie nasłuchu do każdego z dziewięciu przycisków osobno?
  2. Skąd funkcja handleMove wie, które dokładnie pole zostało kliknięte?
  3. Co sprawdza if (board[index] !== "") i co się dzieje, gdy warunek jest prawdziwy?

To tyle na dziś - kolejny temat: kto wygrał, sprawdzamy planszę.

Poza szkołą

A jak to wygląda w produkcji?

Na lekcjiW produkcjiStatus
getElementsByClassName + pętla fordocument.querySelectorAll(".cell") zwraca NodeList z gotową metodą forEach() - krótszy zapis tej samej pętli, częstszy w nowym kodzie.rozszerzane
Stan gry w zwykłej tablicy boardW większych aplikacjach stan trzyma framework (np. React, Vue) i sam przerysowuje widok po każdej zmianie stanu.rozszerzane
Sprawdzenie board[index] !== "" wprost w handleMoveReguły gry (czy ruch dozwolony, kto wygrał) wydziela się do osobnego pliku - dzięki temu da się je testować bez przeglądarki.rozszerzane
Znak gracza wpisywany przez textContent przyciskuBardziej złożone gry rysują planszę na <canvas> albo w WebGL zamiast osobnych elementów HTML na każde pole.zastępowane (w dużych grach)
MDN, Web/API/Document/querySelectorAll (metoda i NodeList.forEach)