The hero's guide continues to move on. Next, we are ready to add more components.
In the future there will be more components to access the hero data, we do not want to copy and paste the same code over and over again. The solution is to create a single, reusable data service and then learn to inject it into the components that need it.
We will refactor the data access code, isolate it to a separate service, keep the component as thin as possible, and focus on supporting the view. In this way, it is easier to unit-test components with a simulation service.
Because data services are usually asynchronous, we will create a commitment (Promise) -based data service in this chapter.
Of course, in the beginning we have to let our program run. In the terminal input
NPM start
Create a Hero Service
Customers have shown us the bigger goal of this app: to show heroes in a variety of ways on different pages. Now we can select a hero from the list, but that's not enough. Soon, we'll add a dashboard to show the best performing heroes and create a standalone view to edit the hero's details. All of these views require heroic data.
Currently, the AppComponent
analog data is displayed. There are at least two places to improve: First, defining a hero's data should not be a component's task; second, it is not so easy to share the data of this heroic list with other components and views.
We can refactor the task of acquiring heroic data into a single service that will provide heroic data and share the service among all the components that need the hero's data.
Create Heroservice
app
create a file named under the directory hero.service.ts
.
Our file name follows the rule that the lowercase server name plus the. Service suffix, if the service name contains more than one word, we write the basic name part in the midline form (dash-case). For example, SpecialSuperHeroService
a service should be defined in a special-super-hero.service.ts
file.
We name this class and HeroService
export it for others to use.
App/hero.service.ts
Import {injectable} from ' @angular/core '; @Injectable () Export class Heroservice {}
View CodeServices that can be injected
Note that we have imported the functions of Angular Injectable
and @Injectable()
used this function as adorners.
don't forget to write the parentheses! If you forget to write, it can cause a difficult error to diagnose.
When TypeScript sees the @Injectable()
adorner, it writes down the metadata for the service. If Angular needs to inject other dependencies into the service, the metadata will be used.
Get Hero Data
Add a getHeros
method called.
@Injectable () Export class Heroservice { getheroes (): void {}//stub}
View Code
To pause on this implementation, let's start with a point.
Data users do not know how the service will obtain data. Our HeroService
services can get heroes ' data from anywhere. It can be obtained from a Web server, can be obtained from the local store of the browser, or it can be from a simulated data source.
This is the beauty of removing the data access code from the component. This allows us to change the way data access is implemented at any time without having to make any changes to the components that use heroes.
Simulating hero data
We have AppComponent
written the simulation data in the component. It shouldn't be there, but it shouldn't be here! We should move the simulation data to its own file.
Cut the array from the app.component.ts
file HEROS
and paste it into app
a file named under mock-heroes.ts
the directory. Also copy the import {Hero}...
statement, because our hero array uses the Hero
class
App/mock-heroes.ts
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 '}];
View Code
We have exported HEROES
constants so that we can import it elsewhere-for example, a HeroService
service.
Also, back to the file that just clipped the HEROES
array app.component.ts
, we left a property that has not yet been initialized heroes
:
heroes: Hero[];
Returns the simulated hero data
Back HeroService
, we import the HEROES
constant and getHeroes
return it in the method. Our HeroService
service now looks like this:
Import {injectable} from ' @angular/core ', import {Hero} from './hero ', Import {heroes} from './mock-heroes '; @Injectabl E () Export class Heroservice { getheroes (): hero[] { return heroes; }}
View CodeUsing the Heroservice service
We can use the Heroservice service in multiple components, starting with AppComponent first.
Usually, we first import what we want to use, for example HeroService
.
Import {Heroservice} from './hero.service ';
Importing this service allows us to reference it in code. AppComponent
How do you get a specific instance in the Run HeroService
?
Are we going to make this heroservice ourselves? No!
Although we can use new
to create HeroService
an instance like this:
Heroservice = new Heroservice (); That's not true
But this is not a good idea, there are many reasons, for example:
Our component has to figure out how to create it HeroService
. If one day we modify HeroService
the constructor, we have to find out the code that created the service and modify it. Circling around patch codes can easily lead to errors and increase the burden of testing.
We new
will create a new service instance each time we use it. What if the service needs to cache the Hero list and share the cache with someone else? What to do? There is no way to do it.
We put AppComponent
a lock into HeroService
a specific implementation. It is difficult to switch implementations in different scenarios. For example, can I operate offline? Can I use a different version of the simulation during the test? It's not easy.
If...... If...... Hey! We've got a lot of work to do here!
There's a way, really! It's an incredibly simple way to solve these problems, and you'll never make excuses for mistakes.
Inject Heroservice
Use these two lines of code instead new
of a single line:
Add a constructor and define a private property.
Add metadata for the component providers
.
Here's the constructor:
Constructor (private Heroservice:heroservice) {}
The constructor does nothing for itself, it defines a private attribute in the parameter heroService
and marks it as HeroService
the injected target.
Now, when AppComponent
you create an instance, Angular knows that you need to provide an HeroService
instance first.
See here, isn't it exciting to be familiar with constructor injection.
The injector does not yet know how to create it HeroService
. If we run our code now, Angular will fail with an error:
Exception:no Provider for heroservice! (AppComponent-Heroservice) (Exception: a provider without heroservice!) (AppComponent-Heroservice))
View Code
Do you have one?
We also have to register a HeroService
provider to tell the injector how to create it HeroService
. To do this, we @Component
add an array property at the bottom of the component's metadata providers
as follows:
Providers:[heroservice]
providers
The array tells Angular that when it creates AppComponent
a new component, it also creates a HeroService
new instance. AppComponent
will use that service to get a list of heroes, as is the case with every sub-component in its component tree.
The Getheroes in AppComponent
We already have the service and put it in a private variable heroService
. We're going to start using it.
Stop and think about it. We can invoke the service and get the data in the same row.
This.heroes = This.heroService.getHeroes ();
In the real world, we don't need to wrap a line of code into a specific method, but in any case, we'll write it in the demo code:
Getheros (): void{
this.heroes = this.heroService.getHeroes();
}
Ngoninit Life cycle Hooks
There's no question that you AppComponent
should get the hero data and show it. Where do we call the getHeroes
method? In the constructor? No!
Years of experience and painful lessons teach us that complex logic should be thrown out of the constructor, especially those that need to fetch data from the server.
Constructors are designed for simple initialization work, such as assigning parameters to a constructor to a property. Its burden should not be too heavy. We should be able to create a component in the test without worrying about it doing the actual work-for example, communicating with the server until we proactively ask it to do so.
If it's not in the constructor, there's always a place to call getHeroes
it.
It's not difficult. As long as we implement the Angular ngoninit life cycle hooks, Angular will invoke this hook on its own initiative. Angular provides a number of interfaces that are used to intervene in several key points in the lifecycle of a component: when it was first created, every time it was changed, and when it was eventually destroyed.
Each interface has a unique method. As long as the component implements this method, Angular will invoke it at the appropriate time.
This is OnInit
the basic outline of the interface:
Import {OnInit} from ' @angular/core '; Export class AppComponent implements OnInit { ngoninit (): void { }}
View Code
We have written a method with initialization logic ngOnInit
, and angular will invoke it at the appropriate time. In this example, we use the call getHeroes
to complete the initialization.
Ngonit (): Void{this.getheroes ();}
View Code
Our app will run as expected, show the list of Heroes, and show the hero's details when we click on the hero's name.
We're almost done, but there's something wrong with it.
Asynchronous services and commitments
Our HeroService
immediate return is a list of simulated heroes whose getHeroes
function signatures are synchronous.
this.heroes = this.heroService.getHeroes();
Request the Hero data and return the result with them.
In the future, we intend to get hero data from the remote server. We haven't called http yet, but we'd like to do that in a later chapter.
At that time, we had to wait for the server to respond, and we couldn't block the user interface response while we waited, even if we wanted to (and should not) do so, because the browser didn't block. (Why should we mention the blockage here, because there is an example of a timely search showing the results)
We have to use some asynchronous techniques, and this will change getHeroes
the signature of the method.
We will use the promise.
HeroService
Will generate a promise
The promise is ... Well, it's a promise, and it promises to back us up when it comes to the results. We ask for an asynchronous service to do something and give it a callback function. It will do (somewhere), once it is done, it will call our callback function and pass the result of the work or the error message to us through the parameters.
HeroService
rewrite the getHeroes
method to return the form of commitment:
Getheroes ():P romise<Hero[]>{ return promise.resolve (Heroes);}
We continue to use simulation data. We simulated an ultra-fast, 0-latency Super Server by returning an immediate resolution of the promise.
Commitment-based action
Back AppComponent
and it's the getHeroes
way we see it still looks like this:
App/app.component.ts (Getheroes-old)
Getheroes (): void { this.heroes = This.heroService.getHeroes (); }
View Code
After we have modified it HeroService
, we will replace it with this.heroes
a promise instead of being an array of heroes.
We have to revise this implementation, turn it into commitment-based, and move on when the promised thing is resolved. Once the promised thing is resolved successfully, we will show the hero data.
We pass the callback function as an argument to the committed object's then method:
App/app.component.ts (getheroes-revised)
Getheroes (): void { this.heroService.getHeroes (). Then (Heroes = This.heroes = Heroes);}
View Code
In the callback function, we assign the heroic array returned by the service to the properties of the component heroes
. Yes, it's done.
Our program is still running, the hero list is still displayed, and when you select a hero, it will still be displayed on the details page.
Here are the code files discussed in this chapter:
App/hero.service.ts
Import { injectable} from ' @angular/core ', import {Hero} from './hero ', Import {heroes} from './mock-heroes '; @Injec Table () Export class Heroservice { getheroes (): Promise<Hero[]> { return promise.resolve (Heroes); }}
View Code
App/app.component.ts
Import {Component, OnInit} from ' @angular/core '; import {Hero} from './hero '; import {heroservice} from './hero.se Rvice '; @Component ({selector: ' My-app ', Template: '<H1>{{title}}</H1> <H2>My Heroes</H2> <ulclass= "Heroes"> <Li*ngfor= "Let Hero of Heroes"[class.selected]= "Hero = = = Selectedhero"(click)= "OnSelect (Hero)"> <spanclass= "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; } '], 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 = t His.heroes = Heroes); } ngoninit (): void {this.getheroes (); } onSelect (Hero:hero): void {This.selectedhero = hero; }}
View Code
App/mock-heroes.ts
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 '}];
View CodeThe road I've traveled
To take stock of what we have done.
We have created a service class that can be shared by multiple components.
We used a ngOnInit
lifecycle hook to AppComponent
get the hero data when it was activated.
We have HeroService
defined it as AppComponent
a provider.
We created the simulated hero data and imported it into our service.
We design the service as a return commitment, and the component gets the data from the commitment.
The road ahead
By using shared components and services, our Heroes ' Guide is more reusable. We also create a dashboard to add menu links that route between views, and to format the data in the template. As our application evolves, we will also learn how to design and make it easier to scale and maintain.
In the next chapter we will learn about the Angular component Routing and the knowledge of navigating between views.
Attachment: A little slower
We can simulate a slow connection.
Import the Hero
class, and HeroService
add the following getHeroesSlowly
method in:
Getheroesslowly (): Promise<Hero[]> { return new Promise <Hero[]>(resolve = setTimeout (resolve,))//delay 2 seconds . Then (() = This.getheroes ());
View Code
Like getHeroes
, it also returns a promise. However, this commitment will wait two seconds before providing the simulation data.
Back AppComponent
, heroService.getHeroesSlowly
replace heroService.getHeroes
, and observe the behavior of the application.
Next, we'll learn to route
Ng2-We create a reusable service to invoke the hero's data