Poznajemy JavaScript od praktycznej strony – jak sprawić, by osadzić ten język wewnątrz strony internetowej tak, aby nasz kod zmieniał coś na tej stronie w odpowiedzi na interakcje użytkownika.

Jednym z celów języka JavaScript jest zapewnienie naszym witrynom pisanym w formacie HTML pewnej interaktywności, poprzez modyfikację drzewa DOM (Document Object Model).

Kontynuujemy poznawanie języka JavaScript. Oderwiemy się na chwilę od języka jako takiego i skupimy na manipulacji elementami drzewa DOM. W poprzednim odcinku zdobyliśmy wszelkie niezbędne podstawy do tego.

Event listener – złap element, podepnij do zdarzenia funkcję

Oto HTML, na którym będziemy pracować:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>This is my website</h1>
    <button id="btn">Click me</button>
    <script>
     //nasz JS tutaj
    </script>
</body>
</html>

Mamy tutaj tagi <h1> oraz <button>. Nasz guzik posiada odpowiednie id o nazwie „btn” (taką unikalną nazwę dla jednego elementu). Dzięki temu będziemy mogli ten guzik „łapać” a to już podstawa do podpięcia pod niego zdarzenia.

Napiszmy sobie najpierw funkcję, którą chcemy wywołać:

<script>
     function greet_user(){
        alert("Hello world!");
    }
    greet_user();
    </script>

Funkcja zdefiniowana i wywołana, nic nowego, czego byśmy wcześniej nie robili.

Teraz „złapmy” sobie nasz przycisk do zmiennej:

<button id="btn">Click me</button>
    <script>
        let btn = document.querySelector("#btn");
        console.log(btn);
     function greet_user(){
        alert("Hello world!");
    }
    greet_user();

Podałem razem z elementem <button>, który ma id równe „btn”, żebyśmy to lepiej widzieli.

Do „łapania” służy document.querySelector. Jeżeli chcemy „łapać” używając id, musimy zacząć nasz selektor od znaku „#”. Po nim następuje wartość id, u nas btn.

Mając funkcję i guzik zapisany do zmiennej możemy dodać tzw. eventListener na zdarzenie „click” czyli kliknięcie, który wtedy wywoła naszą funkcję.

Robi się to tak:

<script>
        let btn = document.querySelector("#btn");

     function greet_user(){
        alert("Hello world!");
    }
    
    btn.addEventListener('click', greet_user);
    </script>

Jeżeli wolimy, możemy do listenera przekazać funkcję anonimową zamiast nazwy jakiejś zdefiniowanej funkcji:

<script>
        let btn = document.querySelector("#btn");
        btn.addEventListener('click', function(){
            alert("Hello world");
            });
    </script>

Łapanie po tagu, zmiana stylu

Pracujemy nad tym plikiem HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>This is my website</h1>
    <button id="btn">Click me</button>
    <script>
        let btn = document.querySelector("#btn");
        btn.addEventListener('click', function(){
            //nasz kod tutaj
            });
    </script>
</body>
</html>

Chcielibyśmy „złapać” h1 i zmienić mu kolor na zielony, po kliknięciu oczywiście.

Aby złapać h1 możemy mu nadać id, jak w przypadku guzika. Albo możemy użyć querySelectora „po tagu”.

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            //nasz kod tutaj
            });
    </script>

Łapiąc „po tagu” dostajemy pierwszy możliwy wynik o takim tagu. Aczkolwiek nie jest zalecane, aby jakikolwiek dokument HTML posiadał więcej niż jeden tag <h1> więc nam to nie robi różnicy.

Teraz zmienimy kolor naszego h1:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            });
    </script>

W ten sposób manipulujemy stylami naszych elementów. Wszystkie znajdują się pod „.style”:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            h1.style.fontSize = "50px";
            h1.style.backgroundColor = "goldenrod";
            });
    </script>

Słówko this – czyli 'ten’

Rzućmy okiem na ten kod HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>This is my website</h1>
    <button id="btn" disabled>Click me</button>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            h1.style.fontSize = "50px";
            h1.style.backgroundColor = "goldenrod";
            
            });
    </script>
</body>
</html>

Mamy tutaj pewien kod przypisany do elementu button, ale nie możemy go wywołać. Nasz guzik posiada bowiem atrybut „disabled”. Oznacza to, że nie można go kliknąć.

To jest oczywiście bez sensu, ale wyobraźmy sobie inną sytuację – klikamy na przycisk, zmienia on nam kolor, wielkość czcionki i inne elementy stylu naszego h1.

Po kliknięciu wszystko zostaje zmienione i nie ma sensu, aby ten guzik dalej był aktywny. Jak go deaktywować?

