439 lines
16 KiB
Markdown
439 lines
16 KiB
Markdown
# Services
|
|
|
|
The Tour of Heroes `HeroesComponent` is currently getting and displaying fake data.
|
|
|
|
After the refactoring in this tutorial, `HeroesComponent` will be lean and focused on supporting the view.
|
|
It will also be easier to unit-test with a mock service.
|
|
|
|
## Why services
|
|
|
|
Components shouldn't fetch or save data directly and they certainly shouldn't knowingly present fake data.
|
|
They should focus on presenting data and delegate data access to a service.
|
|
|
|
In this tutorial, you'll create a `HeroService` that all application classes can use to get heroes.
|
|
Instead of creating that service with `new`,
|
|
you'll rely on Angular [*dependency injection*](guide/dependency-injection)
|
|
to inject it into the `HeroesComponent` constructor.
|
|
|
|
Services are a great way to share information among classes that _don't know each other_.
|
|
You'll create a `MessageService` and inject it in two places:
|
|
|
|
1. in `HeroService` which uses the service to send a message.
|
|
2. in `MessagesComponent` which displays that message.
|
|
|
|
|
|
## Create the _HeroService_
|
|
|
|
Using the Angular CLI, create a service called `hero`.
|
|
|
|
<code-example language="sh" class="code-shell">
|
|
ng generate service hero
|
|
</code-example>
|
|
|
|
The command generates skeleton `HeroService` class in `src/app/hero.service.ts`
|
|
The `HeroService` class should look like the following example.
|
|
|
|
<code-example path="toh-pt4/src/app/hero.service.1.ts" region="new"
|
|
title="src/app/hero.service.ts (new service)" linenums="false">
|
|
</code-example>
|
|
|
|
### _@Injectable()_ services
|
|
|
|
Notice that the new service imports the Angular `Injectable` symbol and annotates
|
|
the class with the `@Injectable()` decorator. This marks the class as one that participates in the _dependency injection system_. The `HeroService` class is going to provide an injectable service, and it can also have its own injected dependencies.
|
|
It doesn't have any dependencies yet, but [it will soon](#inject-message-service).
|
|
|
|
The `@Injectable()` decorator accepts a metadata object for the service, the same way the `@Component()` decorator did for your component classes.
|
|
|
|
### Get hero data
|
|
|
|
The `HeroService` could get hero data from anywhere—a web service, local storage, or a mock data source.
|
|
|
|
Removing data access from components means you can change your mind about the implementation anytime, without touching any components.
|
|
They don't know how the service works.
|
|
|
|
The implementation in _this_ tutorial will continue to deliver _mock heroes_.
|
|
|
|
Import the `Hero` and `HEROES`.
|
|
|
|
<code-example path="toh-pt4/src/app/hero.service.ts" region="import-heroes">
|
|
</code-example>
|
|
|
|
Add a `getHeroes` method to return the _mock heroes_.
|
|
|
|
<code-example path="toh-pt4/src/app/hero.service.1.ts" region="getHeroes">
|
|
</code-example>
|
|
|
|
{@a provide}
|
|
## Provide the `HeroService`
|
|
|
|
You must make the `HeroService` available to the dependency injection system
|
|
before Angular can _inject_ it into the `HeroesComponent`,
|
|
as you will do [below](#inject). You do this by registering a _provider_. A provider is something that can create or deliver a service; in this case, it instantiates the `HeroService` class to provide the service.
|
|
|
|
Now, you need to make sure that the `HeroService` is registered as the provider of this service.
|
|
You are registering it with an _injector_, which is the object that is responsible for choosing and injecting the provider where it is required.
|
|
|
|
By default, the Angular CLI command `ng generate service` registers a provider with the _root injector_ for your service by including provider metadata in the `@Injectable` decorator.
|
|
|
|
If you look at the `@Injectable()` statement right before the `HeroService` class definition, you can see that the `providedIn` metadata value is 'root':
|
|
|
|
```
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
```
|
|
|
|
When you provide the service at the root level, Angular creates a single, shared instance of `HeroService` and injects into any class that asks for it.
|
|
Registering the provider in the `@Injectable` metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
To learn more about providers, see the [Providers section](guide/providers).
|
|
To learn more about injectors, see the [Dependency Injection guide](guide/dependency-injection).
|
|
|
|
</div>
|
|
|
|
The `HeroService` is now ready to plug into the `HeroesComponent`.
|
|
|
|
<div class="alert is-important">
|
|
|
|
This is a interim code sample that will allow you to provide and use the `HeroService`. At this point, the code will differ from the `HeroService` in the ["final code review"](#final-code-review).
|
|
|
|
</div>
|
|
|
|
|
|
## Update `HeroesComponent`
|
|
|
|
Open the `HeroesComponent` class file.
|
|
|
|
Delete the `HEROES` import, because you won't need that anymore.
|
|
Import the `HeroService` instead.
|
|
|
|
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" title="src/app/heroes/heroes.component.ts (import HeroService)" region="hero-service-import">
|
|
</code-example>
|
|
|
|
Replace the definition of the `heroes` property with a simple declaration.
|
|
|
|
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" region="heroes">
|
|
</code-example>
|
|
|
|
{@a inject}
|
|
|
|
### Inject the `HeroService`
|
|
|
|
Add a private `heroService` parameter of type `HeroService` to the constructor.
|
|
|
|
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" region="ctor">
|
|
</code-example>
|
|
|
|
The parameter simultaneously defines a private `heroService` property and identifies it as a `HeroService` injection site.
|
|
|
|
When Angular creates a `HeroesComponent`, the [Dependency Injection](guide/dependency-injection) system
|
|
sets the `heroService` parameter to the singleton instance of `HeroService`.
|
|
|
|
### Add _getHeroes()_
|
|
|
|
Create a function to retrieve the heroes from the service.
|
|
|
|
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" region="getHeroes">
|
|
</code-example>
|
|
|
|
{@a oninit}
|
|
|
|
### Call it in `ngOnInit`
|
|
|
|
While you could call `getHeroes()` in the constructor, that's not the best practice.
|
|
|
|
Reserve the constructor for simple initialization such as wiring constructor parameters to properties.
|
|
The constructor shouldn't _do anything_.
|
|
It certainly shouldn't call a function that makes HTTP requests to a remote server as a _real_ data service would.
|
|
|
|
Instead, call `getHeroes()` inside the [*ngOnInit lifecycle hook*](guide/lifecycle-hooks) and
|
|
let Angular call `ngOnInit` at an appropriate time _after_ constructing a `HeroesComponent` instance.
|
|
|
|
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" region="ng-on-init">
|
|
</code-example>
|
|
|
|
### See it run
|
|
|
|
After the browser refreshes, the app should run as before,
|
|
showing a list of heroes and a hero detail view when you click on a hero name.
|
|
|
|
## Observable data
|
|
|
|
The `HeroService.getHeroes()` method has a _synchronous signature_,
|
|
which implies that the `HeroService` can fetch heroes synchronously.
|
|
The `HeroesComponent` consumes the `getHeroes()` result
|
|
as if heroes could be fetched synchronously.
|
|
|
|
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" region="get-heroes">
|
|
</code-example>
|
|
|
|
This will not work in a real app.
|
|
You're getting away with it now because the service currently returns _mock heroes_.
|
|
But soon the app will fetch heroes from a remote server,
|
|
which is an inherently _asynchronous_ operation.
|
|
|
|
The `HeroService` must wait for the server to respond,
|
|
`getHeroes()` cannot return immediately with hero data,
|
|
and the browser will not block while the service waits.
|
|
|
|
`HeroService.getHeroes()` must have an _asynchronous signature_ of some kind.
|
|
|
|
It can take a callback. It could return a `Promise`. It could return an `Observable`.
|
|
|
|
In this tutorial, `HeroService.getHeroes()` will return an `Observable`
|
|
in part because it will eventually use the Angular `HttpClient.get` method to fetch the heroes
|
|
and [`HttpClient.get()` returns an `Observable`](guide/http).
|
|
|
|
### Observable _HeroService_
|
|
|
|
`Observable` is one of the key classes in the [RxJS library](http://reactivex.io/rxjs/).
|
|
|
|
In a [later tutorial on HTTP](tutorial/toh-pt6), you'll learn that Angular's `HttpClient` methods return RxJS `Observable`s.
|
|
In this tutorial, you'll simulate getting data from the server with the RxJS `of()` function.
|
|
|
|
Open the `HeroService` file and import the `Observable` and `of` symbols from RxJS.
|
|
|
|
<code-example path="toh-pt4/src/app/hero.service.ts"
|
|
title="src/app/hero.service.ts (Observable imports)" region="import-observable">
|
|
</code-example>
|
|
|
|
Replace the `getHeroes` method with this one.
|
|
|
|
<code-example path="toh-pt4/src/app/hero.service.ts" region="getHeroes-1"></code-example>
|
|
|
|
`of(HEROES)` returns an `Observable<Hero[]>` that emits _a single value_, the array of mock heroes.
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
In the [HTTP tutorial](tutorial/toh-pt6), you'll call `HttpClient.get<Hero[]>()` which also returns an `Observable<Hero[]>` that emits _a single value_, an array of heroes from the body of the HTTP response.
|
|
|
|
</div>
|
|
|
|
### Subscribe in _HeroesComponent_
|
|
|
|
The `HeroService.getHeroes` method used to return a `Hero[]`.
|
|
Now it returns an `Observable<Hero[]>`.
|
|
|
|
You'll have to adjust to that difference in `HeroesComponent`.
|
|
|
|
Find the `getHeroes` method and replace it with the following code
|
|
(shown side-by-side with the previous version for comparison)
|
|
|
|
<code-tabs>
|
|
|
|
<code-pane title="heroes.component.ts (Observable)"
|
|
path="toh-pt4/src/app/heroes/heroes.component.ts" region="getHeroes">
|
|
</code-pane>
|
|
|
|
<code-pane title="heroes.component.ts (Original)"
|
|
path="toh-pt4/src/app/heroes/heroes.component.1.ts" region="getHeroes">
|
|
</code-pane>
|
|
|
|
</code-tabs>
|
|
|
|
`Observable.subscribe()` is the critical difference.
|
|
|
|
The previous version assigns an array of heroes to the component's `heroes` property.
|
|
The assignment occurs _synchronously_, as if the server could return heroes instantly
|
|
or the browser could freeze the UI while it waited for the server's response.
|
|
|
|
That _won't work_ when the `HeroService` is actually making requests of a remote server.
|
|
|
|
The new version waits for the `Observable` to emit the array of heroes—
|
|
which could happen now or several minutes from now.
|
|
Then `subscribe` passes the emitted array to the callback,
|
|
which sets the component's `heroes` property.
|
|
|
|
This asynchronous approach _will work_ when
|
|
the `HeroService` requests heroes from the server.
|
|
|
|
## Show messages
|
|
|
|
In this section you will
|
|
|
|
* add a `MessagesComponent` that displays app messages at the bottom of the screen.
|
|
* create an injectable, app-wide `MessageService` for sending messages to be displayed
|
|
* inject `MessageService` into the `HeroService`
|
|
* display a message when `HeroService` fetches heroes successfully.
|
|
|
|
### Create _MessagesComponent_
|
|
|
|
Use the CLI to create the `MessagesComponent`.
|
|
|
|
<code-example language="sh" class="code-shell">
|
|
ng generate component messages
|
|
</code-example>
|
|
|
|
The CLI creates the component files in the `src/app/messages` folder and declare `MessagesComponent` in `AppModule`.
|
|
|
|
Modify the `AppComponent` template to display the generated `MessagesComponent`
|
|
|
|
<code-example
|
|
title = "/src/app/app.component.html"
|
|
path="toh-pt4/src/app/app.component.html">
|
|
</code-example>
|
|
|
|
You should see the default paragraph from `MessagesComponent` at the bottom of the page.
|
|
|
|
### Create the _MessageService_
|
|
|
|
Use the CLI to create the `MessageService` in `src/app`.
|
|
|
|
<code-example language="sh" class="code-shell">
|
|
ng generate service message
|
|
</code-example>
|
|
|
|
Open `MessageService` and replace its contents with the following.
|
|
|
|
<code-example
|
|
title = "/src/app/message.service.ts"
|
|
path="toh-pt4/src/app/message.service.ts">
|
|
</code-example>
|
|
|
|
The service exposes its cache of `messages` and two methods: one to `add()` a message to the cache and another to `clear()` the cache.
|
|
|
|
{@a inject-message-service}
|
|
### Inject it into the `HeroService`
|
|
|
|
Re-open the `HeroService` and import the `MessageService`.
|
|
|
|
<code-example
|
|
title = "/src/app/hero.service.ts (import MessageService)"
|
|
path="toh-pt4/src/app/hero.service.ts" region="import-message-service">
|
|
</code-example>
|
|
|
|
Modify the constructor with a parameter that declares a private `messageService` property.
|
|
Angular will inject the singleton `MessageService` into that property
|
|
when it creates the `HeroService`.
|
|
|
|
<code-example
|
|
path="toh-pt4/src/app/hero.service.ts" region="ctor">
|
|
</code-example>
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
This is a typical "*service-in-service*" scenario:
|
|
you inject the `MessageService` into the `HeroService` which is injected into the `HeroesComponent`.
|
|
|
|
</div>
|
|
|
|
### Send a message from `HeroService`
|
|
|
|
Modify the `getHeroes` method to send a message when the heroes are fetched.
|
|
|
|
<code-example path="toh-pt4/src/app/hero.service.ts" region="getHeroes">
|
|
</code-example>
|
|
|
|
### Display the message from `HeroService`
|
|
|
|
The `MessagesComponent` should display all messages,
|
|
including the message sent by the `HeroService` when it fetches heroes.
|
|
|
|
Open `MessagesComponent` and import the `MessageService`.
|
|
|
|
<code-example
|
|
title = "/src/app/messages/messages.component.ts (import MessageService)"
|
|
path="toh-pt4/src/app/messages/messages.component.ts" region="import-message-service">
|
|
</code-example>
|
|
|
|
Modify the constructor with a parameter that declares a **public** `messageService` property.
|
|
Angular will inject the singleton `MessageService` into that property
|
|
when it creates the `MessagesComponent`.
|
|
|
|
<code-example
|
|
path="toh-pt4/src/app/messages/messages.component.ts" region="ctor">
|
|
</code-example>
|
|
|
|
The `messageService` property **must be public** because you're about to bind to it in the template.
|
|
|
|
<div class="alert is-important">
|
|
|
|
Angular only binds to _public_ component properties.
|
|
|
|
</div>
|
|
|
|
### Bind to the _MessageService_
|
|
|
|
Replace the CLI-generated `MessagesComponent` template with the following.
|
|
|
|
<code-example
|
|
title = "src/app/messages/messages.component.html"
|
|
path="toh-pt4/src/app/messages/messages.component.html">
|
|
</code-example>
|
|
|
|
This template binds directly to the component's `messageService`.
|
|
|
|
* The `*ngIf` only displays the messages area if there are messages to show.
|
|
|
|
|
|
* An `*ngFor` presents the list of messages in repeated `<div>` elements.
|
|
|
|
|
|
* An Angular [event binding](guide/template-syntax#event-binding) binds the button's click event
|
|
to `MessageService.clear()`.
|
|
|
|
The messages will look better when you add the private CSS styles to `messages.component.css`
|
|
as listed in one of the ["final code review"](#final-code-review) tabs below.
|
|
|
|
The browser refreshes and the page displays the list of heroes.
|
|
Scroll to the bottom to see the message from the `HeroService` in the message area.
|
|
Click the "clear" button and the message area disappears.
|
|
|
|
{@a final-code-review}
|
|
|
|
## Final code review
|
|
|
|
Here are the code files discussed on this page and your app should look like this <live-example></live-example>.
|
|
|
|
<code-tabs>
|
|
|
|
<code-pane title="src/app/hero.service.ts"
|
|
path="toh-pt4/src/app/hero.service.ts">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/message.service.ts"
|
|
path="toh-pt4/src/app/message.service.ts">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/heroes/heroes.component.ts"
|
|
path="toh-pt4/src/app/heroes/heroes.component.ts">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/messages/messages.component.ts"
|
|
path="toh-pt4/src/app/messages/messages.component.ts">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/messages/messages.component.html"
|
|
path="toh-pt4/src/app/messages/messages.component.html">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/messages/messages.component.css"
|
|
path="toh-pt4/src/app/messages/messages.component.css">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/app.module.ts"
|
|
path="toh-pt4/src/app/app.module.ts">
|
|
</code-pane>
|
|
|
|
<code-pane title="src/app/app.component.html"
|
|
path="toh-pt4/src/app/app.component.html">
|
|
</code-pane>
|
|
|
|
</code-tabs>
|
|
|
|
## Summary
|
|
|
|
* You refactored data access to the `HeroService` class.
|
|
* You registered the `HeroService` as the _provider_ of its service at the root level so that it can be injected anywhere in the app.
|
|
* You used [Angular Dependency Injection](guide/dependency-injection) to inject it into a component.
|
|
* You gave the `HeroService` _get data_ method an asynchronous signature.
|
|
* You discovered `Observable` and the RxJS _Observable_ library.
|
|
* You used RxJS `of()` to return an observable of mock heroes (`Observable<Hero[]>`).
|
|
* The component's `ngOnInit` lifecycle hook calls the `HeroService` method, not the constructor.
|
|
* You created a `MessageService` for loosely-coupled communication between classes.
|
|
* The `HeroService` injected into a component is created with another injected service,
|
|
`MessageService`.
|