When you have an Angular view that combines static elements, such as a navigation bar and footer, with dynamic elements that depend on data from an HTTP request, the browser will initially render only the static elements. Since we don’t want to display a blank dynamic component while waiting for the HTTP request, we can pre-fetch the data prior to activating the route. In Angular 2, you accomplish this using the Resolve
guard.
This tutorial will use the Tour of Heroes project from the angular.io website as an example. You can download the complete project from https://github.com/johnpapa/angular2-tour-of-heroes.
The first thing we need to do is create a service to fetch the data we want to receive. This service gets the Hero
id parameter from the route and uses the HeroService
to find its associated Hero
data. If we can’t find any data that matches, we’ll send the user back to the dashboard view and cancel navigation to Hero
details.
// hero-detail.resolve.service.ts
import { Injectable } from '@angular/core';
import {
Router, Resolve,
ActivatedRouteSnapshot
} from '@angular/router';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Injectable()
export class HeroDetailResolve implements Resolve {
constructor(private heroService: HeroService, private router: Router) { }
resolve(route: ActivatedRouteSnapshot): Promise | boolean {
let id = +route.params['id'];
return this.heroService.getHero(id).then(hero => {
if (hero) {
return hero;
} else { // id not found
this.router.navigate(['/dashboard']);
return false;
}
});
}
}
Now that we have a resolve service created, we can add the resolve configuration to app-routing.module.ts
. To use the resolve service, we need to import it and also add a providers
section. This makes our service available to the Router
during navigation.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroDetailResolve } from './hero-detail-resolve.service';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{
path: 'dashboard',
component: DashboardComponent,
},
{
path: 'detail/:id',
component: HeroDetailComponent,
resolve: {
hero: HeroDetailResolve
},
},
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ],
providers: [
HeroDetailResolve
]
})
export class AppRoutingModule {}
Because we fetch the Hero
data before the route activates, we no longer have to perform this action in HeroDetailComponent
. Instead, we’ll grab the Hero
data from the ActivatedRoute
data and set it to the component’s Hero
property.
// hero-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
moduleId: module.id,
selector: 'my-hero-detail',
templateUrl: 'hero-detail.component.html',
styleUrls: ['hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private location: Location
) { }
ngOnInit(): void {
this.route.data
.subscribe((data: { hero: Hero }) => {
this.hero = data.hero;
});
}
save(): void {
this.heroService.update(this.hero)
.then(() => this.goBack());
}
goBack(): void {
this.location.back();
}
}
Your view should now wait to render until the Hero
detail data is returned and bound to the HeroDetailComponent
.