Musimy mu ustawić 'disabled’ na 'true’. Ale jak to zrobić?

Cóż, guzik już mamy „złapany” do zmiennej „btn”:

<button id="btn">Click me</button>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            h1.style.fontSize = "50px";
            h1.style.backgroundColor = "goldenrod";
            btn.disabled = true;
            });
    </script>

Pamiętając o usunięciu disabled z HTML, w JS podajemy, że jak już się nasza funkcja wywoła, pozmienia co ma pozmieniać, btn ma mieć 'disabled’ ustawione na 'true’, czyli 'prawda’.

Wcale jednak nie musimy odnosić się do btn w ten sposób. Guzik, dla funkcji naszego event-listenera, jest naszym kontekstem, a do kontekstu możemy odnosić się poprzez słówko kluczowe 'this’:

 <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            h1.style.fontSize = "50px";
            h1.style.backgroundColor = "goldenrod";
            this.disabled = true;
            });
    </script>

Guzik możemy wręcz usunąć, jako że spełnił już swoją rolę:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            h1.style.fontSize = "50px";
            h1.style.backgroundColor = "goldenrod";
            this.remove();
            });
    </script>

Przełączanie się – wartość bool

Chcemy taki guzik, który zamienia kolor naszego h1 raz na zielony, potem na czarny i tak w kółko.

Poznamy sobie przy tym nowy typ wartości, wartość bool, czyli wartość prawda/fałsz.

Oto nasz kod początkowy:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>This is my website</h1>
    <button id="btn">Make h1 green</button>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            });
    </script>
</body>
</html>

Stwórzmy naszą wartość bool:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            h1.style.color = "green";
            });
    </script>

Zmienna isGreen posiada wartość false czyli fałsz. Teraz pora na odpowiedni warunek:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            if(!isGreen){
                h1.style.color = "green";
                isGreen = true;
            }
            
            });
    </script>

Mamy tutaj operator negacji, czyli „!”. Oznacza to, że jeżeli isGreen nie jest prawdziwe/nie zawiera prawdy, to wykonaj poniższy kod warunkowy.

Wykonujemy zamianę koloru. Następnie ustawiamy isGreen na true.

Dodam od siebie, że operator negacji można też użyć do zamiany typu bool na jego przeciwieństwo w taki sposób:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            if(!isGreen){
                h1.style.color = "green";
                isGreen = !isGreen;
            }
            
            });
    </script>

Oznacza to, że do isGreen mamy przypisać jego przeciwieństwo. A zatem jeżeli jest fałsz to prawdę, jeżeli prawda – fałsz. Na razie to zostawmy jak było i dopiszmy sobie blok else:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            if(!isGreen){
                h1.style.color = "green";
                isGreen = true;
            } else {
                h1.style.color = "black";
                isGreen = false;
            }
            
            });
    </script>

Teraz nasz kod patrzy czy isGreen jest ustawione na false i jeżeli tak – zmienia kolor na zielony, ustawia na true, jeżeli nie, zmienia kolor na czarny, ustawia na false.

Teraz możemy sobie ten kod uprościć (albo utrudnić) dodając do niego zamianę wartości isGreen na jej przeciwieństwo. Widzimy bowiem, że po każdym wywołaniu funkcji zamieniamy isGreen na przeciwieństwo – raz na true, innym razem na false.

Możemy zatem wynieść te zamiany poza bloki warunkowe, tym samym skracając kod:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            if(!isGreen){
                h1.style.color = "green";
            } else {
                h1.style.color = "black";
            }
            isGreen = !isGreen;
            });
    </script>

Nadawanie i zamiana poprzez klasę CSS

Kontynuujemy poprzedni przykład. Tym razem spróbujemy zmienić kolor elementu poprzez klasę CSS zdefiniowaną w tagu <style>. Nasz kod HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .green {
        color: green;
        }
    </style>
</head>
<body>
    <h1>This is my website</h1>
    <button id="btn">Make h1 green</button>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            if(!isGreen){
                h1.style.color = "green";
            } else {
                h1.style.color = "black";
            }
            isGreen = !isGreen;
            });
    </script>
</body>
</html>

Teraz pora dokonać pewnych zmian:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let isGreen = false;
        btn.addEventListener('click', function(){
            if(!isGreen){
                h1.classList.add("green");
            } else {
                h1.classList.remove("green");
            }
            isGreen = !isGreen;
            });
    </script>

Do klas danego obiektu możemy się odwoływać poprzez „.classList”. Możemy nadawać mu klasę (add) lub ją odbierać(remove).

