26 June 2019
Angular (13) -- Routing
by Jerry Zhang
Day 21: Angular Routing
Route Guards
Create a new service
auth-guard.service.ts
This class should implement CanActivate
interface in @angular/router
. It enforces us to write a canActivate()
method. This method will receive two arguments, ActivatedRouteSnapshot and RouterStateSnapshot.
Where do we get these two parameters from? We will define that Angular should execute this code before a route is loaded.
canActivate()
method also return something. We put the return type after the parameters. It can return an
Observable
with boolean, a Promise
with boolean, or just a boolean.
The Observable and Promise are asynchronous, while returning a boolean is synchronous. When is it synchronous? Some guards execute some code which runs completely on the client, thus it is synchronous, or maybe some code takes a couple of seconds to finish because of a timeout or reach out to a server, so it runs asynchronously.
Mock a fake auth service
To test our auth-guard service, we create a fake service.
export class AuthService {
loggedIn = false;
isAuthenticated() {
const promise = new Promise(
(resolve, reject) => {
setTimeout(() => {
resolve(this.loggedIn);
}, 800);
}
);
return promise;
}
login() {
this.loggedIn = true;
}
logout() {
this.loggedIn = false;
}
}
Use the fake service in the auth-guard
-
Add @Injectable()
-
Inject the fake service in the constructor.
-
In the canActivate method, we want to check whether this user is logged in or not.
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot):
Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.isAuthenticated()
.then(
(authenticated: boolean) => {
if (authenticated) {
return true;
} else {
this.router.navigate(['/']);
return false;
}
}
);
}
If this user is authenticated, return true; otherwise navigate this user to the root and return false.
Define which routes should be protected by this guard.
In the app-routing.module file, add canActivate property to the routes that we want to guard. It automatically get applied to all the child routes.
{ path: 'servers', canActivate: [AuthGuardService], component: ServersComponent, children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent }
] },
This will make sure that servers routes as well as all its child routes, are now only accessible if the AuthGuard, canActivate method returns true in the end.
Tips: Remember to add the two new services as providers in our app.module.ts
Now, we can only click other tabs. If we click the service tab, it will lead us to the home page after 800 milliseconds.
Protecting Child Routes with canActivateChild
We could move the canActivate attribute to any child path, but if we have many child-paths, then we have to add it to all of them. This is not a convenient way.
There is another guard that we can use, CanActivateChild. We can implement this interface and overwrite
canActivateChild
method.
Because the logic is all the same, we can reuse our canActivate
method.
canActivateChild(childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot):
Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.canActivate(childRoute, state);
}
Then we can use canActivateChild in the app-routing.module
We can choose whether we want to protect the whole route or its child routes.
{ path: 'servers',
// canActivate: [AuthGuardService],
canActivateChild: [AuthGuardService],
component: ServersComponent,
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent }
] },
Add login and logout buttons
In the home component, we add two buttons.
<button class="btn btn-light" (click)="onLogin()">Login</button>
<button class="btn btn-light" (click)="onLogout()">Logout</button>
Then in these two methods, we simply call the authService which needs to be injected to the home component.
export class HomeComponent implements OnInit {
constructor(private router: Router,
private authService: AuthService) { }
ngOnInit() {
}
onLoadServer(id: number) {
this.router.navigate(['/servers', id, 'edit'], {queryParams: {allowEdit: '1'}, fragment: 'loading'});
}
onLogin() {
this.authService.login();
}
onLogout() {
this.authService.logout();
}
}
Now this is working.
Controlling Navigation with canDeactivate
canActivate is used for controlling access into a route, now we will discuss whether a user is allowed to leave a route or not.
This is very useful because sometimes we want to ask a user if he/she really want to leave this page, for example, without saving, the user click the go back by accident.
-
Add a new property
changesSaved
and initialize to false. -
When we change something and click the button, we’ll change this property to true.
-
navigate to an upper route.
this.router.navigate(['../'], {relativeTo: this.route});
Add a guard service
A guard always needs to be a service.
- In edit-server folder, create a new service,
can-deactivate-guard.service.ts
In this service, we firstly export an interface.
import {Observable} from 'rxjs';
import {ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree} from '@angular/router';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(component: CanComponentDeactivate,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot):
Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return component.canDeactivate();
}
}
Config app-routing.module file
In the app-routing.module, we can add a canDeactivate
property, which is passed in an array. We point to our
CanDeactivateGuard.
Now, Angular will run this guard whenever we try to leave this path.
- Do not forget to add this CanDeactivate guard as a provider in the app.module.ts file.
Implement CanComponentDeactive interface in the component
In the component, we need to the logic to check whether this user can leave or not.
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
if (!this.allowEdit) {
return true;
}
if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) &&
!this.changesSaved) {
return confirm('Do you want to discard the changes?');
} else {
return true;
}
}