Witam serdecznie w drugiej części rozważań o Angularowym routingu. W tej lekcji podzielimy routing na logiczne moduły napiszemy też „guarda”, a w następnej lekcji omówimy metodę router.navigate i parametry. Aktualna wersja do tej lekcji znajduje się pod adresem: https://github.com/taaaniel/foodCalc/tree/part_7_router_part_1_end
Poprzedni routing, który zrobiliśmy sprawdzi się w małej aplikacji. Dziś pokażemy bardziej profesjonalne podejście. Zabieramy się do pracy:
Nasz cały routing znajduje się w pliku app.module.ts, nie jest to dobre rozwiązanie. Lepiej aby każdy routing znajdował się w osobnym module. Takie rozwiązanie sprawi, że aplikacja będzie transparentna i łatwiejsza w utrzymaniu czy dalszym developingu. W praktyce powinno wyglądać to tak, że każdy moduł powinien posiadać własny routing, jeśli oczywiście jest to konieczne. Używając Angular CI w konsoli będąc w katalogu z aplikacją wpisujemy:
ng generate module app-routing --flat
Końcówka „–flat” każe stworzyć moduł bez opakowywania go w katalog.
Z app.module.ts wyjmujemy
RouterModule.forRoot([ { path: '', pathMatch: 'full', redirectTo: 'dashboard'}, { path: 'login', component: LoginComponent }, { path: 'dashboard', component: DashboardComponent }, { path: 'dish', component: DishComponent}, { path: 'product', component: ProductComponent} ])
i wkładamy do app-routing.module.ts importując poszczególne komponenty.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { DishComponent } from './dish/dish.component'; import { ProductComponent } from './product/product.component'; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forRoot([ { path: '', pathMatch: 'full', redirectTo: 'dashboard'}, { path: 'login', component: LoginComponent }, { path: 'dashboard', component: DashboardComponent }, { path: 'dish', component: DishComponent}, { path: 'product', component: ProductComponent} ]) ] }) export class AppRoutingModule { }
A w app.module.ts w miejscu zabrananego „RouterModule.forRoot” importujemy: AppRoutingModule. Wygląda to tak:
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'; @NgModule({ declarations: [ AppComponent, LoginComponent, ProductComponent ], imports: [ BrowserModule, BrowserAnimationsModule, SharedModule, HomeModule, DishModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Po skompilowaniu aplikacji widzimy że działa.
Teraz wydzielimy część tablicy routów do innych modułów plus samą tablice przerobimy na zmienną. W pliku app-routing.module.ts deklarujemy zmienna const „APP_ROUTES” która jest typem Tablicy Route czyli:
const APP_ROUTES: Route[]
Następnie podpinamy do niej tablicę routów i eksportujemy samą nazwę RouterModule gdyż musimy tę tablicę wyeksportować do głównego modułu..Plik po zmianach przedstawia się następująco:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Route } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { DishComponent } from './dish/dish.component'; import { ProductComponent } from './product/product.component'; const APP_ROUTES: Route[] = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard'}, { path: 'login', component: LoginComponent }, { path: 'dashboard', component: DashboardComponent }, { path: 'dish', component: DishComponent}, { path: 'product', component: ProductComponent} ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forRoot(APP_ROUTES) ] }) export class AppRoutingModule { }
Kiedy mamy już zrobiony app-routing, w konsoli przechodzimy do katalogu home i generujemy nowy moduł routingu home-routing:
ng generate module home-routing --flat
I przenieśmy praktycznie cały APP_ROUTES czyli :
const APP_ROUTES : Route[] = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard'}, { path: 'login', component: LoginComponent }, { path: 'dashboard', component: DashboardComponent }, { path: 'dish', component: DishComponent}, { path: 'product', component: ProductComponent} ]
Oczywiście nie zmienną APP_ROUTES musimy zmienić na inną, nazwiemy ją zatem HOME_ROUTES i zasisować metodę forChild() w której to przekazujemy tablicę routów HOME_ROUTES :
Plik będzie prezentować się 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'; const HOME_ROUTES: Route[] = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard'}, { path: 'dashboard', component: DashboardComponent }, { path: 'dish', component: DishComponent}, { path: 'product', component: ProductComponent} ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forChild(HOME_ROUTES) ], exports: [ RouterModule ] }) export class HomeRoutingModule { }
Nadszedł teraz czas na zastanowienie się jak będzie to wszystko wyglądać i gdzie będą miejsca na zmianę widoków, czyli gdzie wrzucimy dodatkowe <router-outlet></router-outlet> i jak rozplanujemy cały routing. Dla celów szkoleniowych proponuję taką oto hierarchię i rozplanowanie ścieżek:
- app.module.ts :
- login.component
- 404.component
- home.module.ts
- dashboard.component
- dish.component
- product.componet
Powodów takiego rozłożenia komponentów jest kilka:
- Musimy rozdzielić widok logowania od widoku naszej aplikacji, dlatego też w głownym routingu będą odnośniki do home komponentu, który będzie miał w sobie następny router-outlet, a także do komponentu logowania który powinien być osobnym widokiem.
- W komponencie home, który posiada sidebar, będą wyświetlane logiczne części naszej aplikacji. Będzie to dość wygodna sprawa
- Im bardziej skomplikowany routing tym więcej się nauczymy
Bazując na powyższym schemacie przeredagujmy plik app-routing.module.ts. Wyrzucimy stąd trochę ścieżek:
Gotowy plik wygląda tak:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Route } from '@angular/router'; import { LoginComponent } from './login/login.component'; const APP_ROUTES: Route[] = [ { path: '', pathMatch: 'full', redirectTo: 'home'}, { path: 'login', component: LoginComponent }, { path: '404', component: NotFoundComponent} ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forRoot(APP_ROUTES) ], exports: [ RouterModule ] }) export class AppRoutingModule { }
Dodałem tutaj komponent NotFoundComponent, i od razu go sobie wygenerujmy. Wracamy do głównego katalogu i w konsoli wpisujemy:
ng generate component not-found
Teraz przejdźmy do app.component.html i zamieńmy dotychczasowy selektor na:
<router-outlet></router-outlet>
Następnie przejdźmy do home-routing.module.ts i zmeińby tablice ścieżek:
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'; const HOME_ROUTES: Route[] = [ { path: 'home', component: HomeComponent, children: [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', component: DashboardComponent }, { path: 'dish', component: DishComponent}, { path: 'product', component: ProductComponent} ]}, ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forChild(HOME_ROUTES) ], exports: [ RouterModule ] }) export class HomeRoutingModule { }
Jak widzimy doszła nam tu nowa propercja ‚children’, która tworzy tak jakby ‚podrouty’, czyli jeśli w tym wypadku nadrzędny komponent to HomeComponent i ma on propercje ‚children’ to każda ścieżka będzie tworzona na zasadzie’home/(i tutaj nazwa ścieżki)’ i odpowiadający jej komponent. Pamiętać należy tutaj także oz mianie routerLink’a gdyż teraz po kliknięciu w sidebarze na jakikolwiek route angular zmodyfikuje adres dodając ‚/’ po http://localhost:4200/home/ czyli wyjdzie coś takiego: http://localhost:4200/home//dashboard. Zmieńmy to teraz.
Plik sidebar.component.html wygląda tak:
<section class="sidebar" fxFlex="220px"> <div class="main-menu-header"> <div fxLayout='row' fxFlex="200px"> <div fxFlex="33%"> <button mat-mini-fab> <mat-icon color="primary">message</mat-icon> </button> </div> <div fxFlex="33%"> <button mat-mini-fab> <mat-icon>favorite</mat-icon> </button> </div> <div fxFlex="33%"> <button mat-mini-fab> <mat-icon>list</mat-icon> </button> </div> </div> </div> <div class="main-menu"> <mat-list> <h3 mat-subheader>{{'SIDEBAR.NAVIGATION' | translate: lang}}</h3> <mat-list-item> <mat-icon mat-list-icon>dashboard</mat-icon> <h4 mat-line> <a [routerLink]="['dashboard']">{{'SIDEBAR.DASHBOARD' | translate: lang}}</a> </h4> </mat-list-item> <mat-list-item> <mat-icon mat-list-icon>restaurant</mat-icon> <h4 mat-line> <a [routerLink]="['dish']">{{'SIDEBAR.MY_DISHES' | translate: lang}}</a> </h4> </mat-list-item> <mat-list-item> <mat-icon mat-list-icon>fastfood</mat-icon> <h4 mat-line> <a [routerLink]="['product']">{{'SIDEBAR.MY_PRODUCTS' | translate: lang}}</a> </h4> </mat-list-item> <mat-divider></mat-divider> <h3 mat-subheader>Notes</h3> <mat-list-item> <mat-icon mat-list-icon>note</mat-icon> <h4 mat-line> <a href="#">{{'SIDEBAR.NOTES' | translate: lang}}</a> </h4> </mat-list-item> </mat-list> </div> </section>
Plik body.component.html zostaje bez zmian, tym razem <router-outlet></router-outlet> będzie korzystał z najbliżej położonego modułu routingu czyli home.routing.module.ts. Jest to dość nielogiczna sytuacja w Angularze i trzeba do niej przywyknąć. jednakże całośc nie działałaby poprawnie gdybyśmy nie dodali HomeRoutingModule do głównego modułu alikacji czyli do app.module.ts. A więc plik app.module.ts prezentuje się tak:
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'; @NgModule({ declarations: [ AppComponent, LoginComponent, ProductComponent, NotFoundComponent ], imports: [ BrowserModule, BrowserAnimationsModule, SharedModule, HomeModule, DishModule, AppRoutingModule, HomeRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Ważną rzeczą, o której trzeba pamiętać to kolejność importowania modułów routingu. Musimy pamiętać, że najpierw trzeba zaimportować nadrzędny moduł routingu (w tym wypadku AppRoutingModule ) a następnie moduł który jest podrzędny i będący w zasadzie tak jakby jego rozszerzeniem.
Jeszcze sprawdźmy jak wygląda home.module.ts
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HomeComponent } from './home.component'; import { SharedModule } from '../shared/shared.module'; import { HeaderComponent } from './header/header.component'; import { BodyComponent } from './body/body.component'; import { FooterComponent } from './footer/footer.component'; import { SidebarComponent } from './sidebar/sidebar.component'; import { DashboardModule } from '../dashboard/dashboard.module'; import { RouterModule } from '@angular/router'; @NgModule({ declarations: [HomeComponent, HeaderComponent, BodyComponent, FooterComponent, SidebarComponent], imports: [ CommonModule, SharedModule, DashboardModule, RouterModule, ], exports: [ HomeComponent ] }) export class HomeModule { }
I zauważmy , że nie importujemy tutaj HomeRoutingModule.
Przejdźmy teraz do obiecanego guarda. „Guardy” to takie klasy w Angularze, które decydują czy mamy dostęp do poszczególnych elementów aplikacji. Na początek zacznijmy od napisania widoku panelu logowania. Na razie będzie to tylko widok, więc nie ma tam nic szczególnego, nie będę zatem omawiał co tam się dzieje poza omówieniem dodania niektórych modułów.
Widok loginu czyli plik login.component.html wygląda tak:
<div class="loginComponent"> <div fxFlex></div> <mat-card fxFlex="320px"> <mat-card-header> <mat-card-title>Login</mat-card-title> </mat-card-header> <mat-card-content> <form> <div fxLayout="row"> <div fxFlex></div> <mat-form-field fxLayout="row"> <input matInput placeholder="Email" type="text" name="email" required> </mat-form-field> <div fxFlex></div> </div> <div fxLayout="row"> <div fxFlex></div> <mat-form-field fxLayout="row"> <input matInput placeholder="Password" type="password" name="password" required> </mat-form-field> <div fxFlex></div> </div> </form> </mat-card-content> <mat-card-actions> <div fxLayout="row"> <div fxFlex></div> <button mat-raised-button color="primary">Login</button> <div fxFlex></div> </div> </mat-card-actions> </mat-card> <div fxFlex></div> </div>
Teraz dodajmy style do login.component.scss:
.loginComponent { position: absolute; left: 50%; top: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); }
W widoku zaczęliśmy używać materialowych kontrolek do formularzy i samych formularzy dlatego też musimy dodać w app,module.ts moduł FormsModule, który obsługuje dyrektywy np ngModel i same reaktywne formularze.
App.module.ts wygląda teraz tak:
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'; @NgModule({ declarations: [ AppComponent, LoginComponent, ProductComponent, NotFoundComponent ], imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, SharedModule, HomeModule, DishModule, AppRoutingModule, HomeRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
A w shared.module.ts musimy zaimportować i wyeksportować moduły
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule, MatCardModule, MatMenuModule, MatToolbarModule, MatIconModule, MatListModule, MatChipsModule, MatTableModule, MatTabsModule, MatCheckboxModule, MatFormFieldModule, MatInputModule } from '@angular/material'; import { FlexLayoutModule } from '@angular/flex-layout'; import { HttpClientModule } from '@angular/common/http'; import { L10nConfig, L10nLoader, LocalizationModule, StorageStrategy, ProviderType, LogLevel } from 'angular-l10n'; const l10nConfig: L10nConfig = { locale: { languages: [ { code: 'pl', dir: 'ltr' }, { code: 'en', dir: 'ltr' } ], language: 'pl', storage: StorageStrategy.Cookie }, translation: { providers: [ { type: ProviderType.Static, prefix: './assets/locale/' } ], caching: true, composedKeySeparator: '.', } }; @NgModule({ declarations: [], imports: [ CommonModule, MatButtonModule, MatCardModule, MatMenuModule, MatToolbarModule, MatIconModule, MatListModule, MatChipsModule, MatTableModule, MatTabsModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FlexLayoutModule, HttpClientModule, LocalizationModule.forRoot(l10nConfig) ], exports: [ MatButtonModule, MatCardModule, MatMenuModule, MatToolbarModule, MatIconModule, MatListModule, MatChipsModule, MatTableModule, MatTabsModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FlexLayoutModule, HttpClientModule, LocalizationModule ] }) export class SharedModule { constructor(public l10nLoader: L10nLoader) { this.l10nLoader.load(); } }
Teraz wybiegnijmy sobie trochę w przyszłość i pomyślmy jak będzie zachowywał się ten strażnik czyli „guard”. Otóż jego zadaniem będzie nie wpuszczenie użytkownika w niektóre miejsca, lub wpuszczenie go po wykonaniu określonych czynności. Tymi czynnościami będzie np zalogowanie się, czyli podanie odpowiednich „credentiali”(loginu i hasła). Guard może mieć też inne funkcje, np. pokazywanie różnych danych różnym użytkownikom, lub odmowa wejścia w niektóre części aplikacji, czyli będzie pomagał nam w programowaniu dostępu czy określeniu uprawnień w zależności od roli usera.
Sam system logowania(autoryzacji) i autentykacji będzie omawiany w następnych częściach. Teraz skupimy się na tym że nasz User nie jest zalogowany, czyli jeśli chciałby przejść gdziekolwiek w naszej aplikacji „Guard” zawróci go na stronę logowania.
Aby zapewnić sobie łatwość w utrzymaniu apki stwórzmy nowy katalog o nazwie „auth” w katalogu „app”. Będziemy tam trzymać wszystie pliki, którę będą powiązane z autentykacją użytkownika.
Natomiast w katalogu auth za pomocą komendy w Angular CLI stwórzmy sobie servis guard:
ng generate guard auth
Stworzyliśmy nasz pierwszy serwis w kursie. Pierwszą rzeczą jaką należy zrobić jest zarejestrowanie każdego serwisu w głównym module aplikacji, lub module który będzie z niego korzystał. Rejestracji dokonujemy dopisując nazwę klasy servisy w tablicy providers w app.module.ts czyli tablica providers będzie wyglądała tak:
providers: [AuthGuard],
Nasz auth.guard.ts na początku wygląda tak:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { return true; } }
Jak widzimy klasa „AuthGuard ” implementuje interfejs „CanActivate” z angularowego routera. Ten Interfejs posiada metodę „canActivate”, któr posiada 2 parametry:
next i state. () Metoda zwraca observable, promise lub boolean. (Nie przejmujemy się na razie tym. W następnych części opisze czym są observable przy tworzeniu serwisów, gdzie dowiemy się więcej ) I jeśli, w naszym przypadku, zwraca „true” to oznacza, że użytkownik będzie miał dostęp do widoku. Dlatego takiego guarda będziemy podpinać do każdej ścieżki routów, gdzie chcemy aby pilnował on tych widoków. Czyli nakładamy go na ścieżkę, by pilnował tego widoku, np:
{ path: '', pathMatch: 'full', redirectTo: 'home', canActivate: [AuthGuard]},
Metodzie „canActivate” przypisujemy tablicę „Guardów” bo może ich być więcej w zależności jak będziemy filtrować sobie dostęp do danego widoku.
W naszej apce mamy na razie 2 pliki z rotingiem. Po dodaniu Guarda wyglądają one tak:
app-routing.module.ts
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Route } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { NotFoundComponent } from './not-found/not-found.component'; import { AuthGuard } from './auth/auth.guard'; const APP_ROUTES: Route[] = [ { path: '', pathMatch: 'full', redirectTo: 'home', canActivate: [AuthGuard]}, { path: 'login', component: LoginComponent }, { path: '404', component: NotFoundComponent } ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forRoot(APP_ROUTES) ], exports: [ RouterModule ] }) export class AppRoutingModule { }
i home-routing.module.ts
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'; 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]} ]}, ]; @NgModule({ declarations: [], imports: [ CommonModule, RouterModule.forChild(HOME_ROUTES) ], exports: [ RouterModule ] }) export class HomeRoutingModule { }
Pamiętajmy aby zaimportować AuthGuard w powyższych plikach jeśli piszemy je samodzielnie:)
Teraz w pliku auth.guard.ts zmieniamy wartość zwracaną na false:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { return false; } }
I kompilujemu aplikację. Odpalamy ją w przeglądarce i… nie mamy nic:) Przekierowuje nas, prawie w każdym przypadku na : http://localhost:4200/
Ale w app-routing.module.ts mamy 2 routy, które nie mają guarda, jeśli więc wejdziemy na ścieżkę np : http://localhost:4200/login To naszym oczom ukaże się panel logowania.
Czyli nasz guard działa. Jednakże jak zrobić aby w przypadku wejścia na jakikolwiek ścieżkę , której broni nasz strażnik, a my nie mamy dostępu (return false), przekierowywał na strone logowania? Tego dowiemy się w następnej części gdzie poznamy ważną metodę „router.navigate”.
Aktualna do tego wpisu wersja aplikacji znajduje się pod adresem:
https://github.com/taaaniel/foodCalc/tree/part_7_router_part_2_end
Źródła
- https://angular.io/api/router -oficjalna dokumentacja Angualra odnośnie routingu
Zapraszam do kometowania.
One thought on “Kurs Angular Material – cz.8 Routing (cz.2)”
Ale super artykuł, dziękuje 🙂