Nadanie klasy green sprawia, że tekst staje się zielony. Zabranie jej – tekst wraca do domyślnego koloru, czyli czarny.

W ten sposób możemy pracować z klasami CSS.

Istnieje też jeden sposób na nadawanie/odbieranie klasy:

 <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");

        btn.addEventListener('click', function(){   
            h1.classList.toggle("green");
            });
    </script>

W tym przypadku wartość bool isGreen jest zbędna. Funkcja toggle nadaje bądź usuwa klasę jaką podaliśmy (u nas „green”) w zależności czy taka klasa występuje czy nie.

Atrybut textContent – zawartość tekstowa elementu

Rzućmy okiem na ten kod:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .green {
        color: green;
        }
    </style>
</head>
<body>
    <h1>This is my website</h1>
    <button id="btn">Make h1 green</button>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");

        btn.addEventListener('click', function(){   
            h1.classList.toggle("green");
            });
    </script>
</body>
</html>

Wszystko wydaje się działać jak należy, ale chcemy dodać pewną funkcjonalność. Chcemy, aby napis na guziku się zmieniał. Z „make h1 green” na „make h1 black” i odwrotnie.

Dostać się do tego napisu możemy poprzez atrybut textContent:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");

        btn.addEventListener('click', function(){   
            h1.classList.toggle("green");
            this.textContent = "Make h1 black";
            });
    </script>

Działa, aczkolwiek za pierwszym razem. My chcemy zmieniać ten tekst dynamicznie.

Zrobimy to w ten sposób:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");

        btn.addEventListener('click', function(){   
            h1.classList.toggle("green");
            if(h1.classList.contains('green'))
                this.textContent = "Make h1 black";
            else
                this.textContent = "Make h1 green";
            });
    </script>

Czyli sprawdzamy, czy element h1 posiada klasę 'green’ poprzez użycie funkcji .classList.contains i jeżeli tak ustawiamy odpowiednio tekst, jeżeli nie – ustawiamy inaczej.

Dodam, że tutaj mamy jedną linijkę w bloku if oraz bloku else więc można (tylko jednolinijkowe bloki) zapisać bez nawiasów klamrowych.

Mini-projekt – lista języków

Wypróbujmy ten kod w przeglądarce:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>My languages - list</h1>
    <button id="btn">Add new language</button>
    <p>Known languages:</p>
    <ul id="language-list">
        <li>Python</li>
        <li>PHP</li>
        <li>JavaScript</li>
    </ul>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let lst = document.querySelector("#language-list");

        btn.addEventListener('click', function(){   
            //tutaj nasz kod
            });
    </script>
</body>
</html>

Jak widać tworzy on pewną listę <ul> (unordered list, lista nieuporządkowana – lista „od kropek” zamiast jakichś cyfr) z pewnymi elementami <li> (list item, element listy), które mają symbolizować nasze języki programowania.

Mamy też guzik, za pomocą którego będziemy chcieli dodawać nasze języki do już istniejącej listy.

Mamy też odrobinę kodu wstępnego, który pozwala nam obsłużyć event kliknięcia guzika.

Zobaczmy, czy da się z tego zrobić taki mini-projekt z interaktywną listą, do której możemy dodawać kolejne języki.

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let lst = document.querySelector("#language-list");

        btn.addEventListener('click', function(){   
            let language = prompt("Add another language");
            if(language.length > 0) {
                alert(language);
            }
            });
    </script>

Okej, mamy już mechanizm, który prosi nas o input i sprawdza, czy nie podaliśmy pustego napisu. To naprawdę dużo.

Teraz pora stworzyć własny element li, dodać do niego textContent równy naszemu inputowi i doczepić ten element do naszego <ul>.

Robimy to tak:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let lst = document.querySelector("#language-list");

        btn.addEventListener('click', function(){   
            let language = prompt("Add another language");
            if(language.length > 0) {
                let new_li = document.createElement("li");
                new_li.textContent = language;
                lst.appendChild(new_li);
            }
            });
    </script>

Z nowych rzeczy mamy tu tylko document.createElement, który tworzy element o podanym tagu oraz appendChild, który dodaje element jako dziecko na końcu jego rodzica.

Szybko poszło. Może dodajmy sobie coś jeszcze. Na przykład – możliwość usunięcia elementu z listy:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>My languages - list</h1>
    <button id="btn">Add new language</button>
    <p>Known languages:</p>
    <ul id="language-list">
        <li>Python <button class="delete">X</button></li>
        <li>PHP <button class="delete">X</button></li>
        <li>JavaScript <button class="delete">X</button></li>
    </ul>
    <script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let lst = document.querySelector("#language-list");

        btn.addEventListener('click', function(){   
            let language = prompt("Add another language");
            if(language.length > 0) {
                let new_li = document.createElement("li");
                new_li.textContent = language;
                lst.appendChild(new_li);
            }
            });
    </script>
