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-devMocha 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.tsOjej, 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(); });})