📜 ⬆️ ⬇️

Angular 2 Beta, training course "Tour of Heroes" part 4

Part 1 Part 2 Part 3 Part 4


Services


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).


Run the application, part 4


Where we stayed


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 

Support code conversion and application execution


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.


Creating service 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 HeroService


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, the SpecialSuperHeroService service will be defined by the file name special-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 { } 

Service implementation


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.


Getting heroes

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.


Bogus data of heroes

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[]; 

Return fictitious list of heroes

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; } } 

Using the service of 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?


Should we use new HeroService ? In no case!

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.



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.


Introduction of HeroService

Two lines instead of the one using new :


  1. We will add a constructor.
  2. We will add the 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.


getHeroes in AppComponent

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(); } 

Hook ngOnInit life cycle

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.


Asynchronous services and promises (promises)


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 .


Service heroes make a promise

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.


Keeping the promise

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).

Application structure overview


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.


app / hero.service.ts
  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 ); } } 

app / app.component.ts
  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; } } 

app / mock-heroes.ts
  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"} ]; 

The path that we have passed


Let's summarize what we have created.



Run the application, part 4


The way ahead


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 .


Application: Get it slow


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.


Appendix: Shadow Cloning of the Parent Service


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