Witam w 3 i ostatniej części w tym kursie o routingu. W tej lekcji poznamy metodę „router.navigate” , opowiemy sobie o parametrach w routingu i porozmawiamy sobie o mockach czyli o takiej atrapie obiektu który w sposób kontrolowany naśladuje i zachowuje się jak prawdziwy obiekt, który dostajemy tak jakby z naszego backendu. Zabieramy się do pracy:)
Początek od którego zaczniemy w tym wpisie znajduje się pod adresem: https://github.com/taaaniel/foodCalc/tree/part_7_router_part_2_end.
W poprzedniej lekcji skończyliśmy na tym, że jeśli nie weszliśmy na http://localhost:4200/login lub http://localhost:4200/404 to naszym oczom pojawiała się pusta strona. Chcemy jednak inaczej. Nasz guard powinien bronić dostępu do każdej strony jeśli nie jesteśmy zalogowani. Załóżmy na początku ze nie jesteśmy zatem zalogowani i jeśli wpiszemy w adresie jakikolwiek adres po http://localhost:4200/ (gdzie jest to domyślny adres naszej apki) to przekieruje nas na http://localhost:4200/login. Przyda nam się metoda ” „router.navigate”
Przechodzimy do pliku auth.guard.ts i jak pamiętamy z poprzedniej lekcji metoda „canActivate” zwracała nam „false” dlatego guard bronił dostępu do innych ścieżek. Spróbujmy zmienić ja na jakiś warunek typu : ” Jeśli wartość jest „true” to przenieś mnie na „dashboard”, a jeśli jest „false” to na „login”” Będzie to taka symulacja, że nie jesteśmy dalej zalogowanie. Wprowadźmy sobie zmienną isLoggedIn, która będzie odpowiadała za stan, czy jesteśmy zalogowani czy nie. Dodajmy też constructor do auth.guard.ts w którym będziemy mieli Router Angularowy , oraz napiszmy nasz warunek. Całość wygląda tak :
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { isLoggedIn: boolean; constructor(private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { this.isLoggedIn = true; if (this.isLoggedIn) { return true; } this.router.navigate(['/login']); return false; } }
isLoggedIn jest ustawione na true więc przekieruje nas na Dashboard a jełśi ustawimy ją na false, linijka:
this.router.navigate(['/login']);
przekieruje nas na http://localhost:4200/login. I tak działa właśnie metoda router.navigate czyli przekierowuje nas na podany w parametrze adres.
Teraz zajmijmy się parametrami routingu.
Wyobraźmy sobie sytuacje gdzie mamy jakieś swoje produkty, które posiadają informacje: kalorie, tłuszcze, białka, nazwę i swój unikalny ID. I chcielibyśmy, aby po kliknięciu na taki produkt przekierowywało nas na komponent gdzie odczytamy jego informacje. W teorii możemy stworzyć osobny komponent dla każdego produktu, jednakże w teorii gdy produktów będziemy mieli kilkaset, mija się to z celem. Dlatego wykorzystany tutaj parametry routingu.
W folderze produkt stwórzmy katalog z komponentem product-details. Zatem w konsoli wchodzimy w katalog „product” i wpisujemy:
ng generate component product-details
Będzie to komponent „powtarzalny’ gdyż będzie on zawierał dane dynamiczne w zależności od parametru routingu.
Teraz w pliku product.component.ts stwórzmy sobie zmienną productList która będzie typu „Tablica obiektów” czyli: productList: Array<object>;
Potem stwórzmy tę tablicę dodając do niej przykładowe dane:
this.productList = [ { 'id': 1, 'unit': 1, 'weight': 100, 'name': 'Jajko', 'carbohydrates': 50, 'proteins': 20, 'fats': 30 }, { 'id': 2, 'unit': 1, 'weight': 100, 'name': 'Chleb razowy', 'carbohydrates': 130, 'proteins': 4, 'fats': 30 }, { 'id': 3, 'unit': 1, 'weight': 100, 'name': 'Jabłko', 'carbohydrates': 150, 'proteins': 4, 'fats': 3 }, { 'id': 4, 'unit': 1, 'weight': 100, 'name': 'Ziemniaki', 'carbohydrates': 50, 'proteins': 8, 'fats': 3 }, { 'id': 5, 'unit': 1, 'weight': 100, 'name': 'Wołowina', 'carbohydrates': 250, 'proteins': 17, 'fats': 36 } ];
Czyli całość pliku product.component.ts wygląda tak:
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-product', templateUrl: './product.component.html', styleUrls: ['./product.component.scss'] }) export class ProductComponent implements OnInit { productList: Array<object>; constructor() { } ngOnInit() { this.productList = [ { 'id': 1, 'unit': 1, 'weight': 100, 'name': 'Jajko', 'carbohydrates': 50, 'proteins': 20, 'fats': 30 }, { 'id': 2, 'unit': 1, 'weight': 100, 'name': 'Chleb razowy', 'carbohydrates': 130, 'proteins': 4, 'fats': 30 }, { 'id': 3, 'unit': 1, 'weight': 100, 'name': 'Jabłko', 'carbohydrates': 150, 'proteins': 4, 'fats': 3 }, { 'id': 4, 'unit': 1, 'weight': 100, 'name': 'Ziemniaki', 'carbohydrates': 50, 'proteins': 8, 'fats': 3 }, { 'id': 5, 'unit': 1, 'weight': 100, 'name': 'Wołowina', 'carbohydrates': 250, 'proteins': 17, 'fats': 36 } ]; } }
I to jest własnie nasz Mock czyli takie kontrolowane, przykładowe dane które będziemy na razie używać.
Aby wyświetlić dane te dane w komponencie musimy skorzystać z pętli Angularowej „*ngFor” która to iteruje po podanej przez nas tablicy. w tym wypadku tablicy productList. Całoś wygląda tak:
<ng-container *ngIf="productList"> <mat-list> <mat-list-item *ngFor="let product of productList"> <span class="routerPointer">{{ product?.name }}</span> </mat-list-item> </mat-list> </ng-container>
Dzięki Material Design użytym w tym przykładnie mamy ładną listę. linijka:
<ng-container *ngIf="productList">
Mówi nam, że jeśli nie będzie naszej productList to nie pokaże się ten Container. Znacznik Angularowo -Htmlowy „ng-container” jest to taki znacznik który nie będzie widziany w drzewie DOM w HTML naszej aplikacji (nie wpływa na strukturę ) i znakomicie służy do pokazywanie i ukrywania kontenerów czy komponentów.
Kod pętli:
*ngFor="let product of productList"
Tutaj tworzymy sobie element „product” który jest jakby pojedynczym elementem tworzonym przez dyrektywę „ngFor” na naszej tablicy „productList”, Metoda ta Iteruje po tablicy i tworzy obiekt, w naszym przypadku „produkt” do którego potem możemy się odnosić. Czyli reprezentuje każdy poszczególny element tablicy.
Idąc dalej:
{{ product?.name }}
W nawiasach {{}} wypisujemy dane w widoku. Jest to tzw. interpolacja czyli mówiąc w skrócie wyświetla nam dane. Zauważmy że na końcu naszej zmiennej produkt jest znak zapytania a następnie odniesienie się do propercji obiektu „name”. Po co jest ten znak zapytania? Jest to operator „Elvis”. Ten operator zapobiega wielu błędom środowiska wykonawczego. Jeśli spróbujesz uzyskać dostęp do głęboko zagnieżdżonej właściwości np naszej tablicy, która możne na początku się nie pojawić, która nie istnieje, będziesz miał błąd. W tym momencie pojawia się ten operator Elvis który krótko mówiąc, zapobiega powstaniu takiego błędu, czyli aplikacja się nie wywali a będzie czekała na pojawienie się danych.
Na koniec dodajmy jeszcze style w product.component.scss:
.routerPointer { cursor: pointer; }
Teraz przjdźmy do pliku home-routing.module,ts i dodajmy tam ścieżkę, która będzie reprezentować nasz produkt o określonym ID.
{ path: 'product/:id', component: ProductDetailsComponent, canActivate: [AuthGuard]}
Jak widzimy jest to prawie taka sama ścieżka jak inne, z tym że posiada ona nasz parametr: ‚/:id”, który możemy oczywiście nazwać dowolnie. I oczywiście komponent ProductDetailsComponent który będzie pokazywać szczegóły produktu. Cały plik wygląda obecnie tak:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Route } from '@angular/router'; import { DashboardComponent } from '../dashboard/dashboard.component'; import { DishComponent } from '../dish/dish.component'; import { ProductComponent } from '../product/product.component'; import { HomeComponent } from './home.component'; import { AuthGuard } from '../auth/auth.guard'; import { ProductDetailsComponent } from '../product/product-details/product-details.component'; const HOME_ROUTES: Route[] = [ { path: 'home', component: HomeComponent, children: [ { path: '', pathMatch: 'full', redirectTo: 'dashboard', canActivate: [AuthGuard] }, { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, { path: 'dish', component: DishComponent, canActivate: [AuthGuard]}, { path: 'product', component: ProductComponent, canActivate: [AuthGuard]}, { path: 'product/:id', component: ProductDetailsComponent, canActivate: [AuthGuard]} ]}, ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forChild(HOME_ROUTES) ], exports: [ RouterModule ] }) export class HomeRoutingModule { }
Ok a teram musimy jeszcze przekazać informacje o ID produktu jeśli klikamy w niego. Zatem w pliku product.component.html dopisujemy metodę”routerLink i przekazujemy ID produktu.
[routerLink]="['/home/product', product.id]"
Całość wygląda tak:
<ng-container *ngIf="productList"> <mat-list> <mat-list-item *ngFor="let product of productList" [routerLink]="['/home/product', product.id]"> <span class="routerPointer">{{ product?.name }}</span> </mat-list-item> </mat-list> </ng-container>
Czyli w każdym <mat-list-item> przekazujemy w „[routerLink]” dwa parametry: adres na jaki ma przkierować nas link i drugi element to parametr. W naszym przypadki jest to „product.id”.
Teraz chcielibyśmy zobaczyć czy to w ProductDetailsComponent. A zatem musimy zadeklarować zmienną np . „productId”, która będzie reprezentowała Id produktu. Zmienną tę będziemy pobierać bezpośrednio z adresu naszej przeglądarki , czyli jeśli wybierzmy produkt o Id równym 2 to pokaże nam się 2 itd.
Plik product-details.component.ts wygląda tak:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-product-details', templateUrl: './product-details.component.html', styleUrls: ['./product-details.component.scss'] }) export class ProductDetailsComponent implements OnInit { productId: number; constructor(private route: ActivatedRoute) { } ngOnInit() { this.loadedProduct(); } loadedProduct() { this.productId = +this.route.snapshot.params['id']; } }
Mamy tutaj taką metodę:
loadedProduct() { this.productId = +this.route.snapshot.params['id']; }
Która to przypisuje naszej zmiennej „productId” parametr z routingu dzięki metodzie: „route.snapshot.params”. metodę „loadedProduct()” wywołujemy w metodzie „ngOnInit()”, która jest natywną metodą Angulara i odpowiada za tzw cykl życia komponentu. O tym będę opowiadał w następnej lekcji. Chodzi o to że uruchamia się ona w momencie inicjalizacji komponentu.
Widok naszego product-details.component.html wygląda tak:
<mat-toolbar> Product id: {{ productId }} </mat-toolbar>
Komponent ProductDetailsComponent powinniśmy dodać jeszcze do modułu. Dodajmy go zatem do modułu app.module.ts, gdzie jest jego rodzic ProductComponent:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { SharedModule } from './shared/shared.module'; import { HomeModule } from './home/home.module'; import { LoginComponent } from './login/login.component'; import { ProductComponent } from './product/product.component'; import { DishModule } from './dish/dish.module'; import { AppRoutingModule } from './app-routing.module'; import { HomeRoutingModule } from './home/home-routing.module'; import { NotFoundComponent } from './not-found/not-found.component'; import { FormsModule } from '@angular/forms'; import { AuthGuard } from './auth/auth.guard'; import { ProductDetailsComponent } from './product/product-details/product-details.component'; @NgModule({ declarations: [ AppComponent, LoginComponent, ProductComponent, NotFoundComponent, ProductDetailsComponent ], imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, SharedModule, HomeModule, DishModule, AppRoutingModule, HomeRoutingModule ], providers: [AuthGuard], bootstrap: [AppComponent] }) export class AppModule { }
Skompilujemy naszą apkę i kliknijmy w jakiś produkt. Ukaże nam się coś w tym stylu:
Czyli produkt o ID =2 który kliknęliśmy.
Podsumowując umiemy już tworzyć routing, routing zagnieżdżony, przechodzić pomiędzy komponentami, dodawać parametry czy tworzyć guarda. W następnej lekcji, omówimy cykle życia komponentów. Aktualny stan kodu do końca tego wpisu znajuduje się tutaj: https://github.com/taaaniel/foodCalc/tree/part_9_router_part_3_end
Notatki
- https://hackernoon.com/the-angry-angular-asyncpipe-the-evil-elvis-operator-89293e37e04d O operatorze „Elvis”
Źródła
- https://angular.io/api/router -oficjalna dokumentacja Angualra odnośnie routingu
- https://angular.io/guide/structural-directives -oficjalna dokumentacja Angualra odnośnie dyrektyw np ng-conteinera
Zapraszam do komentowania i eksperymentowania.
Najnowsze komentarze