</body>
</html>

Dodaliśmy guziki o klasie „delete” oraz wartości „X” wewnątrz elementu <li>. Zatem są one dzieckiem <li>.

Teraz musimy je wszystkie „złapać”. Będzie nowa rzecz:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let lst = document.querySelector("#language-list");
        let delBtns = document.querySelectroAll(".delete");
        btn.addEventListener('click', function(){   
            let language = prompt("Add another language");
            if(language.length > 0) {
                let new_li = document.createElement("li");
                new_li.textContent = language;
                lst.appendChild(new_li);
            }
            });
    </script>

querySelectorAll bierze wszystkie elementy spełniające warunek selektora. Selektor zaczynający się od znaku „.” oznacza łapanie po klasie. Mamy zatem pod delBtns wszystkie guziki z klasą „delete”.

Teraz tylko musimy przejść po nich w pętli i nadać im odpowiedni listener, który usuwa ich rodzica – element li, do którego należą.

Oto element naszego kodu:

let btn = document.querySelector("#btn");
let h1 = document.querySelector("h1");
let lst = document.querySelector("#language-list");
let delBtns = document.querySelectorAll(".delete");

delBtns.forEach(function(btn){
   btn.addEventListener('click', function(){
   this.parentElement.remove();
    });
 });

Tutaj przechodzimy w pętli forEach po naszych delBtns. Tam dla każdego guzika wywołujemy funkcję, która przyjmuje guzik, dodaje do niego event-listener na zdarzenie klik, zaś eventem jest usunięcie elementu-rodzica danego guzika.

Jeżeli mniej-więcej rozumiemy (ze sposobem kodowania jeszcze będziemy musieli się obyć) to okej.

Kod jest dalej bez zmian. A powinien być zmieniony. Teraz bowiem, jeżeli coś nowego dodamy, nie będzie ten element miał tego guzika „X” z możliwością usunięcia nowo-utworzonego elementu. Nasz kod dotychczas:

<script>
        let btn = document.querySelector("#btn");
        let h1 = document.querySelector("h1");
        let lst = document.querySelector("#language-list");
        let delBtns = document.querySelectorAll(".delete");

        delBtns.forEach(function(btn){
        btn.addEventListener('click', function(){
        this.parentElement.remove();
        });
        });

        btn.addEventListener('click', function(){   
            let language = prompt("Add another language");
            if(language.length > 0) {
                let new_li = document.createElement("li");
                new_li.textContent = language;
                lst.appendChild(new_li);
            }
            });
    </script>

Musimy zatem do new_li dodać guzik-dziecko o tekście „X”, klasie „delete” (dla porządku, niech wszystkie guziki usuwania taką klasę mają, przydać się może potem w CSS) oraz możliwości usunięcia swojego rodzica po kliknięciu.

Niby nic nowego, ale ta wiedza jest jeszcze świeża. Cóż, podaję kod od momentu event-listenera na elemencie btn:

btn.addEventListener('click', function(){   
            let language = prompt("Add another language");
            if(language.length > 0) {
                let new_li = document.createElement("li");
                new_li.textContent = language;

                
                let new_li_button = document.createElement("button");
                new_li_button.textContent = "X";
                new_li_button.classList.add("delete");
                new_li_button.addEventListener('click', function(){
                this.parentElement.remove();
                });
                new_li.appendChild(new_li_button);
                
                lst.appendChild(new_li);
            }
            });

Czyli tak – mając stworzony nowy <li> oraz ustawiony textContent (to bardzo ważne, aby zachować tę kolejność) tworzymy guzik, któremu nadajemy tekst „X”, klasę „delete”, event-listener, który po kliknięciu usuwa jego rodzica i wrzucamy ten guzik jako dziecko naszego <li>.

Nasze <li> wrzucamy jako dziecko do naszego <ul> (pod zmienną lst).

Przeanalizujmy sobie ten mini-projekt raz jeszcze. Myślę, że zrobiliśmy naprawdę dużo, w dodatku w drzewie DOM, na nieznanym nam terytorium.

Naukę JS będziemy kontynuować, także bardziej klasyczne formy tego języka, nie tylko zabawy z drzewem DOM.

Na razie jednak zrobiliśmy porządny kawałek dobrej roboty i warto tutaj się zatrzymać, przeanalizować to, czego się nauczyliśmy i zrobić sobie przerwę.

Do następnego razu!