Create a reusable service to manage the hero data calls.
As the Tour of Heroes app evolves, you'll add more components that need access to hero data.
Instead of copying and pasting the same code over and over, you'll create a single reusable data service and inject it into the components that need it. Using a separate service keeps components lean and focused on supporting the view, and makes it easy to unit-test components with a mock service.
Because data services are invariably asynchronous, you'll finish the page with a Promise-based version of the data service.
When you're done with this page, the app should look like this live example.
Before continuing with the Tour of Heroes, verify that you have the following structure. If not, go back to the previous pages.
angular-tour-of-heroes src app app.component.ts app.module.ts hero.ts hero-detail.component.ts main.ts index.html styles.css systemjs.config.js tsconfig.json node_modules ... package.json
Enter the following command in the terminal window:
npm start
This command runs the TypeScript compiler in "watch mode", recompiling automatically when the code changes. The command simultaneously launches the app in a browser and refreshes the browser when the code changes.
You can keep building the Tour of Heroes without pausing to recompile or refresh the browser.
The stakeholders want to show the heroes in various ways on different pages. Users can already select a hero from a list. Soon you'll add a dashboard with the top performing heroes and create a separate view for editing hero details. All three views need hero data.
At the moment, the AppComponent
defines mock heroes for display. However, defining heroes is not the component's job, and you can't easily share the list of heroes with other components and views. In this page, you'll move the hero data acquisition business to a single service that provides the data and share that service with all components that need the data.
Create a file in the app
folder called hero.service.ts
.
The naming convention for service files is the service name in lowercase followed by
.service
. For a multi-word service name, use lower dash-case. For example, the filename forSpecialSuperHeroService
isspecial-super-hero.service.ts
.
Name the class HeroService
and export it for others to import.
import { Injectable } from '@angular/core'; @Injectable() export class HeroService { }
Notice that you imported the Angular Injectable
function and applied that function as an @Injectable()
decorator.
Don't forget the parentheses. Omitting them leads to an error that's difficult to diagnose.
The @Injectable()
decorator tells TypeScript to emit metadata about the service. The metadata specifies that Angular may need to inject other dependencies into this service.
Although the HeroService
doesn't have any dependencies at the moment, applying the @Injectable()
decorator from the start ensures consistency and future-proofing.
Add a getHeroes()
method stub.
@Injectable() export class HeroService { getHeroes(): void {} // stub }
The HeroService
could get Hero
data from anywhere—a web service, local storage, or a mock data source. Removing data access from the component means you can change your mind about the implementation anytime, without touching the components that need hero data.
Cut the HEROES
array from app.component.ts
and paste it to a new file in the app
folder named mock-heroes.ts
. Additionally, copy the import {Hero} ...
statement because the heroes array uses the Hero
class.
import { Hero } from './hero'; export const HEROES: Hero[] = [ {id: 11, name: 'Mr. Nice'}, {id: 12, name: 'Narco'}, {id: 13, name: 'Bombasto'}, {id: 14, name: 'Celeritas'}, {id: 15, name: 'Magneta'}, {id: 16, name: 'RubberMan'}, {id: 17, name: 'Dynama'}, {id: 18, name: 'Dr IQ'}, {id: 19, name: 'Magma'}, {id: 20, name: 'Tornado'} ];
The HEROES
constant is exported so it can be imported elsewhere, such as the HeroService
.
In app.component.ts
, where you cut the HEROES
array, add an uninitialized heroes
property:
heroes: Hero[];
Back in the HeroService
, import the mock HEROES
and return it from the getHeroes()
method. The HeroService
looks like this:
import { Injectable } from '@angular/core'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; @Injectable() export class HeroService { getHeroes(): Hero[] { return HEROES; } }
You're ready to use the HeroService
in other components, starting with AppComponent
.
Import the HeroService
so that you can reference it in the code.
import { HeroService } from './hero.service';
How should the AppComponent
acquire a runtime concrete HeroService
instance?
You could create a new instance of the HeroService
with new
like this:
heroService = new HeroService(); // don't do this
However, this option isn't ideal for the following reasons:
HeroService
. If you change the HeroService
constructor, you must find and update every place you created the service. Patching code in multiple places is error prone and adds to the test burden.new
. What if the service caches heroes and shares that cache with others? You couldn't do that.AppComponent
locked into a specific implementation of the HeroService
, switching implementations for different scenarios, such as operating offline or using different mocked versions for testing, would be difficult.Instead of using the new line, you'll add two lines.
providers
metadata.Add the constructor:
constructor(private heroService: HeroService) { }
The constructor itself does nothing. The parameter simultaneously defines a private heroService
property and identifies it as a HeroService
injection site.
Now Angular knows to supply an instance of the HeroService
when it creates an AppComponent
.
Read more about dependency injection in the Dependency Injection page.
The injector doesn't know yet how to create a HeroService
. If you ran the code now, Angular would fail with this error:
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
To teach the injector how to make a HeroService
, add the following providers
array property to the bottom of the component metadata in the @Component
call.
providers: [HeroService]
The providers
array tells Angular to create a fresh instance of the HeroService
when it creates an AppComponent
. The AppComponent
, as well as its child components, can use that service to get hero data.
The service is in a heroService
private variable.
You could call the service and get the data in one line.
this.heroes = this.heroService.getHeroes();
You don't really need a dedicated method to wrap one line. Write it anyway:
getHeroes(): void { this.heroes = this.heroService.getHeroes(); }
AppComponent
should fetch and display hero data with no issues.
You might be tempted to call the getHeroes()
method in a constructor, but a constructor should not contain complex logic, especially a constructor that calls a server, such as as a data access method. The constructor is for simple initializations, like wiring constructor parameters to properties.
To have Angular call getHeroes()
, you can implement the Angular ngOnInit lifecycle hook. Angular offers interfaces for tapping into critical moments in the component lifecycle: at creation, after each change, and at its eventual destruction.
Each interface has a single method. When the component implements that method, Angular calls it at the appropriate time.
Read more about lifecycle hooks in the Lifecycle Hooks page.
Here's the essential outline for the OnInit
interface (don't copy this into your code):
import { OnInit } from '@angular/core'; export class AppComponent implements OnInit { ngOnInit(): void { } }
Add the implementation for the OnInit
interface to your export statement:
export class AppComponent implements OnInit {}
Write an ngOnInit
method with the initialization logic inside. Angular will call it at the right time. In this case, initialize by calling getHeroes()
.
ngOnInit(): void { this.getHeroes(); }
The app should run as expected, showing a list of heroes and a hero detail view when you click on a hero name.
The HeroService
returns a list of mock heroes immediately; its getHeroes()
signature is synchronous.
this.heroes = this.heroService.getHeroes();
Eventually, the hero data will come from a remote server. When using a remote server, users don't have to wait for the server to respond; additionally, you aren't able to block the UI during the wait.
To coordinate the view with the response, you can use Promises, which is an asynchronous technique that changes the signature of the getHeroes()
method.
A Promise essentially promises to call back when the results are ready. You ask an asynchronous service to do some work and give it a callback function. The service does that work and eventually calls the function with the results or an error.
This is a simplified explanation. Read more about ES2015 Promises in the Promises for asynchronous programming page of Exploring ES6.
Update the HeroService
with this Promise-returning getHeroes()
method:
getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); }
You're still mocking the data. You're simulating the behavior of an ultra-fast, zero-latency server, by returning an immediately resolved Promise with the mock heroes as the result.
As a result of the change to HeroService
, this.heroes
is now set to a Promise rather than an array of heroes.
getHeroes(): void { this.heroes = this.heroService.getHeroes(); }
You have to change the implementation to act on the Promise when it resolves. When the Promise resolves successfully, you'll have heroes to display.
Pass the callback function as an argument to the Promise's then()
method:
getHeroes(): void { this.heroService.getHeroes().then(heroes => this.heroes = heroes); }
As described in Arrow functions, the ES2015 arrow function in the callback is more succinct than the equivalent function expression and gracefully handles
this
.
The callback sets the component's heroes
property to the array of heroes returned by the service.
The app is still running, showing a list of heroes, and responding to a name selection with a detail view.
At the end of this page, Appendix: take it slow describes what the app might be like with a poor connection.
Verify that you have the following structure after all of your refactoring:
angular-tour-of-heroes src app app.component.ts app.module.ts hero.ts hero-detail.component.ts hero.service.ts mock-heroes.ts main.ts index.html styles.css systemjs.config.js tsconfig.json node_modules ... package.json
Here are the code files discussed in this page.
import { Injectable } from '@angular/core'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; @Injectable() export class HeroService { getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); } }
import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <hero-detail [hero]="selectedHero"></hero-detail> `, styles: [` .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } `], providers: [HeroService] }) export class AppComponent implements OnInit { title = 'Tour of Heroes'; heroes: Hero[]; selectedHero: Hero; constructor(private heroService: HeroService) { } getHeroes(): void { this.heroService.getHeroes().then(heroes => this.heroes = heroes); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } }
import { Hero } from './hero'; export const HEROES: Hero[] = [ {id: 11, name: 'Mr. Nice'}, {id: 12, name: 'Narco'}, {id: 13, name: 'Bombasto'}, {id: 14, name: 'Celeritas'}, {id: 15, name: 'Magneta'}, {id: 16, name: 'RubberMan'}, {id: 17, name: 'Dynama'}, {id: 18, name: 'Dr IQ'}, {id: 19, name: 'Magma'}, {id: 20, name: 'Tornado'} ];
Here's what you achieved in this page:
ngOnInit
lifecycle hook to get the hero data when the AppComponent
activates.HeroService
as a provider for the AppComponent
.Your app should look like this live example.
The Tour of Heroes has become more reusable using shared components and services. The next goal is to create a dashboard, add menu links that route between the views, and format data in a template. As the app evolves, you'll discover how to design it to make it easier to grow and maintain.
Read about the Angular component router and navigation among the views in the next tutorial page.
To simulate a slow connection, import the Hero
symbol and add the following getHeroesSlowly()
method to the HeroService
.
getHeroesSlowly(): Promise<Hero[]> { return new Promise(resolve => { // Simulate server latency with 2 second delay setTimeout(() => resolve(this.getHeroes()), 2000); }); }
Like getHeroes()
, it also returns a Promise. But this Promise waits two seconds before resolving the Promise with mock heroes.
Back in the AppComponent
, replace getHeroes()
with getHeroesSlowly()
and see how the app behaves.
Next Step
Routing
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/tutorial/toh-pt4.html