angular-cn/public/docs/ts/latest/tutorial/toh-pt4.jade

250 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

include ../../../../_includes/_util-fns
:marked
# Services
Our app is growing.
Use cases are flowing in for reusing components, passing data to components, sharing the hero data, and preparing to retrieve the data asynchronously via a promise.
.l-main-section
:marked
## Reviewing Where We Left Off
Before we continue with our Tour of Heroes, lets verify we have the following structure. If not, well need to go back and follow the previous chapters.
code-example(format="." language="bash").
npm start
.filetree
.file angular2-tour-of-heroes
.children
.file node_modules
.file app
.children
.file app.component.ts
.file boot.ts
.file index.html
.file package.json
.file tsconfig.json
:marked
### Keep the app transpiling and running
We want to start the TypeScript compiler, have it watch for changes, and start our server. We'll do this by typing
code-example(format="." language="bash").
npm run go
:marked
This will keep the application running while we continue to build the Tour of Heroes.
## Creating a Hero Service
Our stakeholders have shared their larger vision for our app. They tell us they want to show the heroes in various ways in different pages. We have a way to select a hero from a list, but we will also need a dashboard with the top heroes and a separate view for editing hero details.
All of these views need hero data. Our `AppComponent` defines and uses a list of heroes, but it is not ideal to create our AppComponent when just the heroes data elsewhere. Fortunately we can create a shared service that will provide the heroes.
### Creating the HeroService
Were going to create a service that can be used by any component that wants hero data. Lets start by creating a file and naming it `hero.service.ts`. We name the class `HeroService` and export it, so our components can import it.
```
export class HeroService { }
```
#### The getHeroes Method
We create a method named `getHeroes` in our `HeroService`. It will return an array of `Hero` objects, so lets import the `Hero` class and define our method.
```
import { Hero } from './hero';
export class HeroService {
getHeroes() : Hero[] {
}
}
```
#### Mocking the Heroes
Our `HeroService` shouldnt be defining the hero data. Instead, the service should handle retrieving the data from another source. That source could be a mock data, a web service, or even local storage. We will design our `HeroService` to get the data from any of these sources and not affect the calling component. This will make it more reusable and more testable.
Once.
We have a list of heroes in `AppComponent`. We will move it to a new file named `mock-heroes.ts` and export the list.
+makeExample('toh-3/ts-snippets/app/mock-heroes.ts', 'mocking-heroes')
:marked
### Returning the Mocked Heroes
Our `HeroService` needs to get the list of heroes, so lets import the mocked heroes module. Then well return the HEROES array.
```
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
export class HeroService {
getHeroes() {
return HEROES;
}
}
```
TypeScript can implicitly determine that that return type is `Hero[]` since the return value is of that same type. This allows use to remove the explicit return type from the `getHeroes` method.
### Injecting the Hero Service
Weve set ourselves up so we can use the `HeroService` from other components. Lets import the `HeroService` in our `AppComponent`.
```
import {HeroService} from './hero.service';
```
Importing the service allows us to reference it, but we need to make sure the `HeroService` dependency is instantiated when our component needs it. We inject the `HeroService` into our `AppComponent`s constructor.
```
constructor(private _heroService: HeroService) { }
```
We just injected our dependency into the component, thus we performed Dependency Injection.
.l-sub-section
:marked
Learn more about Dependency Injection in the [Dependency Injection](../guide/dependency-injection.html) chapter.
:marked
We made our instance of the injected `HeroService` be a private property on our `AppComponent` class. As a convention we prefixed the private property with an underscore.
Since we are not defining the heroes in the `AppComponent` any longer, lets refactor the `hero` property declaration to be an uninitialized array of `Hero`.
```
public heroes: Hero[];
```
### The OnInit Lifecycle Hook
When our `AppComponent` is created we want it to get the list of heroes. We need to know when the component is initialized and activated, so well use the `OnInit` lifecycle event to tell us this.
Lets import Angulars `OnInit` interface, implement it on our `AppComponent` and define its required `onInit` method. First we add the `OnInit` interface to the import statement.
```
import {bootstrap, Component, CORE_DIRECTIVES, FORM_DIRECTIVES, OnInit} from 'angular2/angular2';
```
Now we implement the interface.
```
class AppComponent implements OnInit {
```
Then we define the `onInit` method and get our heroes from our `HeroService`.
```
onInit() {
this.heroes = this._heroService.getHeroes();
}
```
.l-sub-section
:marked
Learn more about lifecycle hooks in the [Lifecycle Hooks](../guide/lifecycle-hooks.html) chapter.
:marked
Why not use the constructor to get the heroes? When we test our application we want an opportunity to create the class without any state being set. This will make it easier to test and reduce external factors, such as calling a service in the constructor. Therefore the constructor is best suited to help us inject dependencies and initialize variables. We need a place to get our heroes right after our class is constructed but before the view is rendered. The OnInit lifecycle hook gives us this opportunity.
<!-- TODO
.l-sub-section
:marked
Learn more about testing components in chapter [Testing Components]
:marked
-->
### Binding the Hero Service
When we view our app in the browser we see we have an error displayed in the developer console
code-example(format="." language="html").
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
:marked
We used Dependency Injection to tell our `AppComponent` that it should inject the `HeroService`. However we need to tell our app about the `HeroService` so it can provide it when needed. The way we do this is by declaring the `HeroService` as a binding when we bootstrap our app.
Lets pass a second argument to the `bootstrap` method to declare the `HeroService` as an application binding.
```
bootstrap(AppComponent, [HeroService]);
```
We can add other bindings here, as needed.
When we view our app in the browser the error is gone and our application runs as expected showing our list of heroes.
## Promises
Our `HeroService` synchronously returns a list of heroes. It operates synchronously because the list of heroes is mocked. What happens when we want to switch that to get the heroes from a web service? The web service call over http would happen asynchronously.
We dont yet call http, but we aspire to in later chapters. So how do we write our `HeroService` so that it wont require refactoring the consumers of `HeroService` later? We make our `HeroService`s `getHeroes` method return a promise to provide the heroes.
The key is that our components wont know how the data is being retrieved. We can return mock heroes or heroes from http, and the component will call the services method the same way.
### Returning a Promise
Lets refactor the `getHeroes` method in `HeroService` to return the heroes in a promise.
```
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
}
```
The `Promise` is immediately resolving and passing the the hero data back in the promise.
### Acting on a Promise
Lets refactor the `getHeroes` method in `HeroService` to return the heroes in a promise. First, we create a new method in the `AppComponent` to get the heroes and named it `getHeroes`.
When we call our heroes we start by resetting the `selectedHero` and `heroes` properties.
```
getHeroes() {
this.selectedHero = undefined;
this.heroes = [];
}
```
The `getHeroes` method in `HeroService` returns a promise. So we cannot simply set the return value to `this.heroes`. The method returns a promise and the `heroes` property expects an array of `Hero`. What do we do?
We define a `then` to handle the response from the promise when it resolves. We will set the heroes inside of the `then`.
```
this._heroService.getHeroes()
.then(heroes => this.heroes = heroes);
```
The `then` accepts a function, in this case a lambda that passes in the heroes and sets them to the `heroes` property on `AppComponent`.
We need to return a value for the heroes from the method so a caller can get the heroes when they are ready. Lets return our components `heroes` property, which we first reset to an empty array.
```
return this.heroes;
```
When we put this all together we see we are setting our heroes to an empty array. Then we call the service and get a promise. Finally we return the reference to our `heroes` property, which has the empty array.
```
getHeroes() {
this.selectedHero = undefined;
this.heroes = [];
this._heroService.getHeroes()
.then(heroes => this.heroes = heroes);
return this.heroes;
}
```
So how do the heroes get populated? When the promise resolves, the `heroes` are updated to include the response from the promise.
Finally we call the method we just created in our `onInit` method.
```
onInit() {
this.heroes = this.getHeroes();
}
```
When we view our app in the browser we can see the heroes are displayed.
We are using mock data right now, but we aspire to call a web service over http asynchronously in the future. When we do refactor to use http, the beauty of the promise we created here is that our component wont have to change at all!
### Reviewing the App Structure
Lets verify that we have the following structure after all of our good refactoring in this chapter:
.filetree
.file angular2-tour-of-heroes
.children
.file node_modules
.file app
.children
.file app.component.ts
.file boot.ts
.file hero.ts
.file hero-detail.component.ts
.file hero.service.ts
.file mock-heroes.ts
.file index.html
.file package.json
.file tsconfig.json
:marked
## Recap
### The Road Weve Travelled
Lets take stock in what weve built.
- We created a reusable component
- We learned how to make a component accept input
- We created a service class that can be shared by many components
- We created mock hero data and imported them into our service
- We designed our service to return a promise and our component to get our data from the promise
### The Road Ahead
. . . Well learn more about all of these in the next chapter.
Our Tour of Heroes has become more reusable using shared components and services.
We want to create a dashboard, add menu links that route between the views, and format data in a template.
As our app evolves, well learn how to design it to make it easier to grow and maintain.
Well learn more about these tasks in the coming chapters.