Menu

Wait for Data Before Rendering Views in Angular 2

December 12, 2016 by Christopher Sherman

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.

JavaScript Angular