Router Events
- Routing Basics
- Feature Target
- Steps
Feature Target
Display a spinner in case the application is waiting for the data being loaded and seems not to be responding.
Step 1
To observe the unwanted behaviour lets add a delay to our Service.
findOne(id: number): Observable<Book> {
return this.http.get<Book>(BookService.BOOK_URI + '/' + id)
.pipe(delay(1000));
}
How does the app behave now?
Step 2
To be sure that it is the resolve that is causing the issue lets trace it.
RouterModule.forRoot(routes, { enableTracing: true }),
See the resolve now?
Step 3
Lets handle the ResolveStart - ResolveEnd case and show something special
export class AppComponent {
isLoadingData = false;
constructor(private router: Router) {
router.events.subscribe((routerEvent: RouterEvent) => {
this.checkRouterEvent(routerEvent);
});
}
Step 4
checkRouterEvent(routerEvent: RouterEvent): void {
if (routerEvent instanceof ResolveStart) {
this.isLoadingData = true;
}
if (routerEvent instanceof ResolveEnd ||
routerEvent instanceof NavigationCancel ||
routerEvent instanceof NavigationError) {
this.isLoadingData = false;
}
}
Step 5
The template can now use the isLoadingData flag now.
Final Step
Lets tweak it with some CSS
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
Feature Target
Prevent the user from leaving a dirty form unless he confirms his actions.
Step 1
Lets define an DirtyAware Interface at (rename the folder):
src/app/shared/routing/dirty-aware.ts
And inside:
export interface DirtyAware {
isDirty(): boolean;
}
Step 2
Now we can create an abstract guard service implementing a CanDeactivate
interface
src/app/shared/routing/can-deactivate-guard.service.ts
And inside:
@Injectable()
export class CanDeactivateGuard implements CanDeactivate<DirtyAware> {
canDeactivate(): Observable<boolean> | boolean {
return false;
}
}
Step 3
Now, having a dummy guard, lets connect it to our existing Routing at routes.ts
...
component: BookDetailsComponent,
resolve: {
book: BookDetailsResolver
},
canDeactivate: [CanDeactivateGuard]
},
{
path: 'book',
component: BookDetailsComponent,
canDeactivate: [CanDeactivateGuard]
},
...
Try it now.
Step 4
Naturally, it is not present in the app.module.ts - providers part, adapt it.
@NgModule({
...,
imports: [
...
],
providers: [CanDeactivateGuard],
bootstrap: [AppComponent]
})
Step 5
Now it is time to make use of the DirtyAware interface.
export class BookDetailsComponent implements OnInit, DirtyAware {
and the implmementation:
isDirty(): boolean {
return this.bookForm.dirty && !this.submitted;
}
Step 6
The Guard itself also needs to use it
canDeactivate(
component: DirtyAware
): Observable<boolean> | boolean {
if (!component.isDirty()) {
return true;
}
return false;
}
Step 7
Improve the UX - ask the User now
canDeactivate(
component: DirtyAware
): Observable<boolean> | boolean {
if (!component.isDirty()) {
return true;
}
if (confirm('Are you sure to cancel?')) {
return true;
}
return false;
}
Step 8*
Rework the modal dialog to use the Material Design. Generate a new Component
ng g c can-deactivate-dialog
Step 9*
Invoke it when the can-deactivate-guard is triggered
constructor(public dialog: MatDialog) {}
...
const dialogRef = this.dialog.open(CanDeactivateDialogComponent);
return dialogRef.afterClosed().pipe(
map(result => {
if (result) {
return true;
}
return false;
})
);
Step 10*
This does not work now because of a very good reason.
BookModule,
MatDialogModule
],
entryComponents: [CanDeactivateDialogComponent],
providers: [CanDeactivateGuard],
Step 11*
Now, lets tweak the dialog so it displays something meaningfull.
Discard changes
Are you sure?
Step 12*
Repair the declarations of the dialog component, so it works.
@NgModule({
declarations: [AppComponent,
CanDeactivateDialogComponent],
imports: [
Feature Target
Allow adding new Books only for those Users having a concrete permission.
Step 1
Create a Roles Object
export enum Role {
ANONYMOUS, ADMIN
}
Step 2
Extend the Routes so they are able to hold the Roles that will guard them.
export interface AuthorizedRoute extends Route {
authorizedRoles?: Role[];
permitAll?: boolean;
children?: AuthorizedRoutes;
}
export declare type AuthorizedRoutes = AuthorizedRoute[];
Step 3
Overwrite the Routing configuration and provide some Role definitions
...
permitAll: true
...
path: 'book',
component: BookDetailsComponent,
canDeactivate: [CanDeactivateGuard],
authorizedRoles: [Role.ADMIN]
},
Step 4
Trigger reset config so the router can actually use the guards.
export class AppModule {
constructor (private router: Router) {
// add security checkers
router.resetConfig(addAuthorizationGuards(routes));
}
Step 5
Provide a bit of magic so the Routing is actually using the Roles. (File: add-authorization-guards.ts)
export function addAuthorizationGuards(routes: Routes): Routes {
addAuthorizationGuardsToRoutesHavingPathsAndComponents(routes);
return routes;
function addAuthorizationGuardsToRoutesHavingPathsAndComponents(routesToGuard: Routes) {
for (const route of routesToGuard) {
if (route.path && route.component) {
route.canActivate = route.canActivate || [];
route.canActivate.push(ActivateIfUserAuthorized);
}
if (route.children) {
addAuthorizationGuardsToRoutesHavingPathsAndComponents(route.children);
}
}
}
}
Step 6_1
Now implement the overwritten guard.
@Injectable()
export class ActivateIfUserAuthorized implements CanActivate {
constructor(private security: SecurityService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
if (route && route.routeConfig && route.routeConfig['permitAll']) {
return true;
}
Step 6_2
if (route && route.routeConfig && route.routeConfig['authorizedRoles']) {
const routeAuthorizedRoles: Role[] = route.routeConfig['authorizedRoles'];
return this.security.getCurrentUserRoles().pipe(
map((roles: Role[]) =>
roles.reduce(
(prev, role) => routeAuthorizedRoles.includes(role) || prev,
false
)
),
catchError(() => of(false))
);
}
return false;
}
}
Step 7
Create a Security Service that returns the current User-Roles.
@Injectable()
export class SecurityService {
currentRoles: Role[] = [Role.ADMIN, Role.ANONYMOUS];
getCurrentUserRoles(): Observable<Role[]> {
return of(this.currentRoles);
}
}
Step 8
Now provide the Service and Guard. Then see if everything is working okay. Play with it changing the Roles that the Service returns.
providers: [BookService,
ActivateIfUserAuthorized,
SecurityService]
Step 9
Provide a bar with a slider so one can switch the roles on and off. (File: app.component.html)
and style it
.admin-menu {
background-color: #3498db;
}
Step 10
Import it in the app.module so it runs smoothly.
BookModule,
MatDialogModule,
MatSlideToggleModule
],
entryComponents: [CanDeactivateDialogComponent],
Step 11
Make use of the slider!
Step 12
Implement event handling
slideUpdate(e: Event) {
console.log('Changing App Roles');
this.securityService.changeRoleTo(e['checked']);
}
Step 13
Provide an implememntation to the Security Service.
changeRoleTo(isAdmin: boolean) {
if (isAdmin) {
this.currentRoles = [Role.ADMIN];
} else {
this.currentRoles = [Role.ANONYMOUS];
}
}