# Angular CRUD App ## Część I ### Założenia aplikacji Projekt zakłada zbudowanie aplikacji służącej do zarządzania studentami Software Development Academy. + Wyświetlanie listy studentów + Edycja studentów + Dodawanie nowego studenta + Usuwanie studentów + Komunikacja poprzez API ### Wymagania wstępne + Podstawowa znajomość JavaScriptu oraz [TypeScriptu](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) + Wersja LTS [Node.js](https://nodejs.org/en/download/) + Menadżer pakietów [npm](https://docs.npmjs.com) + Edytor [Visual Studio Code](https://code.visualstudio.com) + [Angular CLI](https://cli.angular.io) do uruchomienia projektu + Konsola wiersza poleceń *(np. CMD lub PowerShell)* ### Instalacja Node.js i NPM 1. Pobierz instalator w wersji LTS z oficjalnej strony [Node.js](https://nodejs.org/en/download/). 1. Postępuj zgodnie z instrukcjami wyświetlanymi w instalatorze. **Odznacz dodatkowe narzędzia.** 1. Uruchom ponownie komputer. ### Weryfikacja instalacji Czas sprawdzić, czy mamy poprawnie zainstalowane Node.js i NPM. W tym celu wykonaj kilka poleceń w konsoli wiersza poleceń, aby zobaczyć, jaka wersja każdego z nich jest zainstalowana. 1. Sprawdź wersję Node.js: ```bash node -v ``` 1. Sprawdź wersję npm: ```bash npm -v ``` ## Część II ### Instalacja Angular CLI Angular Command Line Interface (CLI) to narzędzie, które pomaga w tworzeniu szkieletów i budowaniu Angularowych aplikacji. Przyspiesza również żmudne zadania, takie jak tworzenie plików różnego typu. Aby zainstalować Angular CLI, w oknie konsoli wykonaj następujące polecenie: ```bash npm install -g @angular/cli ``` ### Weryfikacja instalacji Wykonaj następujące polecenie w wierszu poleceń: ```bash ng version ``` Polecenie powinno zwrócić aktualną wersję Angular CLI i TypeScriptu. ### Stworzenie angularowej aplikacji Skoro wszystko jest gotowe, jesteśmy gotowi do stworzenia naszej pierwszej aplikacji przy użyciu Angulara. Otwórz wiersz poleceń, przejdź do folderu, w którym chcesz utworzyć katalog z Angularem i wykonaj następującą komendę: ```bash ng new my-app ``` Polecenie `ng new` wyświetli monit o podanie informacji o funkcjach, które należy uwzględnić w początkowej aplikacji. Zaakceptuj wartości domyślne poprzez `Enter`. Komenda ta zrobi za nas kilka rzeczy: + Zainicjuje repozytorium `git`. + Utworzy plik `package.json` ze wszystkimi zależnościami Angulara. + Skonfiguruje TypeScript, Webpack, Testy. + Utworzy folder `src` z początkowym kodem. + Wykona `npm install`, aby pobrać wszystkie pakiety do `node_modules`. Całkiem fajnie, co? Pamiętaj, że dobry programista, to leniwy programista. Ok, teraz przejdź do nowo stworzonego katalogu z aplikacją: ```bash cd my-app ``` Uruchom wbudowany serwer, który obserwuje zmiany w plikach i buduje aplikację: ```bash ng serve --open ``` Opcja `--open` automatycznie otwiera kartę w przeglądarce na http://localhost:4200/. Jeśli instalacja i konfiguracja zakończyła się sukcesem, powinna ukazać się strona podobna do [tej](img/app-works.png). ## Część III ### src/index.html Budujemy SPA *(ang. Single Page Application)*, więc wszystko zostanie załadowane do pliku `src/index.html`. Otwórz go i przyjrzyj się mu. Jak zapewne widzisz, to dość standardowy kod HTML, z wyjątkiem dwóch elementów specyficznych dla naszej aplikacji: + `<base href="/">` – znacznik potrzebny do prawidłowego działania routingu. + `<app-root></app-root>` – główny komponent Angulara. ### src/app Katalog `app` zawiera części używane do zmontowania całej aplikacji. W tym katalogu można znaleźć moduły aplikacji, komponenty i routing. #### app.module.ts Będziemy często używać tego pliku. Najważniejszą częścią są metadane wewnątrz dekoratora `@NgModule`. Tam zawarte są deklaracje, importy, provider'y i bootstrap. + **declarations:** komponenty, dyrektywy i pipe'y. + **imports:** routingi i moduły. + **bootstrap:** lista komponentów, które chcemy załadować podczas uruchamiania aplikacji. W naszym przypadku jest to `AppComponent`. #### app.component.ts AppComponent wygląda trochę podobnie do modułu aplikacji, ale zamiast `@NgModule` mamy dekorator `@Component`. Ponownie najważniejszą częścią jest wartość tych atrybutów *(metadanych)*. + **selector:** nazwa komponentu. Pamiętasz `<app-root>` w pliku `index.html`? To właśnie tutaj jest to zdefiniowane. + **templateUrl:** ścieżka do pliku szablonu HTML. `<app-root>` zostanie zastąpione tym, co masz w szablonie. + **styleUrls:** ścieżka do pliku CSS. Arkusz styli wpłynie wyłącznie na ten komponent, czyli nie przeniknie do innych części aplikacji. W klasie `AppComponent` można zdefiniować zmienne *(np. `title`)*, które są używane w szablonach. Usuńmy kod znajdujący się w `app.component.html` i zastąpmy całą zawartość następującym kodem: ```html <header class="container"> <h1> {{ title }} </h1> </header> <main class="container"> </main> <footer class="container"> Copyright &copy; 2020 by SDA. </footer> ``` ## Część IV ### Tworzenie nowego komponentu Utwórzmy nowy komponent do wyświetlania listy studentów. Możemy szybko go wygenerować przy pomocy Angular CLI wpisując: ```bash ng generate component students ``` To polecenie utworzy nowy folder z czterema plikami: ```bash CREATE src/app/students/students.component.css (0 bytes) CREATE src/app/students/students.component.html (23 bytes) CREATE src/app/students/students.component.spec.ts (642 bytes) CREATE src/app/students/students.component.ts (283 bytes) ``` CLI doda również wygenerowany komponent *Students* do `AppModule`: ```bash UPDATE src/app/app.module.ts (404 bytes) ``` Śmiało sprawdź każdy z tych plików. Będą wyglądać podobnie do wcześniej poznanych składników aplikacji. Zwróć jednak uwagę na metodę `ngOnInit()` wewnątrz pliku `students.components.ts`. Jest to jeden z kilku punktów *zaczepienia* cyklu życia. Angular wywołuję tę metodę zaraz po uruchomieniu komponentu. To dobre miejsce na wprowadzenie logiki inicjalizacji. Dodajmy nasz nowy komponent do głównego komponentu aplikacji. Przejdź do `src/app/app.component.html` i wprowadź pomiędzy znacznik `<main>`: ```html <app-students></app-students> ``` Jeśli masz uruchomiony proces `ng serve`, powinien on automatycznie zaktualizować stronę i wyświetlić *"students works!".* ### Szablon komponentu *Students* Samo *"students works!"* nie jest zbyt użyteczne. Zmieńmy to poprzez dodanie kodu HTML, który będzie prezentował naszą listę studentów. Przejdź do pliku `src/app/students/students.component.html` i wklej ten kod HTML: ```html <section> <table> <thead> <tr> <th>#</th> <th>Imię i nazwisko</th> <th>Adres e-mail</th> <th>Akcje</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>Jan Kowalski</td> <td><a href="mailto:jan@kowalski.pl">jan@kowalski.pl</a></td> <td> <a href="#" class="button button-green">Edytuj</a> <button class="button button-red">Usuń</button> </td> </tr> </tbody> </table> </section> ``` ### Stylizacja aplikacji Takie elementy jak tabele i przyciski z widoku komponentu *Students* mogą się powtarzać wielokrotnie w całej aplikacji. Z tego względu nie ma sensu stylować tych elementów wyłącznie w pliku CSS związanym z konkretnym komponentem. Zdecydowanie lepszą opcją jest użycie globalnego arkusza styli dostępnego w pliku `src/styles.css`. To właśnie w tym pliku powinny znaleźć się wszelkie uniwersalne selektory i ich właściwości, np. te dotyczące tła, fontu itd. Otwórz ten plik i wprowadź odpowiednie zmiany dla tła, tabeli oraz przycisków z wyróżnieniem na kolor czerwony i zielony (klasa *button-red* oraz *button-green*). Przykładowa zawartość `src/styles.css`: ```css /* Ustalenie właściwości "box-sizing" dla wszystkich elementów */ * { box-sizing: border-box; } /* Właściwości dla body */ body { font-family: Arial, Helvetica, sans-serif; font-size: 16px; } /* Klasa kontenera, która ma ograniczoną szerokość i wyśrodkowuje content */ /* Ustawienie lewego i prawego marginesu na "auto" wyśrodkowuje kontener */ .container { margin: auto 20px; width: 100%; } /* Tabele */ table { width: 100%; } table th, table td { padding: 8px; text-align: left; } /* Cieniujemy co drugi (parzysty) wiersz tabeli */ /* Używamy tbody, aby upewnić się, że nie cieniujemy nagłówka - to zachowuje kolejność cieniowania */ table tbody > tr:nth-child(odd) { background: #dddddd; } /* Przyciski */ .button { display: inline-block; margin: 2px 0; padding: 12px 30px 14px; border: 0; background: #999999; border-radius: 6px; color: #ffffff; text-decoration: none; font-size: 16px; cursor: pointer; } /* Przycisk po najechaniu kursorem */ .button:hover { background: #888888; } /* Przycisk w stanie aktywnym */ .button:active, .button:focus { background: #777777; } /* Przycisk w kolorze zielonym */ .button-green { background: #33cc55; } .button-green:hover { background: #22bb44; } .button-green:active, .button-green:focus { background: #22aa44; } /* Przycisk w kolorze czerwonym */ .button-red { background: #dd3333; } .button-red:hover { background: #cc2222; } .button-red:active, .button-red:focus { background: #bb2222; } /* Media Query dla urządzeń o minimalnej szerokości 992px */ /* czyli dla komputerów stacjonarnych */ @media (min-width: 992px) { .container { max-width: 960px; } } ``` Zwróć uwagę na właściwość `box-sizing: border-box`, która jest ustawiona dla wszystkich elementów HTML. Właściwość ta sprawia, że tworzenie układów CSS jest łatwiejsze i bardziej intuicyjne, a to przez to, ponieważ zapewnia, że marginesy wewnętrzne i obramowanie nie zwiększają szerokości i wysokości elementów. Na końcu przykładowego pliku CSS została również użyta reguła `Media Query`, która służy do wczytywania styli w zależności od rodzaju urządzenia czy dostępnej szerokości okna przeglądarki. Dzięki temu możemy dopasować stronę do wielkości urządzenia użytkownika. Więcej o tej regule znajdziesz [tutaj](https://developer.mozilla.org/pl/docs/Web/CSS/Media_Queries/Using_media_queries). ## Część V ### Model pojedynczego studenta Przed zbudowaniem CRUD-owego serwisu zdefiniujmy interfejs pojedynczego studenta do pracy z danymi. Utwórz nowy plik `student.ts` wewnątrz folderu `src/app` i dodaj do niego następujący kod: ```js export interface Student { id: number; name: string; email: string; } ``` ### Utworzenie service'u Zacznijmy od stworzenia serwisu zawierającego wstępną listę studentów, którymi chcemy zarządzać. Serwis przyda nam się do manipulowania danymi. Komponenty nie powinny pobierać ani zapisywać danych bezpośrednio, lecz powinny skupiać się na prezentacji danych i regulować dostęp do nich wśród różnych klas. Utwórzmy zatem serwis za pomocą interfejsu wiersza polecenia, wpisując: ```bash ng generate service student ``` Polecenie wygeneruje szkielet klasy `StudentService` w `src/app/student.service.ts` w następujący sposób: ```js import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class StudentService { constructor() { } } ``` ### Funkcjonalność CRUD Aby uruchomić funkcjonalność *Create-Read-Update-Delete*, będziemy musieli zmodyfikować trzy pliki: + `src/app/student.service.ts` + `src/app/students/students.component.ts` + `src/app/students/students.component.html` #### Włączenie mechanizmu HTTP Aby móc się komunikować ze zdalnymi serwerami przez protokół `HTTP` potrzebujemy angularowego mechanizmu o nazwie `HttpClient`. Sprawmy, by był on dostępny w każdym miejscu aplikacji w dwóch krokach. Po pierwsze, dodaj go do głównego modułu `src/app/app.module.ts` importując go na samej górze pliku: ```js import { HttpClientModule } from '@angular/common/http'; ``` Następnie, wciąż jeszcze w tym samym pliku, dodaj `HttpClient` do tablicy `imports`: ```js imports: [ BrowserModule, HttpClientModule ], ``` #### Przygotowanie service'u do CRUD-a Zmodyfikujmy serwis `student.service.ts`, aby móc zarządzać listą studentów. Do generowania samej listy wykorzystamy platformę [JSONPlaceholder](https://jsonplaceholder.typicode.com), która oferuje różnego rodzaju *udawane* REST API. Dodajmy mechanizmy `HttpClient` oraz `HttpHeaders` *(służy do konfiguracji nagłówka dla żądania HTTP)* importując je w pliku z serwisem: ```js import { HttpClient, HttpHeaders } from '@angular/common/http'; ``` W związku z tym, iż będziemy się łączyć ze zdalnym serwerem, `StudentService` musi czekać na odpowiedź serwera, a zatem metody CRUD, które wkrótce zdefiniujemy, muszą się wykonywać asynchronicznie *(funkcje asynchroniczne nie blokują dalszego wykonywania się kodu)*. Należy zatem zaimportować klasę `Observable`, która jest kluczowym składnikiem biblioteki [RxJS](https://rxjs-dev.firebaseapp.com). ```js import { Observable } from 'rxjs'; ``` Zaimportujmy również wcześniej stworzony interfejs `Student`: ```js import { Student } from './student'; ``` Będąc nadal w `StudentService`, wstrzyknij `HttpClient` do konstruktora w prywatnej właściwości o nazwie `http`: ```js constructor(private http: HttpClient) { } ``` Zdefiniujmy teraz *(przed konstruktorem)* prywatną zmienną `studentsUrl` z adresem do zasobu użytkowników: ```js private studentsUrl = 'https://jsonplaceholder.typicode.com/users'; ``` #### READ: Odczytanie danych studentów Aby odczytać dane z API, musimy użyć metody `http.get()` pochodzącej z klasy `HttpClient` wewnątrz stworzonego przez nas serwisu. Metoda ta zwróci nam obiekt w postaci `Observable<Student[]>`, który wypisze tablicę studentów pozyskanych podczas żądania HTTP. ```js getStudents(): Observable<Student[]> { return this.http.get<Student[]>(this.studentsUrl); } ``` Interfejs API danych z serwera określa kształt danych JSON, które otrzyma nasza aplikacja. Natomiast nasz interfejs API danych zwraca dane studentów jako tablicę. Może się też tak zdarzyć, że nie będziemy chcieli pobierać listy wszystkich studentów, a jedynie konkretnego studenta o danym numerze ID. Dodajmy zatem kolejną metodę, która to umożliwi: ```js getStudent(id: number): Observable<Student> { const url = `${this.studentsUrl}/${id}`; return this.http.get<Student>(url); } ``` Zwróć uwagę na trzy istotne różnice w stosunku do metody `getStudents()`: + `getStudent()` tworzy adres URL żądania z identyfikatorem żądanego studenta. + Serwer powinien odpowiedzieć jednym studentem, a nie szeregiem studentów. + `getStudent()` zwraca `Observable <Student>` *(obserwowalny obiekt "Student")* zamiast obserwowalnej tablicy studentów. #### UPDATE: Aktualizacja danych studenta Ogólna struktura metody `updateStudent()` jest podobna do struktury `getStudents()`, ale używa ona `http.put()` do utrwalenia zmian danego studenta na serwerze. Jako, że w tym projekcie korzystamy z *udawanego* REST API, to zmiany te nie zostaną zapisane. Najpierw dodajmy pod zmienną `studentsUrl` nowy obiekt, tj. `httpOptions`. Interfejs API sieci web oczekuje specjalnego nagłówka w żądaniach aktualizacji danych. ```js httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; ``` Teraz możemy przystąpić do utworzenia nowej metody: <!----> <!-- Dlaczego ANY? --> <!----> ```js updateStudent(student: Student): Observable<Student> { const url = `${this.studentsUrl}/${student.id}`; return this.http.put<Student>(url, student, this.httpOptions); } ``` Metoda `HttpClient.put()` przyjmuje trzy parametry: + Adres URL + Dane do aktualizacji (w tym przypadku edytowany student) + Opcje wysyłanego żądania #### CREATE: Tworzenie nowego studenta Tak jak w przypadku aktualizacji, nie utworzymy na serwerze *JSONPlaceholder* nowego użytkownika, ale zasymulujemy to zdarzenie. ```js addStudent(student: Student): Observable<Student> { return this.http.post<Student>(this.studentsUrl, student, this.httpOptions); } ``` Metoda `addStudent()` różni się od `updateStudent()` na dwa sposoby: + Wywołuje `HttpClient.post()` zamiast `put()`. + Oczekuje, że serwer wygeneruje `id` dla nowego studenta, którego zwróci jako `Observable<Student>`. #### DELETE: Usuwanie studenta ```js deleteStudent(student: Student): Observable<Student> { const url = `${this.studentsUrl}/${student.id}`; return this.http.delete<Student>(url, this.httpOptions); } ``` Zwróć uwagę na następujące rzeczy: + Metoda wywołuje `HttpClient.delete()`. + Adres URL to adres URL z identyfikatorem studenta do usunięcia. + Nie są wysyłane żadne dane, tak jak w przypadku `put()` i `post()`. ## Część VI ### Wyświetlenie listy studentów #### Modyfikacja `students.component.ts` Stworzyliśmy odpowiednie metody w naszym serwisie, ale póki co, nigdzie z nich nie korzystamy. Czas to zmienić. Otwórz `src/app/students/students.component.ts` i go nieco zmodyfikuj wg poniższych wskazówek. Zacznijmy od zaimportowania modelu studenta oraz serwisu `StudentService`: ```js import { Student } from '../student'; import { StudentService } from '../student.service'; ``` Zaraz przed konstruktorem zdefiniujmy zmienną `students` o tablicowym typie `Student`: ```js students: Student[]; ``` Wstrzyknijmy następnie zaimportowany serwis do konstruktora: ```js constructor(private studentService: StudentService) { } ``` Teraz wypadałoby stworzyć metodę, która przy pomocy serwisu zaciągnie dane studentów do tablicy `students`: ```js getStudents(): void { this.studentService.getStudents() .subscribe(students => this.students = students); } ``` Pozostało ją wywołać wewnątrz punktu zaczepienia *(ang. hook)* cyklu życia `ngOnInit()`: ```js ngOnInit(): void { this.getStudents(); } ``` Chociaż można by wywołać ją w konstruktorze, to nie jest to najlepsza praktyka. Konstruktor nie powinien się tym zajmować, a tym bardziej wywoływać funkcji, która wykonuje żądania HTTP do zdalnego serwera. Zarezerwuj go dla prostej inicjalizacji. #### Modyfikacja `students.component.html` Zmieńmy szablon komponentu, abyśmy mogli renderować dane z serwisu. Przejdź do `src/app/students/students.component.html` i zmodyfikuj zawartość znacznika `<tbody>` w taki sposób, by zastąpić wartości statyczne przez dynamiczne: ```html <tbody> <tr *ngFor="let student of students"> <td>{{student.id}}</td> <td>{{student.name}}</td> <td> <a href="mailto:{{student.email}}">{{student.email}}</a> </td> <td> <a href="#" class="button button-green">Edytuj</a> <button class="button button-red">Usuń</button> </td> </tr> </tbody> ``` A teraz przyjrzyjmy się temu, co właśnie zrobiliśmy. Jak zapewne się domyślasz, dodaliśmy nowe wiązanie danych *(ang. data-binding)* do szablonu: + `*ngFor`: wykorzystujemy pętlę `for...of` do iteracji tablicy obiektów `students`, którą zdefiniowaliśmy w komponencie. W tym przypadku powtórzy ona zawartość znacznika wiersza tabeli `<tr>` tyle razy ile studentów zwrócił serwis. + `{{student.id}}`: wyświetlamy pojedyczne właściwości danego obiektu poprzez interpolację *(ang. string interpolation)*. ### Usuwanie studenta #### Modyfikacja `students.component.ts` Dodaj metodę `delete()` do klasy komponentu: ```js delete(student: Student): void { this.students = this.students.filter(s => s !== student); this.studentService.deleteStudent(student).subscribe(); } ``` Pomimo, że komponent deleguje usuwanie studentów do `StudentService`, pozostaje odpowiedzialny za aktualizowanie własnej listy studentów `students`. Metoda `delete()` natychmiast usuwa studenta do z tablicy komponentu, przewidując, że StudentService odniesie sukces na serwerze. #### Modyfikacja `students.component.html` Przy każdym studencie na liście studentów znajduje się przycisk usuwania, lecz nie wykonuje on żadnej akcji. Dodajmy zatem do niego *event binding*: ```html <button class="button button-red" (click)="delete(student)">Usuń</button> ``` ## Część VII ### Nawigacja z routingiem Aby móc edytować poszczególnych studentów lub dodawać nowych, należałoby utworzyć osobne "podstrony", gdzie przy pomocy odpowiedniego formularza można byłoby to zrobić. Do realizacji tego celu wykorzystamy *routing*, czyli możliwość tworzenia adresów z różnymi widokami. W Angularze dobrą praktyką jest ładowanie i konfigurowanie routera w oddzielnym module najwyższego poziomu, który jest dedykowany do routingu i importowany przez główny moduł `AppModule`. Zgodnie z konwencją, nazwa klasy modułu to `AppRoutingModule` i należy ją utworzyć w pliku `app-routing.module.ts` wewnątrz folderu `src/app`. Wykorzystajmy CLI do wygenerowania tego modułu: ```bash ng generate module app-routing --flat --module=app ``` Zwróć uwagę na dwie dodatkowe *flagi* polecenia: + `--flat`: umieszcza plik w `src/app` zamiast w jego własnym folderze. + `--module=app`: rejestruje plik w tablicy importu `AppModule`. Wygenerowany plik, tj. `src/app/app-routing.module.ts` zastąp przy pomocy następującego kodu: ```js import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { StudentsComponent } from './students/students.component'; const routes: Routes = [ { path: '', redirectTo: '/students', pathMatch: 'full' }, { path: 'students', component: StudentsComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } ``` Spójrzmy na ten kod. Po pierwsze, `AppRoutingModule` importuje `RouterModule` i `Routes`, aby aplikacja mogła mieć funkcje routingu. Następny import, `StudentsComponent`, wskaże routerowi miejsce, do którego może się udać po skonfigurowaniu tras. Trasy informują router, który widok ma być wyświetlany, gdy użytkownik kliknie łącze lub wklei adres URL w pasku adresu przeglądarki. Typowa trasa w Angularze ma dwie właściwości: + `path`: ciąg, który odpowiada adresowi URL w pasku adresu przeglądarki. + `component`: komponent, który router powinien utworzyć podczas nawigowania do tej trasy. Aby aplikacja automatycznie nawigowała do listy studentów, dodaliśmy specjalną trasę, która przekierowuje "pusty" adres URL na ścieżkę `/students`. Zastosowano tutaj właściwość `pathMatch: 'full'`, która musi się pojawić podczas przekierowań tras z pustymi ścieżkami. Dalej widzimy dekorator `@NgModule`, który inicjuje router i rozpoczyna jego nasłuchiwanie w poszukiwaniu zmian lokalizacji przeglądarki. Importujemy w nim moduł `RouterModule`, który pobiera tablicę konfiguracyjną `routes`, a także ten same moduł eksportujemy, by był dostępny w całej aplikacji. ### Dodanie dyrektywy routera Otwórz szablon głównego komponentu `AppComponent` i zastąp element `<app-students>` na `<router-outlet>`: ```html <main class="container"> <router-outlet></router-outlet> </main> ``` Szablon `AppComponent` nie potrzebuje już `<app-students>`, ponieważ aplikacja będzie wyświetlać `StudentsComponent` tylko wtedy, gdy użytkownik będzie do niego nawigował. Od tej chwili to `<router-outlet>` mówi aplikacji jakie widoki *(komponenty)* mają być wyświetlane pod danym adresem. ## Część VIII ### Komponent edycji studenta Routing ma większy sens, gdy istnieje wiele widoków. Na razie jest tylko widok z listą studentów. Utwórzmy zatem nowy komponent, który będzie prezentował pola do edycji pojedynczego studenta. Wykonaj następujące polecenie: ```bash ng generate component studentEdit ``` Zastąp wygenerowany kod TypeScript w pliku `student-edit.component.ts` na poniższy: ```js import { Component, OnInit, Input } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; import { Student } from '../student'; import { StudentService } from '../student.service'; @Component({ selector: 'app-student-edit', templateUrl: './student-edit.component.html', styleUrls: [ './student-edit.component.css' ] }) export class StudentEditComponent implements OnInit { student: Student; constructor( private route: ActivatedRoute, private studentService: StudentService, private location: Location ) {} ngOnInit(): void { this.getStudent(); } getStudent(): void { // Uzyskanie wartości parametru "id" i jego konwersja na liczbę // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Unary_plus const id = +this.route.snapshot.paramMap.get('id'); this.studentService.getStudent(id) .subscribe(student => this.student = student); } goBack(): void { this.location.back(); } save(): void { // Zapisuje dane i przekierowuje do poprzedniego widoku this.studentService.updateStudent(this.student) .subscribe(() => this.goBack()); } } ``` Przyjrzyjmy się wstrzykiwanym elementom do konstruktora: + `ActivatedRoute` przechowuje informacje o trasie do tej instancji, tj. do `StudentEditComponent`. Komponent ten jest szczególnie zainteresowany parametrami trasy pobranymi z adresu URL, a my chcemy uzyskać wartość parametru `id`, który jest numerem identyfikacyjnym studenta. + `StudentService` pobiera dane o studencie ze zdalnego serwera. + `Location` jest serwisem Angulara służącym do interakcji z przeglądarką. Używamy go do powrotu do widoku, w którym się wcześniej znajdował użytkownik. Zastąp wygenerowany kod HTML nowego komponentu przez poniższy: ```html <section *ngIf="student"> <h2>Edycja #{{student.id}} <i>{{student.name}}</i></h2> <div> <label for="name">Imię i nazwisko:</label> <input type="text" id="name" [(ngModel)]="student.name"> </div> <div> <label for="email">Adres e-mail:</label> <input type="email" id="email" [(ngModel)]="student.email"> </div> <div> <button (click)="goBack()" class="button">Anuluj</button> <button (click)="save()" class="button button-green">Zapisz</button> </div> </section> ``` Powyższy kod zawiera dziwny "atrybut" przy polach tekstowych, prawda? `[(ngModel)]` to składnia dwukierunkowego wiązania danych *(ang. two-way data-binding)* w Angularze. W tym przypadku wiąże ona m.in. właściwość `student.name` z polem tekstowym w HTML, dzięki czemu dane są wymieniane w dwóch kierunkach: z właściwości `student.name` do pola tekstowego i z pola tekstowego z powrotem do `student.name`. Dzięki temu zaraz po wprowadzeniu zmiany w polu tekstowym zauważysz zmianę w tytule nad formularzem. To jednak nie wszystko. Element `<section>` posiada warunek `*ngIf="student"`. Sprawia on, że sekcja ta pojawi się dopiero wtedy, kiedy serwis pobierze z serwera dane o studencie, czyli wtedy, kiedy obiekt `student` nie będzie pusty. ### Modyfikacja głównego modułu Do poprawnego działania wiązania dwukierunkowego, które wprowadziliśmy w kodzie komponentu edycji poprzez `[(ngModel)]` w polu tekstowym, potrzebny dodatkowy moduł angularowy – `FormsModule`. Otwórz plik `src/app/app.module.ts` i zaimportuj go: ```js import { FormsModule } from '@angular/forms'; ``` Następnie dodaj go do tablicy `imports` wewnątrz dekoratora `@NgModule`: ```js imports: [ BrowserModule, HttpClientModule, AppRoutingModule, FormsModule ], ``` ### Modyfikacja routingu Aby przejść do edycji studenta, router potrzebuje odpowiedniej trasy. Zaimportujemy zatem nasz nowy komponent do `app-routing.module.ts`: ```js import { StudentEditComponent } from './student-edit/student-edit.component'; ``` Natomiast w samych ścieżkach routera dodajmy następującą: ```js { path: 'edit/:id', component: StudentEditComponent } ``` ### Modyfikacja komponentu listy studentów Liście studentów brakuje odpowiedniego hiperłącza, by móc przejść do edycji konkretnego studenta. Zastąp kod przycisku edycji na: ```html <a routerLink="/edit/{{student.id}}" class="button button-green">Edytuj</a> ``` Zastosowanie dyrektywy `routerLink` do elementu w szablonie powoduje, że element ten staje się łączem, które inicjuje nawigację do odpowiedniej trasy routera. Łącze to otwiera jeden lub więcej komponentów w `<router-outlet>` na stronie. ## Część IX ### Komponent tworzenia studenta Nasza aplikacja potrafi wykonać prawie każdą funkcję z czterech podstawowych, a mianowicie: pobieranie danych, aktualizacja danych i usuwanie wybranych elementów. Zostało nam tylko tworzenie nowych. Dodajmy odpowiednie pola pod listą wszystkich studentów. W tym celu otwórz plik `src/app/students/students.component.html` i dodaj: ```html <div class="new-student"> <h3>Dodaj nowego studenta</h3> <label for="name">Imię i nazwisko:</label> <input type="text" id="name" #name> <label for="email">Adres e-mail:</label> <input type="email" id="email" #email> <button (click)="add(name.value, email.value); name.value=''; email.value=''">Dodaj</button> </div> ``` W odpowiedzi na zdarzenie kliknięcia należy dorzucić do pliku `students.component.ts` metodę `add()`, która obsłuży dwa parametry: `name` oraz `email`. Dodatkowo po wywołaniu tej metody, czyścimy dotychczas wpisaną wartość obu pól, by można było dodać nowego studenta. Do klasy `StudentsComponent` dodaj następującą metodę: ```js add(name: string, email: string): void { // Usunięcie białych znaków z danych name = name.trim(); email = email.trim(); // Zaprzestanie wykonywania, kiedy pola są puste if (!name || !email) { return; } // Zaprzestanie wykonywania, kiedy adres e-mail nie zawiera "@" if (email.indexOf('@') < 1) { return; } // Przesłanie danych do serwera i zaktualizowanie lokalnej tablicy this.studentService.addStudent({ name, email } as Student) .subscribe(student=> { this.students.push(student); }); } ``` Gdy podana nazwa i email nie są puste, aplikacja tworzy niemalże kompletny obiekt studenta, któremu brakuje tylko identyfikatora i przekazuje go do metody serwisu `addStudent()`. Po pomyślnym zapisaniu, wywołanie zwrotne `subscribe()` odbiera z serwera nowego studenta i umieszcza go na liście. ## Część X ### Obsługa nieistniejących tras Przy użyciu CLI wygeneruj komponent `NotFound`, który będzie odpowiadał za obsługę nieistniejących tras routera, czyli takich przypadków, kiedy użytkownik poda adres w przeglądarce, którego nie obsługuje nasza aplikacja. Gdy już utworzysz komponent, zaimportuj go do pliku routera i dodaj nową trasę: ```js { path: '**', component: NotFoundComponent } ``` W powyższym przykładzie router wybiera trasę z dwiema gwiazdkami, jeśli żądany adres URL nie pasuje do żadnej ze ścieżek znajdujących się w tablicy tras. Zamiast jednak ładować komponent `NotFound` przy każdej nieistniejącej trasie, wykonaj odpowiednie przekierowanie na ścieżkę `/404`, która będzie wyświetlała ten komponent. ### Nowe pole studenta Zwracany plik JSON z API pod adresem http://jsonplaceholder.typicode.com/users zawiera o wiele więcej danych niż wykorzystujemy w aplikacji. Dodaj do tabeli spisu wszystkich studentów kolumnę *Nazwa użytkownika*, która będzie wyświetlała zawartość właściwości `username`. Dodaj również odpowiednie pole do komponentu edycji oraz tworzenia nowego studenta. ### Stylizacja komponentów Sama aplikacja, jak i jej komponenty nie wyglądają zbyt dobrze. Spraw, by poszczególne segmenty aplikacji prezentowały się lepiej wykorzystując CSS. Spróbuj również wprowadzić różne właściwości dla tego samego selektora, ale w różnych plikach komponentów, np. dla nagłówka H1. Sprawdź czy style komponentów wzajemnie na siebie wpływają i nadpisują. ## Część XI ### Wyszukiwanie studentów Przy bazie liczącej jedynie 10 studentów, znalezienie tego właściwego nie jest problemem. Kłopoty zaczynają się, kiedy liczba użytkowników mierzona jest w setkach lub tysiącach. Dodajmy zatem nad tabelką funkcję wyszukiwania studentów. Gdy użytkownik wpisze nazwę w pole wyszukiwania, będzie wykonywał żądanie HTTP dla studentów filtrowanych przez wpisaną frazę. Zacznijmy od dodania metody `searchStudents()` do serwisu `StudentService`: ```js searchStudents(term: string): Observable<Student[]> { // Jeśli brak frazy, zwróć pustą tablicę // "of" - tworzy obserwowalny strumień if (!term.trim()) { return of([]); } return this.http.get<Student[]>(`${this.studentsUrl}?name_like=${term}`); } ``` Metoda zwraca pustą tablicą, jeśli nie jest wyszukiwane żadne hasło. Reszta przypomina funkcję `getStudents()`. Jedyną znaczącą różnicą jest adres URL, który zawiera ciąg zapytań z wyszukiwaną frazą. ### Komponent wyszukiwania Stwórz `StudentSearchComponent` za pomocą CLI: ```bash ng generate component StudentSearch ``` Wygenerowany szablon `StudentSearchComponent` zastąp kodem, który będzie zawierał pole wyszukiwania i zwracał wyniki: ```html <div class="search-component"> <h3><label for="search-box">Wyszukaj studenta</label></h3> <input #searchBox id="search-box" (input)="search(searchBox.value)" /> <ul class="search-result"> <li *ngFor="let student of students$ | async" > <a routerLink="/edit/{{student.id}}"> {{student.name}} </a> </li> </ul> </div> ``` Wpisywana fraza w pole *search-box* przez użytkownika została powiązana poprzez *event binding* `(input)` z wywołaniem metody `search()`, którą dodamy do komponentu. Zauważ, że `*ngFor` iteruje listę o nazwie `students$`. Znak `$` wskazuje, iż obiekt `students$` nie jest zwykłą tablicą, lecz obserwowalną. Ponieważ `*ngFor` nie może nic zrobić z obiektem obserwowalnym, użyliśmu znaku *pipe* (`|`), a następnie słówka `async`. Dzięki temu automatycznie subskrybujemy się do `Observable`, więc nie musimy tego robić w klasie komponentów. Klasę komponentu `StudentSearch` zastąp następującym kodem: ```js import { Component, OnInit } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { Student } from '../student'; import { StudentService } from '../student.service'; @Component({ selector: 'app-student-search', templateUrl: './student-search.component.html', styleUrls: [ './student-search.component.css' ] }) export class StudentSearchComponent implements OnInit { students$: Observable<Student[]>; private searchTerms = new Subject<string>(); constructor(private studentService: StudentService) {} // Dodanie wyszukiwanej frazy do obserwowalnego strumienia search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.students$ = this.searchTerms.pipe( // Odczekanie 300ms po każdym naciśnięciu klawisza przed uwzględnieniem frazy debounceTime(300), // Zignorowanie frazy, jeżeli jest ona taka sama jak poprzednia distinctUntilChanged(), // Przełączenie na nowe obserwowalne wyszukiwanie za każdym razem, gdy zmieni się fraza // https://jsfiddle.net/ys7Lruns/ switchMap((term: string) => this.studentService.searchStudents(term)), ); } } ``` Przejdźmy do analizy powyższego kodu: + Właściwość `SearchTerms` jest obiektem `RxJS Subject`. `Subject` jest zarówno źródłem obserwowalnych wartości, jak i samym obserwatorem. Można go zasubskrybować tak, jak każdy inny obserwowalny obiekt. Możliwe jest również dodanie do niego nowej wartości poprzez wywołanie jego metody `next(value)`, tak jak robi to metoda `search()`. + Przekazanie nowej frazy wyszukiwania bezpośrednio do `searchHeroes()` po każdym naciśnięciu klawisza użytkownika spowodowałoby powstanie nadmiernej ilości żądań HTTP, co mogłoby negatywnie wpłynąć na zasoby serwera. Zamiast tego, wewnątrz metody `ngOnInit()` przetwarzamy `searchTerms` przez sekwencję operatorów RxJS, które redukują liczbę wywołań `searchStudents()`. + `debounceTime(300)` czeka 300 milisekund po każdym naciśnięciu klawisza przed przejściem do kolejnych operacji. Dzięki temu żądania nie będą wysyłane częściej niż co 300ms. + `distinctUntilChanged()` zapewnia, że żądanie jest wysyłane tylko w przypadku zmiany tekstu. + `switchMap()` wywołuje metodę wyszukiwania dla każdej wpisanej frazy, która przechodzi przez `debounce()` i `distinctUntilChanged()`. Unieważnia ona poprzednie obserwowalne frazy, zwracając tylko najnowsze wywołanie metody `searchStudents()`. Wyniki wcześniejszych wywołań są anulowane i odrzucane. <!-- Max tłumaczy switchMap(): https://www.youtube.com/watch?v=6lKoLwGlglE --> Ostatnia rzecz to dodanie kodu CSS do pliku styli komponentu *(`student-search.component.css`)*: ```css /* Pole wyszukiwania (<input>) */ #search-box { width: 200px; } /* Lista wyszukiwania (<ul>) */ .search-result { margin-top: 0; padding-left: 0; } .search-result li { width: 200px; height: 25px; padding: 5px; border: 1px solid #dddddd; border-top: 0; background-color: #ffffff; cursor: pointer; list-style-type: none; } .search-result li:hover { background-color: #999999; } .search-result li:hover a { color: #ffffff; } .search-result li a { display: block; color: #999999; text-decoration: none; } ``` ### Modyfikacja komponentu listy studentów Otwórz widok komponentu `StudentsComponent` i dodaj znacznik komponentu wyszukiwania `<app-student-search>` nad tabelą. ```html <section> <app-student-search></app-student-search> <table> ... </table> ... </section> ``` ## Część XII ### Wdrażanie aplikacji na GitHub Twoim zadaniem jest wdrożenie wykonanej aplikacji prosto na GitHub i uruchomienie jej w usłudze *GitHub Pages*. W tym celu zaloguj się do serwisu GitHub i utwórz nowe repozytorium dla projektu. Następnie wykonując odpowiednie polecenie w konsoli, powiąż folder projektu z nowym repo i przekaż kod. Po przesłaniu kodu na repozytorium, musisz dokonać zmiany w pliku `angular.json`, a dokładniej, to w pozycji `outputpath`. Domyślnie po uruchomieniu komendy budowania aplikacji, kod jest generowany w katalogu `/dist/<nazwa-projektu>`. Natomiast GitHub Pages obsługuje katalog `docs`, więc należy zmienić tę ścieżkę. Po tej zmianie, należy "zbudować" wszystkie zasoby aplikacji w celu przesłania na produkcję, uruchamiając to polecenie: ```bash ng build --prod --baseHref="https://<nazwa-użytkownika>.github.io/<nazwa-repo>/" ``` Komenda ta zminifikuje *(zmniejszy)* i połączy zasoby w celu szybszego uruchomiania aplikacji. Może jednak wystąpić problem z poprawnym linkowaniem do zasobów. Aby go rozwiązać, często wykorzystuje się dodatkową falgę `--baseHref`, do której przekazujemy docelowy adres, pod którym ma znaleźć się zbudowana aplikacja. Dodaj wygenerowany kod do lokalnego repozytorium i prześlij go na GitHub. Na końcu przejdź do ustawień repozytorium i w sekcji "GitHub Pages" przy etykiecie "Source" zaznacz katalog `/docs`.