Wróćmy do strony z ostatnich zajęć o HTML/CSS. Zrobiliśmy tam popup, który był mało użyteczny. Wyświetlał się na stałe i nic nie można było z nim zrobić. Spróbujmy doprowadzić do tego, żeby popup wyświetlał się tylko po tym, gdy spróbujemy wysłać formularz rejestracji z pustym imieniem albo nazwiskiem pasażera lub z datą lotu wcześniejszą niż aktualna. Tekst w popupie powinien opisywać napotkany błąd. No i chcemy móc zamknąć popup gdy już przeczytamy o błędach.
Spróbujmy dostać się z poziomu JS do jakiegoś elementu strony - np. do przycisku wysyłającego formularz. Znamy jego selektor (jeśli nie, to można go znaleźć z pomocą narzędzi deweloperskich), spróbujmy więc dostać się do obiektu JS związanego z wyświetlanym elementem.
W konsoli możemy wyszukać element wpisując coś w rodzaju:
let el = document.querySelector('input[type=submit]');
Teraz możemy obejrzeć dostępne z JS właściwości tego elementu. Niestety wyświetlona lista to tylko atrybuty, bez metod. Metody możemy zobaczyć wpisując w konsoli el.
, i patrząc na wyniki autouzupełniania. Jest ich raczej dużo, więc będziemy szukali i używali tylko tych potrzebnych do wykonania tego zadania.
Spróbujmy ukryć przycisk. Można usunąć go z drzewa:
el.remove();
ale gdy możemy chcieć wyświetlić go ponownie wygodniej jest zmienić właściwość display
z CSS. Można to zrobić bezpośrednio:
el.style.display = 'none';
albo modyfikować klasy do których należy element:
el.classList.add('hidden');
Oczywiście w ten sposób możemy dowolnie modyfikować wygląd elementu. Warto sprawdzić w Inspektorze jak wyglądają wprowadzane w taki sposób zmiany.
Po eksperymentach z użyciem konsoli dopiszmy do skryptu wczytywanego przez naszą stronę ukrywanie przycisku. Niestety, na konsoli pojawia się błąd, a przycisk ciągle widać. Wynika to z tego, że skrypt jest wczytywany i wykonuje się zanim przeglądarka stworzy całe drzewo DOM, bo jest wczytywany na początku strony, w <head>
. Przenieśmy znacznik <script>
na sam koniec strony, tuż przed </body>
, i już wszystko będzie OK. Inną metodę radzenia sobie z tym problemem poznamy później.
Wyświetlmy teraz zawartość pola input
z imieniem. Jeśli coś do niego wpiszemy, to w Inspektorze będzie widać, że jest atrybut value
, który zawiera ten tekst. Spróbujmy dopisać do naszego skryptu logowanie zawartości tego pola. Niestety edytor zupełnie nie chce zauważyć, że znaleziony przez querySelector
element ma atrybut value
. Gdy wpiszemy nazwę ręcznie, to kod oczywiście zadziała, ale chcielibyśmy używać poprawnych typów. Potrzebujemy poznać typ elementu, żeby móc go użyć w edytorze. Z powodu skomplikowanej historii obiektów w JS typ możemy sprawdzić pisząc:
jeśli el
to nasz element. Gdy w edytorze napiszemy więc:
let el = document.querySelector('input[name=imie]') as HTMLInputElement;
to podpowiadanie składni zacznie działać, a TypeScript będzie wiedział, że jesteśmy pewni, że ten element ma taki typ. Jeśli nie jesteśmy pewni, warto skorzystać z operatora instanceof
żeby się upewnić.
Teraz zmodyfikujmy jakiś tekst na stronie. Wybierzmy dowolny akapit (<p>
), a jeśli takiego nie ma - to go dopiszmy. Jak pobrać jego zawartość i jak ją zmienić? Można skorzystać z wielu różnych atrybutów, m.in. textContent
, innerText
, innerHTML
. Ich opisy można znaleźć na MDN.
Dodawanie elementów też nie jest trudne. Musimy utworzyć element, np. następująco:
let nowyElement = document.createElement('<div>');
a potem zmodyfikować jego zawartość; atrybuty możemy dodawać przez setAttribute
, a dodatkowe elementy i teksty w środku stworzonego elementu przez przypisanie do właściwości innerHTML
. Można też je tworzyć wywołując znowu createElement
.
Utworzony element trzeba dodać do drzewa. Służą do tego metody appendChild
i insertBefore
.
Stwórzmy nowy akapit i dodajmy go na końcu strony, tuż przed </body>
.
Gdy tworzymy jakiś nowy element, to czasem oprócz danych, które są w nim wyświetlane możemy potrzebować też innych przydatnych danych, których nie chcemy wyświetlać użytkownikowi. Na przykład lista pilotów zawiera imię i nazwisko, ale dobrze byłoby też znać identyfikator pilota w bazie danych czy tabeli z danymi. Takie informacje można przechować w atrybutach data-*
:
<ul>
<li data-pilot-id="76710">Stanisław Skalski</li>
</ul>
Wtedy możemy się do tej wartości odwołać jak do każdego innego atrybutu, przez getAttribute('data-pilot-id')
, ale można też użyć pola dataset
, w którym nazwy atrybutów są zabawnie pozmieniane. Korzystając z tego słownika możemy nie tylko odczytywać, ale i dodawać oraz zmieniać wartości.
Dopisz do listy pasażerów atrybut data-identyfikator-pasazera
z losowymi wartościami, a następnie za pomocą TS wybierz pasażera z największym w sensie leksykograficznym identyfikatorem i wyświetl jego imię i nazwisko na konsoli bądź w okienku alert
.
Pewnym ograniczeniem jest fakt, że wartości atrybutów data-*
są tekstami. Jeśli chcemy związać z wierzchołkiem coś innego - np. funkcję - to warto zauważyć, że obiekty, w tym obiekty DOM, mogą być indeksami w słownikach typu Map
.
Czasem będziemy chcieli wykonać jakąś akcję po określonym czasie. Możemy skorzystać z funkcji setTimeout
, żeby spowodować wywołanie naszego kodu mniej więcej po podanym w milisekundach czasie. Dopiszmy do naszej strony wywołanie, które po 2 sekundach od załadowania strony wypisze na konsoli tekst o tym, że minęły 2 sekundy od załadowania strony. Wyjdzie coś w rodzaju:
setTimeout(() => {
console.log('No już wreszcie.');
}, 2000);
Do wyłączenia wcześniej ustawionego zegarka służy clearTimeout
, a jeśli chcemy wywołań cyklicznie co określony czas, to możemy użyć setInterval/clearInterval
albo wołać setTimeout
po zakończeniu każdego kolejnego wywołania.
Gdy chcemy wykonać kilka kolejnych kroków, każdy kilka sekund po poprzednim, to kod wygląda niezbyt fajnie:
function teczoweKolory(el: HTMLElement) {
setTimeout(function () {
console.log('red');
el.style.backgroundColor = 'red';
setTimeout(function() {
el.style.backgroundColor = 'orange';
setTimeout(function() {
el.style.backgroundColor = 'yellow';
setTimeout(function() {
el.style.backgroundColor = 'green';
setTimeout(function() {
el.style.backgroundColor = 'blue';
setTimeout(function() {
el.style.backgroundColor = 'indigo';
setTimeout(function() {
el.style.backgroundColor = 'purple';
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}
Ale po zastosowaniu do listy lotów działa.
Żeby wyglądało lepiej, potrzebujemy jakiegoś innego narzędzia niż proste callbacki. Promise
tutaj zdecydowanie pomoże.
Napisz jednoargumentową funkcję zwracającą Promise
i opóźniającą wykonanie programu o podaną w parametrze liczbę milisekund.
Korzystając z tej funkcji zmień powyższą funkcję tęczoweKolory tak, żeby miała mniej wcięć.
Zadanie z gwiazdką: zmieniaj kolory w pętli, tak żeby w kodzie programu było tylko jedno wywołanie wait
. Oczywiście skorzystaj z Promise
.
Drugie zadanie z gwiazdką: Jeszcze raz to samo, ale Promise
występuje jawnie tylko w funkcji oczekiwania.
Z Promise
'ów można korzystać do zupełnie innych, asynchronicznych rzeczy, takich jak obsługa pobierania danych za pomocą fetch
.
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
Zadanie: wyświetl zdjęcie autora najnowszego commitu z repozytorium TypeScript na githubie. Podpowiedź: url to https://api.github.com/repos/Microsoft/TypeScript/commits
, odpowiedź będzie tekstem, więc można skorzystać z text()
. W przeglądarce można zobaczyć zawartość JSONa. Oczywiście warto pamiętać o wykrywaniu błędów.
Zadanie z gwiazdką: wyświetl lub wypisz na konsoli listę repozytoriów autora najnowszego commitu posortowaną alfabetycznie bez odróżniania wielkich i małych liter.
Potrafimy już wyświetlić interesujące nas rzeczy, a nawet pobrać jakieś informacje z formularzy, ale trudno cały czas powiedzieć, żeby nasze strony były interaktywne. Zajmijmy się teraz reakcjami na czynności użytkownika naszej aplikacji.
Zadanie: na stronie z informacjami o locie obsłuż kliknięcia w prawej kolumnie (na dużym ekranie), czyli w opóźnione loty i formularz, zmieniając kolor tła po każdym kliknięciu.
Pytania: Czy kliknięcie w pole input formularza zmienia kolor tła? Ilu handlerów potrzeba do zrobienia zadania?
Podłączmy procedurę obsługi zdarzenia z poprzedniego zadania do całego pojemnika zawierającego grid
. W tym momencie obsługujemy oczywiście kliknięcia na całym obszarze grida, musimy więc odsiać kliknięcia w niewłaściwych miejscach.
Zadanie: korzystając z właściwości zdarzenia (event.target
) obsłuż kliknięcia w opóźnione loty i formularz rezerwacji za pomocą tylko jednej procedury obsługi. Pewnie przyda się tu Node.contains
.
Zadanie z dwiema gwiazdkami: obsłuż też kliknięcia w puste miejsce kolumny.
Zadanie: zmień program tak, żeby zmianą koloru tła reagował na kliknięcie w dowolne miejsce tabelki za wyjątkiem obszaru formularza rezerwacji. W rozwiązaniu nie używaj event.target
.
Zadanie: dopisz do procedury obsługi zdarzenia wypisywanie 10*i-tej liczby Fibonacciego, gdzie i to kolejny numer kliknięcia. Wyliczaj ją metodą rekurencyjną.
Pytanie: co się dzieje z przeglądarką po kilku kliknięciach? Czemu?
Warto zachować tę funkcję, bo przyda się później.
Zadanie z gwiazdką: przerób obliczenia tak, żeby ciągle korzystały z tego samego wzoru, ale nie psuły przeglądarki.
Oczywiście kliknięcie to nie jest jedyne zdarzenie, które możemy odebrać. Fajna lista jest np. pod adresem https://developer.mozilla.org/en-US/docs/Web/Events
Zobaczmy jeszcze dwa zdarzenia związane z formularzami: input
i submit
.
Zadanie: wyłącz przycisk submit
w formularzu rezerwacji lotu i włącz go dopiero gdy wybrane będą lotniska, wybrana data nie będzie w przeszłości i wpisane będą imie i nazwisko (czyli co najmniej 2 słowa). Po wciśnięciu przycisku wyświetl potwierdzenie z informacjami z formularza.
Innego rodzaju ważnymi zdarzeniami są load
i DOMContentLoaded
.
Zadanie: umieść w <head>...</head>
skrypt, który wpisze do rezerwacji lotu jakieś imię i nazwisko.
Mamy już wspaniałą stronę, teraz pora przekonać innych, że działa doskonale. W tym celu napiszemy testy, które bezsprzecznie to wykażą.
Zaczniemy od instalacji potrzebnych rzeczy. Do testowania kodu w TypeScripcie istnieje wiele bibliotek, my skorzystamy z Mocha i Chai. Dodatkowo od razu zainstalujemy rzeczy potrzebne do uruchamiania testów stron.
W katalogu z plikiem project.json
wpiszmy następujące zaklęcie:
npm install mocha chai typescript ts-node selenium-webdriver mocha-webdriver @types/chai @types/mocha @types/selenium-webdriver --save-dev
Mocha to biblioteka do uruchamiania testów, Chai pozwoli nam pisać testy używając fajniejszej składni, ts-node nauczy Mochę uruchamiać testy napisane w TypeScripcie a selenium-webdriver będzie za nas uruchamiał przeglądarkę.
Testy piszemy oczywiście w plikach oddzielnych od kodu programu. Musimy więc umieć wczytać kod z pliku z kodem. Skupmy się na napisanej wcześniej funkcji wyliczającej liczby Fibonacciego. Tej najprostszej, blokującej przeglądarkę.
Musimy przerobić plik zawierający tę funkcję na moduł i wyeksportować z niego funkcję fib
. Nic prostszego starczy zmienić napis function fib(...
na export function fib(...
. Wówczas w innym pliku możemy wczytać nasz moduł pisząc:
import fib from './program.ts'
zakładając, że funkcja jest w pliku program.ts
. Obszerny opis modułów jest pod adresem http://exploringjs.com/es6/ch_modules.html
Napiszmy teraz pierwszy test:
import { fib } from "./program";
import { expect } from "chai";
import "mocha";
describe("Fibonacci", () => {
it("should equal 0 for call with 0", () => {
expect(fib(0)).to.equal(42);
});
});
i uruchommy go:
npx mocha -r ts-node/register testy.ts
Ojej, błąd. Musimy znaleźć, czy w teście, czy w programie.
Zadanie: napraw test i dopisz kilka kolejnych. Instrukcja chai jest tu: https://www.chaijs.com/api/bdd/
Zadanie z gwiazdką: przetestuj nieblokującą wersję funkcji.
Zwykle kompletny program wyświetla jednak jakieś informacje w okienku przeglądarki. Gdy korzystamy z testów tak, jak do tej pory, to w ogóle nie ma żadnej przeglądarki. Będziemy chcieli uruchamiać Firefoxa, sprawdzać zawartość stron i wysyłać zdarzenia. Pomoże nam w tym już zainstalowane Selenium, ale do jego działania potrzebny będzie jeszcze geckodriver dostępny na stronie:
https://github.com/mozilla/geckodriver/releases
Trzeba go ściągnąć, rozpakować w jakimś katalogu, który jest w ścieżce, a następnie napisać geckodriver -V
żeby się upewnić, że wszystko działa. Żeby móc pisać testy z funkcjami asynchronicznymi trzeba ustawić zmienną środowiska:
export TS_NODE_COMPILER_OPTIONS='{"lib": ["ES2015"]}'
I wtedy można napisać kolejny plik z testami (zmieniając kilka nazw w nawiasach kwadratowych):
import {Builder, Capabilities} from 'selenium-webdriver';
import { expect } from 'chai';
import { driver } from 'mocha-webdriver';
describe('testDrugi', function () {
it('should say something', async function() {
this.timeout(20000);
await driver.get('file://[ścieżka.do.pliku.ze.stroną].html');
expect(await driver.find('[selektor.opisu.miasta.docelowego]').getText()).to.include('[miasto.docelowe]');
await driver.find('input[type=text]').sendKeys('Jan Woreczko');
await driver.find('button').doClick();
});
})