A tour of heroes is evolving, and we look forward to adding new components in the near future.
Several components need access to the data of the characters, and we do not want to copy and paste the same code over and over again. Instead, we will create one data service that can be reused later on and learn how to use it in the components that need it.
Refactoring data access to a separate service helps to not “inflate” the components and aims to support the display. In addition, the use of the mok service (mock - stub, allows you not to access the real data from the server) facilitates unit testing of the component (unit-tests).
Since the data received by the service will always be asynchronous, we will end this chapter with a version of the service based on the promise (promise).
Before continuing our tour of heroes, let's check that our project has the following structure. If this is not the case, you will need to return to the previous chapters.
angular2-tour-of-heroes app app.component.ts hero.ts hero-detail.component.ts main.ts node_modules ... typings ... index.html package.json styles.css systemjs.config.js tsconfig.json typings.json
Open a terminal / console window. Start the TypeScript compiler, which will monitor the changes, and the server by entering the command:
npm start
The application will start and will be updated automatically while we create the Tour of Heroes.
Our customers shared their vision of our application. They say they want to display heroes in different ways on different pages. We can already choose a hero from the list. Soon we will add a panel to display the best characters, and create a separate view for editing the details of the hero. All these representations need data heroes.
Currently, AppComponent
contains fictitious hero data. We have at least two objections. First, the definition of these heroes is not the responsibility of the component. Secondly, we cannot easily provide access to this data to other components and views.
We can refactor the logic of obtaining data about heroes into one service, which will provide access to data about heroes and give access to this service to those components that need heroes.
Create a file in the app
folder named hero.service.ts
.
We have adopted a convention in which the service is referred to as lowercase letters with the addition of.service
. If the service name consists of several words, the name “lower case with a dash” is used for naming. For example, theSpecialSuperHeroService
service will be defined by the file namespecial-super-hero.service.ts
.
HeroService
class HeroService
and mark it exported so that other classes can import it.
hero.service.ts (exported class)
import { Injectable } from '@angular/core'; @Injectable() export class HeroService { }
Notice that we imported the Angular Injectable
function and used the @Injectable()
decorator.
Do not forget the parentheses! Ignoring them leads to a difficult to diagnose error.
TypeScript sees the @Injectable()
decorator and makes available metadata about our service, the metadata that Angular may need to be embedded into other dependencies of this service.
HeroService
has no dependencies at the moment. Add a decorator anyway. This is an example of "best practice" - use the @Injectable()
decorator from the very beginning to show its purpose in perspective.
Add a getHeroes
stub getHeroes
.
hero.service.ts (getHeroes stub)
@Injectable() export class HeroService { getHeroes() { } }
Let's wait a little bit with implementation to make an important remark.
The consumer of our service does not know how the service receives data. Our HeroService
can get Hero
data from anywhere. He could get data from a web service or local storage device or from the wet method that returns some dummy data.
This is the beauty of making data access from a component. We can change implementations as often as we like and when it is needed, without affecting the components that need data about the characters.
We already have Hero
dummy data that is in the AppComponent
. But here they have no place. We will move this dummy data to a separate file.
Cut the HEROES
array from app.component.ts
and paste it into the new file in the app
folder called heroes.ts
. In addition, you need to copy the import {Hero} ...
because the array of heroes uses the Hero
class.
mock-heroes.ts (array of heroes)
import { Hero } from './hero'; export var 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"} ];
We export the HEROES
array as a constant, so we can import it elsewhere — for example, as our HeroService
.
In the meantime, in app.component.ts
, from where we cut the HEROES
array, we leave the heroes
non-initialized property:
app.component.ts (heroes property)
heroes: Hero[];
Returning to HeroService
we import our HEROES
stub and return it in the getHeroes
method. Our HeroService
service looks like this:
hero.service.ts
import { Injectable } from '@angular/core'; import { HEROES } from './mock-heroes'; @Injectable() export class HeroService { getHeroes() { return HEROES; } }
We are ready to use HeroService
in other components. Let's start with the AppComponent
.
At the beginning, as usual, we import what we want to use, that is, HeroService
.
app.component.ts (import HeroService)
import { HeroService } from './hero.service';
Import service allows you to refer to it in our code. How should an AppComponent
get a specific instance of HeroService
at runtime?
We could create a new instance of HeroService using new
, for example, like this ( no need to do that! ):
heroService = new HeroService(); // don't do this
This is a bad idea for several reasons.
HeroService
. If we ever change the HeroService
constructor, we need to find each place where the service is created and make edits. Such edits can lead to errors, and add additional burden on testing.AppComponent
we are tied to a specific implementation of HeroService
. It will be difficult to switch implementations for different scenarios. Can we work offline? Do we need different versions of fictitious implementations when testing? It is not simple.What if ... what if ... Hey, we have work to do!
We will do it. Avoiding these problems is so easy that there is no excuse for doing it wrong.
Two lines instead of the one using new :
providers
metadata to the component.Here is the constructor:
app.component.ts (constructor)
constructor(private heroService: HeroService) { }
By itself, the constructor does nothing. The parameter simultaneously determines the private property heroService
and identifies it as a place HeroService
implemented.
Angular will now know to transfer a copy of HeroService
when it creates a new AppComponent
.
Angular should get this copy from somewhere. This is the function of the Angular Dependency Injector (dependency "implementer" - further Injector). The injector has a container of previously created services. Either it finds and returns a previously existing HeroService
instance from the container, or it creates a new instance, adds it to the container, and returns it to Angular.
Read more about dependency injection in Dependency Injection .
The injector does not yet know how to create a HeroService
. If we run our code now, Angular will give an error:
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
We have to teach the injector how to create HeroService
by registering the provider HeroService
. To do this, you need to add the property array providers
at the bottom of the component metadata in the @Component
call.
app.component.ts (providing HeroService)
providers: [HeroService]
The providers
array indicates Angular to create a new HeroService
instance when it creates a new AppComponent
. AppComponent
can use this service to get heroes, and in addition, each descendant of a component in the tree of this component can receive it.
Services and component tree
Recall that AppComponent
creates an instance of HeroDetail
, based on the <my-hero-detail>
at the bottom of its template. HeroDetail
is a descendant of AppComponent
.
If HeroDetailComponent
component requires the HeroService
parent component, it will ask Angular to inject the service into its constructor, which will look the same as for AppComponent:
hero-detail.component.ts (constructor)
constructor(private heroService: HeroService) { }
The HeroDetailComponent
component HeroDetailComponent
not repeat the array of its parent's providers
! Think why. In the annex below, this will be discussed in more detail.
AppComponent
is a component of the highest level of our application. The application should have only one copy of this component and only one copy of HeroService
in our entire application.
We have a service in the private variable heroService
. Let's use her.
Let's pause to think. We can call the service and get the data in one line.
this.heroes = this.heroService.getHeroes();
In fact, we do not need a special method to wrap one string. We write this anyway:
app.component.ts (getHeroes)
getHeroes() { this.heroes = this.heroService.getHeroes(); }
AppComponent
should receive and display heroes without AppComponent
fuss. Where should we call getHeroes
? In the constructor? Do n't do that!
Years of experience and bitter tears have taught us to make complex logic from the constructor, especially the one that can use the server as a data source.
The constructor is intended for simple initializations, like assigning constructor parameters to properties. But not for heavy things. We should be able to create a component in the test and not worry that it can do real work — such as calling the server! - until we tell him to do it.
If there is no constructor, then someone should make a call to the getHeroes
method.
Angular will do this if we implement the ngOnInit Lifecycle Hook (the ngOnInit life cycle hook). Angular offers a number of interfaces to participate in the critical moments of the component life cycle: during creation, after each change, and during its final destruction.
Each interface has one method. When a component implements this method, Angular makes it call at the appropriate time.
Learn more about the life cycle of hooks in the chapter Life cycle of hooks .
Here is a brief description of the OnInit interface:
app.component.ts (OnInit protocol)
import { OnInit } from '@angular/core'; export class AppComponent implements OnInit { ngOnInit() { } }
We write the ngOnInit
method with our initialization logic inside and let Angular call it at the right time. In our case, the initialization is to call getHeroes
.
app.component.ts (OnInit protocol)
ngOnInit() { this.getHeroes(); }
Our application, as expected, shows a list of heroes and a view with detailed information about the hero when we click on the name of the hero.
We are getting closer. But something else is not quite right.
Our HeroService
returns a list of bogus heroes immediately. This is the getHeroes
synchronous signature signature:
this.heroes = this.heroService.getHeroes();
It is necessary to request heroes, and they are in the returned result.
In the future we are going to get heroes from a remote server. For now, we will not use HTTP, but we will strive for this in the next chapters.
When we make a call, we need to wait for the server to respond, and we will not be able to block the user interface while we wait, even if we want (although we should not), since the browser will not block.
We will have to use some kind of asynchronous data retrieval method that will change the signal of our getHeroes
method.
We will use promises .
A promise ... well, a promise to make our call later when the results are ready. We ask the asynchronous service to do some work and give the callback function. It does this work (somewhere) and eventually it calls our function with the results of the work or with an error.
This is a simplified description. Learn about ES2015 promises here and elsewhere on the Internet.
Update HeroService
using the getHeroes promise method:
hero.service.ts (getHeroes)
getHeroes() { return Promise.resolve(HEROES); }
We still use dummy data. We imitate the behavior of the superhigh-speed, zero-latency server, returning the already allowed (fulfilled) promise with a fictitious list of our heroes, as a result.
Returning to the AppComponent
component and the getHeroes
method, we see that it still looks like this:
app.component.ts (getHeroes - old version)
getHeroes() { this.heroes = this.heroService.getHeroes(); }
As a result of our change in HeroService
, we will redo this.heroes
to work with a promise, instead of an array of heroes.
We have to change our implementation in order to fulfill the promise when it is allowed (fulfilled) . When the promise is resolved successfully, then we will have heroes for display.
We pass our callback function as an argument to the then promise method:
app.component.ts (getHeroes - new version)
getHeroes() { this.heroService.getHeroes().then(heroes => this.heroes = heroes); }
The ES2015 arrow function in the callback is more powerful than using the equivalent function, and works elegantly with this .
Our callback assigns the heroes
component property an array of heroes that the service returned. That's all!
Our application should still show a list of heroes, and display a detailed view of the hero when choosing from the list.
Go to the "Get it Slow" app to see what data retrieval with a bad connection will look like (delayed response).
Let's check that we have the following structure after our wonderful refactoring in this chapter:
angular2-tour-of-heroes app app.component.ts hero.ts hero-detail.component.ts hero.service.ts main.ts mock-heroes.ts node_modules ... typings ... index.html package.json tsconfig.json typings.json
The code files that we discussed in this chapter.
import { Injectable } from '@angular/core'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; @Injectable() export class HeroService { getHeroes() { return Promise.resolve(HEROES); } // See the "Take it slow" appendix getHeroesSlowly() { return new Promise<Hero[]>(resolve => setTimeout(()=>resolve(HEROES), 2000) // 2 seconds ); } }
import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroDetailComponent } from './hero-detail.component'; 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> <my-hero-detail [hero]="selectedHero"></my-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; } `], directives: [HeroDetailComponent], providers: [HeroService] }) export class AppComponent implements OnInit { title = 'Tour of Heroes'; heroes: Hero[]; selectedHero: Hero; constructor(private heroService: HeroService) { } getHeroes() { this.heroService.getHeroes().then(heroes => this.heroes = heroes); } ngOnInit() { this.getHeroes(); } onSelect(hero: Hero) { this.selectedHero = hero; } }
import { Hero } from './hero'; export var 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"} ];
Let's summarize what we have created.
ngOnInit
lifecycle hook to get our heroes when activating the AppComponent
.HeroService
as a provider for AppComponent
.Our Heroes Tour has become more reusable using common components and services. We want to create an information panel, add menu links, for routing between views, and also format the data in a template. As our application develops, we will learn how to design it in order to simplify its expansion and support.
We will learn about the Angular routing component and navigation between views in the next chapter .
We can emulate a slow connection.
Import the Hero
type and add the following getHeroesSlowly
method to HeroService
hero.service.ts (getHeroesSlowy)
getHeroesSlowly() { return new Promise<Hero[]>(resolve => setTimeout(()=>resolve(HEROES), 2000) // 2 seconds ); }
Like getHeroes
, this method returns a promise. But this promise makes a delay of 2 seconds until resolved with a dummy list of heroes.
Back in AppComponent
, replace heroService.getHeroes
with heroService.getHeroesSlowly
and see how the application behaves.
We said earlier that if we include the parent AppComponent
HeroService
in HeroDetailComponent
, we should not add an array of providers to the HeroDetailComponent
metadata .
Why? Because with this we will tell Angular to create a new HeroService
instance at the HeroDetailComponent
level. The HeroDetailComponent
component HeroDetailComponent
not need its own copy of the service; he needs a copy of his parent's service. Adding an array of providers
creates a new service instance that is a clone of the parent instance.
Think carefully about where and when you need to register a supplier. Understand the scope of this registration. Be careful not to create a new instance of the service at the wrong level.
Important note. With the release of the next release in Angular there have been changes in the ngFor directive.
Instead of, for example,*ngFor="#hero of heroes"
you need to use the new syntax*ngFor="let hero of heroes"
.
So far, when using the old syntax, only a warning appears.
Source: https://habr.com/ru/post/283556/
All Articles