Книга: Angular 2 Cookbook
Назад: Controlling service instance creation and injection with NgModule
Дальше: Injecting a value as a service with useValue and OpaqueTokens

Service injection aliasing with useClass and useExisting

As your application becomes more complex, you may come to a situation where you would like to use your services in a polymorphic style. More specifically, some places in your application may want to request Service A, but a configuration somewhere in your application will actually give it Service B. This recipe will demonstrate one way in which this can be useful, but this behavior allows your application to be more extensible in multiple ways.

Note

The code, links, and a live example of this are available at .

Getting ready

Suppose you begin with the following skeleton application.

Dual services

You begin with two services, ArticleService and EditorArticleService, and their shared interface, ArticleSourceInterface. EditorArticleService inherits from ArticleService:

[app/article-source.interface.ts]      export interface ArticleSourceInterface {     getArticle():Article   }      export interface Article {     title:string,      body:string,      // ? denotes an optional property     notes?:string    }   [app/article.service.ts]      import {Injectable} from '@angular/core';   import {Article, ArticleSourceInterface}      from './article-source.interface';      @Injectable()   export class ArticleService implements ArticleSourceInterface {     private title_:string =        "Researchers Determine Ham Sandwich Not Turing Complete";     private body_:string =        "Computer science community remains skeptical";        getArticle():Article {       return {         title: this.title_,         body: this.body_       };     }   }   [app/editor-article.service.ts]      import {Injectable} from '@angular/core';   import {ArticleService} from './article.service';   import {Article, ArticleSourceInterface}      from './article-source.interface';      @Injectable()   export class EditorArticleService extends ArticleService        implements ArticleSourceInterface {     private notes_:string = "Swing and a miss!";        constructor() {        super();      }        getArticle():Article {       // Combine objects and return the joined object       return Object.assign(         {},          super.getArticle(),          {            notes: this.notes_          });     }   }   

A unified component

Your objective is to be able to use the following component so that both these services can be injected into the following component:

[app/article.component.ts]      import {Component} from '@angular/core';   import {ArticleService} from './article.service';   import {Article} from './article-source.interface';      @Component({     selector: 'article',     template: `       <h2>{{article.title}}</h2>       <p>{{article.body}}</p>       <p *ngIf="article.notes">         <i>Notes: {{article.notes}}</i>       </p>     `   })   export class ArticleComponent {     article:Article;     constructor(private articleService_:ArticleService) {        this.article = articleService.getArticle();     }   }   

How to do it...

When listing providers, Angular 2 allows you to declare an aliased reference that specifies what service should actually be provided when one of the certain types is requested. Since Angular 2 injection will follow the component tree upwards to find the provider, one way to declare this alias is by wrapping the component with a parent component that will specify this alias:

[app/default-view.component.ts]      import {Component} from '@angular/core';   import {ArticleService} from './article.service';      @Component({     selector: 'default-view',     template: `       <h3>Default view</h3>       <ng-content></ng-content>     `,     providers: [ArticleService]   })   export class DefaultViewComponent {}   [app/editor-view.component.ts]      import {Component } from '@angular/core';   import {ArticleService} from './article.service';   import {EditorArticleService} from './editor-article.service';      @Component({     selector: 'editor-view',     template: `       <h3>Editor view</h3>       <ng-content></ng-content>     `,     providers: [       {provide: ArticleService, useClass: EditorArticleService}     ]   })   export class EditorViewComponent {}   

Note

Note that both these classes are acting as passthrough components. Other than adding a header (which is merely for learning the purpose of instruction in this recipe), these classes are only specifying a provider and are unconcerned with their content.

With the wrapper classes defined, you can now add them to the application module, then use them to create two instances of ArticleComponent:

[app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RootComponent} from './root.component';   import {ArticleComponent} from './article.component';   import {DefaultViewComponent} from './default-view.component';   import {EditorViewComponent} from './editor-view.component';   import {ArticleComponent} from './article.component';   import {ArticleService} from './article.service';   import {EditorArticleService} from './editor-article.service';      @NgModule({     imports: [       BrowserModule     ],     declarations: [       RootComponent,       ArticleComponent,       DefaultViewComponent,       EditorViewComponent     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}   [app/root.component.ts]      import {Component} from '@angular/core';      @Component({     selector: 'root',     template: `       <default-view>         <article></article>       </default-view>       <hr />       <editor-view>         <article></article>       </editor-view>     `   })   export class RootComponent {}   

With this, you should now see that the editor version of ArticleComponent gets the notes, but the default version does not.

How it works...

In Angular 1, the service type that was supposed to be injected was identified from a function parameter by doing a direct match of the parameter symbol. function(Article) would inject the Article service, function (User) the User service, and so on. This led to nastiness, such as the minification-proofing of constructors by providing an array of strings to match against ['Article', function(Article) {}].

This is no longer the case. When a provider is registered, the useClass option utilizes the two-part dependency injection matching scheme in Angular 2. The first part is the provider token, which is the parameter type of the service being injected. In this case, private articleService_:ArticleService uses the ArticleService token to request that an instance be injected. Angular 2 takes this and matches this token against the declared providers in the component hierarchy. When a token is matched, Angular 2 will use the second part, the provider itself, to inject an instance of the service.

In reality, providers: [ArticleService] is a shorthand for providers: [{provide: ArticleService, useClass: ArticleService}]. The shorthand is useful since you will almost always be requesting the service class that would match the injected class. However, in this recipe, you are configuring Angular 2 to recognize an ArticleService token and so use the EditorArticleService provider.

There's more...

An attentive developer will have realized by this point that the utility of useClass is limited in the sense that it does not allow you to independently control where the actual service is provided. In other words, the place where you intercept the provider definition with useClass is also the place where the replacement class will be provided.

In this example, useClass is suitable since you are perfectly happy to provide EditorArticleService in the same place where you are specifying that it should be used to replace ArticleService. However, it is not difficult to imagine a scenario in which you would like to specify the replacement service type but have it injected higher up in the component tree. This, after all, would allow you to reuse instances of a service instead of having to create a new one for each useClass declaration.

For this purpose, you can use useExisting. It requires you to explicitly provide the service type separately, but it will reuse the provided instance instead of creating a new one. For the application you just created, you can now reconfigure it with useExisting, and provide both the services at the RootComponent level.

To demonstrate that your reasoning about the service behavior is correct, double the number of Article components, and add a log statement to the constructor of ArticleService to ensure you are only creating one of each service:

[app/root.component.ts]      import {Component} from '@angular/core';      @Component({     selector: 'root',     template: `       <default-view>         <article></article>       </default-view>       <editor-view>         <article></article>       </editor-view>       <default-view>         <article></article>       </default-view>       <editor-view>         <article></article>       </editor-view>     `   })   export class RootComponent {}   [app/article.service.ts]      import {Injectable} from '@angular/core';   import {Article, ArticleSourceInterface} from './article-source.interface';      @Injectable()   export class ArticleService implements ArticleSourceInterface {     private title_:string =        "Researchers Determine Ham Sandwich Not Turing Complete";     private body_:string =        "Computer science community remains skeptical";        constructor() {       console.log('Instantiated ArticleService!');     }        getArticle():Article {       return {         title: this.title_,         body: this.body_       };     }   }   [app/editor-view.component.ts]      import {Component} from '@angular/core';   import {ArticleService} from './article.service';   import {EditorArticleService} from './editor-article.service';      @Component({     selector: 'editor-view',     template: `       <h3>Editor view</h3>       <ng-content></ng-content>     `,     providers: [       {provide: ArticleService, useExisting: EditorArticleService}     ]   })   export class EditorViewComponent {}   [app/default-view.component.ts]      import {Component} from '@angular/core';   import {ArticleService} from './article.service';      @Component({     selector: 'default-view',     template: `       <h3>Default view</h3>       <ng-content></ng-content>     `     // providers removed   })   export class DefaultViewComponent {}   

In this configuration, with useClass, you will see that one instance of ArticleService and two instances of EditorArticleService are created. When replaced with useExisting, you will find that only one instance of each is created.

Thus, in this reconfigured version of the recipe, your application is doing the following:

  • At the RootComponent level, it is providing EditorArticleService
  • At the EditorViewComponent level, it is redirecting ArticleService injection tokens to EditorArticleService
  • At the ArticleComponent level, it is injecting ArticleService using the ArticleService token

Refactoring with directive providers

If this implementation seems clunky and verbose to you, you are certainly on to something. The intermediate components are performing their jobs quite well, but aren't really doing anything other than shimming in an intermediate provider's declaration. Instead of wrapping in a component, you can migrate the provider's statement into a directive and do away with both the view components:

[app/root.component.ts]      import {Component} from '@angular/core';   import {ArticleComponent} from './ article.component';   import {ArticleService} from './article.service';   import {EditorArticleService} from './editor-article.service';      @Component({     selector: 'root',     template: `       <article></article>       <article editor-view></article>       <article></article>       <article editor-view></article>     `   })   export class RootComponent {}   [app/editor-view.directive.ts]      import {Directive } from '@angular/core';   import {ArticleService} from './article.service';   import {EditorArticleService} from './editor-article.service';      @Directive({     selector: '[editor-view]',     providers: [       {provide: ArticleService, useExisting: EditorArticleService}     ]   })   export class EditorViewDirective {}   [app/app.module.ts]      import {NgModule} from '@angular/core';   import {BrowserModule} from '@angular/platform-browser';   import {RootComponent} from './root.component';   import {ArticleComponent} from './article.component';   import {DefaultViewComponent} from './default-view.component';   import {EditorViewDirective} from './editor-view.directive';   import {ArticleComponent} from './article.component';   import {ArticleService} from './article.service';   import {EditorArticleService} from './editor-article.service';      @NgModule({     imports: [       BrowserModule     ],     declarations: [       RootComponent,       ArticleComponent,       DefaultViewComponent,       EditorViewDirective     ],     providers: [       ArticleService,       EditorArticleService     ],     bootstrap: [       RootComponent     ]   })   export class AppModule {}   

Your application should work just the same!

See also

  • Injecting a simple service into a component walks you through the basics of Angular 2's dependency injection schema
  • Controlling service instance creation and injection with NgModule gives a broad overview of how Angular 2 architects provider hierarchies using modules
  • Injecting a value as a service with useValue and OpaqueTokens shows how you can use dependency-injected tokens to inject generic objects
  • Building a provider-configured service with useFactory details the process of setting up a service factory to create configurable service definitions
Назад: Controlling service instance creation and injection with NgModule
Дальше: Injecting a value as a service with useValue and OpaqueTokens

thank you
Flame
cant read the code since it is all on a single line. Also this comments section is russian
Rakuneque
DATA COLLECTION AND ANALYSIS Two reviewers extracted data and assessed methodological quality independently lasix torsemide conversion Many others were in that space already