Chapter 4. Dependency injection

This chapter covers

  • Introducing Dependency Injection (DI) as a design pattern
  • Benefits of DI
  • How Angular implements DI
  • Registering object providers and using injectors
  • The hierarchy of injectors
  • Applying DI in the online auction application

In the previous chapter, we discussed the router, and now your online auction application knows how to navigate from the Home view to an almost-empty Product Details view. In this chapter, you’ll continue working on the online auction, but this time we’ll concentrate on how to use Angular to automate the process of creating objects and assembling the application from its building blocks.

Any Angular application is a collection of components, directives, and classes that may depend on each other. Although each component can explicitly instantiate its dependencies, Angular can do this job using its dependency injection (DI) mechanism.

We’ll start this chapter by identifying the problem that DI solves and reviewing the benefits of DI as a software engineering design pattern. Then we’ll go over the specifics of how Angular implements the DI pattern using an example ProductComponent that depends on a ProductService. You’ll see how to write an injectable service and how to inject it into another component.

Then you’ll see a sample application that demonstrates how Angular DI allows you to easily replace one component dependency with another by changing just one line of code. After that, we’ll introduce a more advanced concept: a hierarchy of injectors. At the end of the chapter, we’ll go through a hands-on exercise to build the next version of the online auction that uses the techniques covered in the chapter.

4.1. The Dependency Injection and Inversion of Control patterns

Design patterns are recommendations for how to solve certain common tasks. A given design pattern can be implemented differently depending on the software you use. In this section, we’ll briefly introduce two design patterns: Dependency Injection (DI) and Inversion of Control (IoC).

4.1.1. The Dependency Injection pattern

If you’ve ever written a function that takes an object as an argument, you can say that you wrote a program that instantiates this object and injects it into the function. Imagine a fulfillment center that ships products. An application that keeps track of shipped products can create a product object and invoke a function that creates and saves a shipment record:

var product = new Product();
createShipment(product);

The createShipment() function depends on the existence of an instance of the Product object. In other words, the createShipment() function has a dependency: Product. But the function itself doesn’t know how to create Product. The calling script should somehow create and give (think inject) this object as an argument to the function.

Technically, you’re decoupling the creation of the Product object from its use—but both of the preceding lines of code are located in the same script, so it’s not real decoupling. If you need to replace Product with MockProduct, it’s a small code change in this simple example.

What if the createShipment() function had three dependencies (such as product, shipping company, and fulfillment center), and each of those dependencies had its own dependencies? In that case, creating a different set of objects for createShipment() would require many more manual code changes. Would it be possible to ask someone to create instances of dependencies (with their dependencies) for you?

This is what the Dependency Injection pattern is about: if object A depends on an object of type B, object A won’t explicitly instantiate object B (as with the new operator in the previous example). Rather, it will have B injected from the operational environment. Object A just needs to declare, “I need an object of type B; could someone please give it to me?” The words of type are important here. Object A doesn’t request a specific implementation of the object and will be happy as long as the injected object is of type B.

4.1.2. The Inversion of Control pattern

Inversion of Control (IoC) is a more general pattern than DI. Rather than making your application use some API from a framework (or a software container), the framework creates and supplies the objects that the application needs. The IoC pattern can be implemented in different ways, and DI is one of the ways of providing the required objects. Angular plays the role of the IoC container and can provide the required objects according to your component’s declarations.

4.1.3. Benefits of dependency injection

Before we explore the syntax of Angular’s implementation of DI, let’s look at the benefits of having objects injected versus instantiating them with a new operator. Angular offers a mechanism that helps with registering and instantiating component dependencies. In short, DI helps you write code in a loosely coupled way and makes your code more testable and reusable.

Loose coupling and reusability

Say you have a ProductComponent that gets product details using the ProductService class. Without DI, your ProductComponent needs to know how to instantiate the ProductService class. This can be done multiple ways, such as using new, calling getInstance() on a singleton object, or invoking createProductService() on some factory class. In any case, ProductComponent becomes tightly coupled with ProductService.

If you need to reuse ProductComponent in another application that uses a different service to get product details, you must modify the code (for example, productService = new AnotherProductService()). DI allows you to decouple application components by sparing them from the need to know how to create their dependencies.

Consider the following ProductComponent example:

@Component({
  providers: [ProductService]
})
class ProductComponent {
  product: Product;

  constructor(productService: ProductService) {

    this.product = productService.getProduct();
  }
}

In Angular applications, you register objects for DI by specifying providers. A provider is an instruction to Angular about how to create an instance of an object for future injection into a target component or directive. In the preceding code snippet, the line providers:[ProductService] is shorthand for providers:[{provide:ProductService, useClass:ProductService}].

Note

You saw the providers property in chapter 3, but it was defined not on the component but on the module level.

Angular uses the concept of a token, which is an arbitrary name representing an object to be injected. Usually the token’s name matches the type of the object to be injected, so the preceding code snippet instructs Angular to provide a ProductService token using the class of the same name. Using an object with the property provide, you can map the same token to different values or objects (such as to emulate the functionality of the ProductService while someone else is developing a real service class).

Note

In section 4.4.1, you’ll see how to declare a token with an arbitrary name.

Now that you’ve added the providers property to the @Component annotation of ProductComponent, Angular’s DI module will know that it has to instantiate an object of type ProductService. ProductComponent doesn’t need to know which concrete implementation of the ProductService type to use—it’ll use whatever object is specified as a provider. The reference to the ProductService object will be injected via the constructor’s argument, and there’s no need to explicitly instantiate ProductService in ProductComponent. Just use it as in the preceding code, which calls the service method getProduct() on the ProductService instance magically created by Angular.

If you need to reuse the same ProductComponent in a different application with a different implementation of the type ProductService, change the providers line, as in the following example:

providers: [{provide: ProductService, useClass: AnotherProductService}]

Now Angular will instantiate AnotherProductService, but the code using the type ProductService won’t break. In this example, using DI increases the reusability of ProductComponent and eliminates its tight coupling with ProductService. If one object is tightly coupled with another, this may require substantial code modifications if you want to reuse just one of them in another application.

Testability

DI increases the testability of your components in isolation. You can easily inject mock objects if their real implementations aren’t available or you want to unit-test your code.

Say you need to add a login feature to your application. You can create a LoginComponent (to render ID and password fields) that uses a LoginService component, which should connect to a certain authorization server and check the user’s privileges. The authorization server has to be provided by a different department, but it’s not ready yet. You finish coding the LoginComponent, but you can’t test it for reasons that are out of your control, such as a dependency on another component developed by someone else.

In testing, we often use mock objects that mimic the behavior of real objects. With a DI framework, you can create a mock object, MockLoginService, that doesn’t connect to an authorization server but rather has hard-coded access privileges assigned to the users with certain ID/password combinations. Using DI, you can write a single line that injects MockLoginService into your application’s Login view without needing to wait until the authorization server is ready. Later, when that server is ready, you can modify the providers line so Angular will inject the real LoginService component, as shown in figure 4.1.

Figure 4.1. DI in testing

Note

In the hands-on section of chapter 9, you’ll see how to unit-test injectable services.

4.2. Injectors and providers

Now that you’ve had a brief introduction to Dependency Injection as a general software engineering design pattern, let’s go over the specifics of implementing DI in Angular. In particular, we’ll go over such concepts as injectors and providers.

Each component can have an Injector instance capable of injecting objects or primitive values into a component or service. Any Angular application has a root injector available to all of its modules. To let the injector know what to inject, you specify the provider. An injector will inject the object or value specified in the provider into the constructor of a component.

Note

Although eagerly loaded modules don’t have their own injectors, a lazy-loaded module has its own sub-root injector that’s a direct child of the application root injector.

Providers allow you to map a custom type (or a token) to a concrete implementation of this type (or value). You can specify the provider(s) either inside the component’s @Component decorator or as a property of @NgModule, as in every code sample so far.

Tip

In Angular, you can inject data only via a constructor’s arguments. If you see a class with a no-argument constructor, it’s a guarantee that nothing is injected into this component.

You’ll be using ProductComponent and ProductService for all the code samples in this chapter. If your application has a class implementing a particular type (such as Product-Service), you can specify a provider object for this class during the AppModule bootstrap, like this:

@NgModule({
  ...
  providers: [{provide:ProductService,useClass:ProductService}]
})

When the token name is the same as the class name, you can use the shorter notation to specify the provider in the module:

@NgModule({
  ...
  providers: [ProductService]
})

The providers property can be specified in the @Component annotation. The short notation of the ProductService provider in @Component looks like this:

providers:[ProductService]

No instance of ProductService is created at this point. The providers line instructs the injector as follows: “When you need to construct an object that has an argument of type ProductService, create an instance of the registered class for injection into this object.”

Note

Angular also has the viewProviders property, which is used when you don’t want the child components to use providers declared in the parent. You’ll see an example of using viewProviders in section 4.5.

If you need to inject a different implementation of a particular type, use the longer notation:

@NgModule({
  ...
  providers: [{provide:ProductService,useClass:MockProductService}]
})

Here it is on the component level:

@Component({
   ...
   providers: [{provide:ProductService, useClass:MockProductService}]
})

This gives the following instruction to the injector: “When you need to inject an object of type ProductService into a component, create an instance of the class MockProductService.”

Thanks to the provider, the injector knows what to inject; now you need to specify where to inject the object. In TypeScript, it comes down to declaring a constructor argument specifying its type. The following line shows how to inject an object of type ProductService into the constructor of a component:

constructor(productService: ProductService)
Injection with TypeScript vs. ES6

TypeScript simplifies the syntax of injection into a component because it doesn’t require you to use any DI annotations with the constructor arguments. All you need to do is specify the type of the constructor’s argument:

constructor(productService: ProductService)

This works because any component has an annotation @Component. And because the TypeScript compiler is configured with the option "emitDecoratorMetadata": true, Angular will automatically generate all required metadata for the object to be injected.

Because you use SystemJS for on-the-fly TypeScript transpiling, you can add the following TypeScript compiler option in systemjs.config.js:

typescriptOptions: {
    "emitDecoratorMetadata": true
  }

If you’re writing the class in ES6, add the @Inject annotation with an explicit type to the constructor’s arguments:

constructor(@Inject(ProductService) productService)

The constructor will remain the same regardless of which concrete implementation of ProductService is specified as a provider. Figure 4.2 shows a sample sequence diagram of the injection process.

Figure 4.2. Injecting in time

4.2.1. How to declare a provider

You can declare custom providers as an array of objects that contain a provide property. Such an array can be specified in the providers property of the module or on the component level.

Here’s an example of a single-element array that specifies the provider object for the ProductService token:

[{provide:ProductService, useClass:MockProductService}]

The provide property maps the token to the method of instantiating the injectable object. This example instructs Angular to create an instance of the MockProduct-Service class wherever the ProductService token is used as a dependency. But the object creator (Angular’s injector) can use a class, a factory function, a string, or a special OpaqueToken class for instantiation and injection:

  • To map a token to an implementation of a class, use the object with the useClass property, as shown in the preceding example.
  • If you have a factory function that instantiates objects based on certain criteria, use an object with the useFactory property, which specifies a factory function (or a fat-arrow expression) that knows how to instantiate required objects. The factory function can have an optional argument with dependencies, if they exist.
  • To provide a string with a simple injectable value (such as the URL of a service), use the object with the useValue property.

In the next section, you’ll use the useClass property while reviewing a basic application. Section 4.4 will illustrate useFactory and useValue.

4.3. A sample application with Angular DI

Now that you’ve seen a number of code snippets related to Angular DI, let’s build a small application that will bring all the pieces together. We want to prepare you for using DI in the online auction application.

4.3.1. Injecting a product service

Let’s create a simple application that uses ProductComponent to render product details and ProductService to supply data about the product. If you use the downloadable code that comes with the book, this app is located in the main-basic.ts file in the di_samples directory. In this section, you’ll build an application that produces the page shown in figure 4.3.

Figure 4.3. A sample DI application

ProductComponent can request the injection of the ProductService object by declaring the constructor argument with a type:

constructor(productService: ProductService)

Figure 4.4 shows a sample application that uses these components.

Figure 4.4. Injecting ProductService into ProductComponent

AppModule bootstraps AppComponent, which includes ProductComponent, which is dependent on ProductService. Note the import and export statements. The class definition of ProductService starts with the export statement, to enable other components to access its content. ProductComponent includes the import statement providing the name of the class (ProductService) and the module being imported (located in the file product-service.ts).

The providers attribute defined on the component level instructs Angular to provide an instance of the ProductService class when requested. ProductService may communicate with some server, requesting details for the product selected on the web page, but we’ll skip this part for now and concentrate on how this service can be injected into ProductComponent. Let’s implement the components from figure 4.4.

In addition to index.html, you’ll be creating the following files:

  • The main-basic.ts file will contain the code to load the AppModule, which includes AppComponent, which hosts ProductComponent.
  • ProductComponent will be implemented in the product.ts file.
  • ProductService will be implemented in a single product-service.ts file.

Each of these is pretty simple. The main-basic.ts file, shown in the following listing, contains the code of the module and root component, which hosts the Product-Component child component. The module imports and declares ProductComponent.

Listing 4.1. main-basic.ts
import {Component} from '@angular/core';
import ProductComponent from './components/product';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@Component({
    selector: 'app',
    template: `<h1> Basic Dependency Injection Sample</h1>
             <di-product-page></di-product-page>`
})
class AppComponent {}

@NgModule({
    imports:      [ BrowserModule],
    declarations: [ AppComponent, ProductComponent],
    bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Based on the tag <di-product-page>, it’s easy to guess that there’s a component with the selector having this value. This selector is declared in ProductComponent, whose dependency (ProductService) is injected via the constructor.

Listing 4.2. product.ts

In listing 4.2, the name of the type is the same as the name of the class—Product-Service—so you use a short notation without the need to explicitly map the provide and useClass properties. When specifying providers, you separate the name (a token) of the injectable object from its implementation. In this case, the name of the token is the same as the name of the type: ProductService. The actual implementation of this service can be located in a class called ProductService, OtherProductService, or something else. Replacing one implementation with another comes down to changing the providers line.

The constructor of ProductComponent invokes getProduct() on the service and places a reference to the returned Product object in the product class variable, which is used in the HTML template. By using double curly braces, listing 4.2 lets you bind the title, description, and price properties of the Product class.

The product-service.ts file includes the declaration of two classes: Product and ProductService.

Listing 4.3. product-service.ts

In a real-world application, the getProduct() method would have to get the product information from an external data source, such as by making an HTTP request to a remote server.

To run this example, open a command window in the project folder and execute the command npm start. The live-server will open the window, as shown earlier in figure 4.3. The instance of ProductService is injected into ProductComponent, which renders product details provided by the server.

In the next section, you’ll see a ProductService decorated with the @Injectable annotation, which can be used to generate DI metadata when the service itself has dependencies. The @Injectable annotation isn’t needed here because Product-Service doesn’t have any other service injected into it, and Angular doesn’t need additional metadata to inject ProductService into components.

4.3.2. Injecting the Http service

Often, a service will need to make an HTTP request to get the requested data. Product-Component depends on ProductService, which is injected using the Angular DI mechanism. If ProductService needs to make an HTTP request, it’ll have an Http object as its own dependency. ProductService will need to import the Http object for injection; @NgModule must import HttpModule, which defines Http providers. The ProductService class should have a constructor for injecting the Http object. Figure 4.5 shows ProductComponent depending on ProductService, which has its own dependency: Http.

Figure 4.5. A dependency can have its own dependency.

The following code snippet illustrates the Http object’s injection into ProductService and the retrieval of products from the products.json file:

import {Http} from '@angular/http';
import {Injectable} from "@angular/core";

@Injectable()

export class ProductService {
    constructor(private http:Http){
      let products = http.get('products.json');
    }
   // other app code goes here
}

The class constructor is the injection point, but where do you declare the provider for injecting the Http type object? All the providers required to inject various flavors of Http objects are declared in HttpModule. You just need to add it to your AppModule, like this:

import { HttpModule} from '@angular/http';
...
@NgModule({
  imports: [
    BrowserModule,
    HttpModule
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
Note

In section 8.3.4, you’ll write an application illustrating the architecture shown in figure 4.5.

Now that you’ve seen how to inject an object into a component, let’s look at what it takes to replace one implementation of the service with another using Angular DI.

4.4. Switching injectables made easy

Earlier in this chapter, we stated that the DI pattern allows you to decouple components from their dependencies. In the previous section, you decoupled Product-Component from ProductService. Now let’s simulate another scenario.

Suppose you’ve started development with a ProductService that’s supposed to get data from a remote server, but the server’s feed isn’t ready. Rather than modify the code in ProductService to introduce hard-coded data for testing, you’ll create another class: MockProductService.

Moreover, to illustrate how easy it is to switch from one service to another, you’ll create a small application that uses two instances of ProductComponent. Initially, the first one will use MockProductService and the second ProductService. Then, with a one-line change, you’ll make both of them use the same service. Figure 4.6 shows how the multiple_injectors application will render product components in the browser.

Figure 4.6. Rendering two products

The iPhone 7 product is rendered by Product1Component, and the Samsung 7 is rendered by Product2Component. This application focuses on switching product services using Angular DI, so we’ve kept the components and services simple. Toward this end, all of the TypeScript code is located in one main.ts file.

A class playing the role of an interface

In appendix B, we explain the TypeScript interfaces, which are a useful way to ensure that an object being passed to a function is valid or that a class implementing an interface sticks to a declared contract. A class can implement an interface using the keyword implements, but there’s more: in TypeScript, all classes can be used as interfaces (although we don’t encourage using this feature), so ClassA can implement ClassB. Even if the code isn’t initially written with interfaces, you can still use a concrete class as if it were declared as an interface.

The content of main.ts is shown in listing 4.4. We’d like to draw your attention to the following line:

class MockProductService implements ProductService

This shows one class “implementing” another as if the latter was declared as an interface.

Listing 4.4. main.ts

If a component doesn’t need a specific ProductService implementation, there’s no need to explicitly declare a provider for each component, as long as a provider was specified at the parent’s level. In listing 4.4, Product1Component doesn’t declare its own providers, so Angular will find one on the application level. But each component is free to override the providers declaration made at the app or parent component level, as in Product2Component.

ProductService becomes a common token that both product components understand. Product2Component declares an explicit provider, which maps MockProduct-Service to the common ProductService custom type. This component-level provider will override the parent’s one. If you decide that Product1Component should use MockProductService as well, you can add the providers line to its @Component annotation, as in Product2Component.

Running this application renders product components in the browser, as shown earlier in figure 4.6. This all is good, but suppose you’re notified by another team that the ProductService class (used as the app-level provider) won’t be available for some time. How can you switch to using MockProductService exclusively for a while?

This requires a one-line change. Replacing the providers line in the module declaration will do the trick:

@NgModule({
...
providers: [{provide:ProductService, useClass:MockProductService}]
...
})

From now on, wherever the type Product-Service needs to be injected and no providers line is specified on the component level, Angular will instantiate and inject Mock-ProductService. Running the application after making the preceding change renders the components as shown in figure 4.7.

Figure 4.7. Rendering two products with MockProductService

Imagine that your application had dozens of components using ProductService. If each of them instantiated this service with a new operator or a factory class, you’d need to make dozens of code changes. With Angular DI, you were able to switch the service by changing one line in the providers declaration.

JavaScript hoisting and classes

Class declarations aren’t hoisted (hoisting is explained in appendix A). Typically, each class is declared in a separate file, and their declarations are imported on top of the script so all class declarations are available up front.

If multiple classes are declared in one file, both ProductService and MockProductService must be declared before the components that use them. If you run into a situation where the objects are declared after the point of injection, consider using the function forwardRef() with the annotation @Inject (see the Angular documentation for forwardRef() at http://mng.bz/31YN).

4.4.1. Declaring providers with useFactory and useValue

Let’s look at some examples that illustrate the factory and value providers. In general, factory functions are used when you need to implement application logic prior to instantiating an object. For example, you may need to decide which object to instantiate, or your object may have a constructor with arguments that you need to initialize before creating an instance.

The following listing, from the main-factory.ts file, shows how you can specify a factory function as a provider. This factory function creates either ProductService or MockProductService based on a boolean flag.

Listing 4.5. Specifying a factory function as a provider
const IS_DEV_ENVIRONMENT: boolean = true;

@Component({
  selector: 'product2',

  providers:[{
    provide: ProductService,
    useFactory: (isDev) => {
      if (isDev){
        return new MockProductService();
      } else{
        return new ProductService();
      }
    },
    deps: ["IS_DEV_ENVIRONMENT"]}],

  template: '{{product.title}}'
})
class Product2Component {
  product: Product;

  constructor(productService: ProductService) {
    this.product = productService.getProduct();
  }
}

First you declare a token with an arbitrary name (IS_DEV_ENVIRONMENT in this case) and set it to true to let the program know you’re operating in the development environment (that is, you want to work with the mock product service). The factory uses the arrow expression that will instantiate MockProductService.

The constructor of Product2Component has an argument of type ProductService, and the service will be injected there. You could use such a factory for Product1Component as well; changing the value of IS_DEV_ENVIRONMENT to false would inject the instance of ProductService into both components.

Listing 4.5 isn’t the best solution for switching environments: it reaches out to IS_DEV_ENVIRONMENT, which was declared outside of the component, breaking the component’s encapsulation. You want the component to be self-contained, so let’s try to inject the value of IS_DEV_ENVIRONMENT into the component; that way, it doesn’t need to reach out to the external code.

Declaring a constant (or a variable) isn’t enough to make it injectable. You need to register the value of IS_DEV_ENVIRONMENT with the injector, using provide with useValue, which lets you use it as an injectable parameter in the arrow expression in listing 4.5.

Note

Both useFactory and useValue come from Angular Core. useValue is a special case of useFactory, for when the factory is represented by a single expression and doesn’t need any other dependencies.

For an easy switch between development and other environments, you can specify the environment’s value provider on the root component level, as shown in listing 4.6; then the service factory will know which service to construct. The value of the use-Factory property is a function with two arguments: the factory function itself and its dependencies (deps).

Note

Listing 4.6 and many other code examples in this book use fat-arrow function expressions (described in appendix A). In essence, a fat-arrow function expression is a shorter notation for anonymous functions. For example, (isDev) => {...} is equivalent to function(isDev) {...}.

Listing 4.6. Specifying the environment’s value provider

Because you inject the value into IS_DEV_ENVIRONMENT at the app level, any child component that uses this factory will be affected by a simple switch from false to true.

To recap, a provider maps the token to a class or a factory to let the injector know how to create objects. The class or factory may have its own dependencies, so the providers should specify all of them. Figure 4.8 illustrates the relationships between the providers and the injector in listing 4.6.

Figure 4.8. Binding a factory with dependencies

Angular prepares a tree of providers, finds the injector, and uses it for the Product2Component component. Angular will use either the component’s injector or the parent’s. We’ll discuss the hierarchy of injectors next.

4.4.2. Using OpaqueToken

Injecting into a hard-coded string (such as IS_DEV_ENVIRONMENT) may cause problems if your application has more than one provider that uses a string with the same value for a different purpose. Angular offers an OpaqueToken class that’s preferable to using strings as tokens.

Imagine that you want to create a component that can get data from different servers (such as dev, prod, and QA). The next listing illustrates how you can introduce an injectable value, BackendUrl, as an instance of OpaqueToken rather than as a string.

Listing 4.7. Using OpaqueToken instead of a string
import {Component, OpaqueToken, Inject, NgModule} from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserModule } from '@angular/platform-browser';

export const BackendUrl  = new OpaqueToken('BackendUrl');

@Component({
  selector: 'app',
  template: 'URL: {{url}}'
})
class AppComponent {
  constructor(@Inject(BackendUrl) public url: string) {}
}

@NgModule({
  imports:      [ BrowserModule],
  declarations: [ AppComponent],
  providers: [  {provide:BackendUrl, useValue: 'myQAserver.com'}],
  bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

You wrap the string “BackendUrl” into an instance of OpaqueToken. Then, in the constructor of this component, instead of injecting a vague string type, you inject a concrete BACKEND_URL type with the value provided in the module declaration.

4.5. The hierarchy of injectors

Any Angular application is a tree of nested components. When the web page loads, Angular creates an application object with its injector. It also creates a hierarchy of components with corresponding injectors, according to the application structure. For example, you may want a certain function to be executed when your application is initialized:

{provide:APP_INITIALIZER, useValue: myappInit}

The application’s root component hosts other components. If you include, for example, component B in the template of component A, the latter becomes a parent of the former. In other words, a root component is a parent to other child components, which in turn can have their own children.

Consider the following HTML document, which includes a root component represented by the tag <app>:

<html>
  <body>
    <app></app>
  </body>
</html>

From the following code, you can see that app is a selector of the AppComponent, which is a parent of the <product1> and <product2> components:

@Component({
  selector: 'app',
  template: `
    <product1></product1>
    <product2></product2>
  `
})
class AppComponent {}

The parent component’s injector creates an injector for each child component, so you have a hierarchy of components and a hierarchy of injectors. Also, the template markup of each component can have its own Shadow DOM with elements, and each element gets its own injector. Figure 4.9 shows the hierarchy of injectors.

Figure 4.9. The hierarchy of injectors

When your code creates a component that requires a particular object to be injected, Angular looks for a provider of the requested object at the component level. If found, the component’s injector is used. If it’s not found, Angular checks whether the provider exists on one of the parent components. If the provider for the requested object isn’t found at any level of the injectors hierarchy, Angular will throw an error.

Note

Angular creates an additional injector for a lazy-loaded module. Providers declared in the @NgModule of a lazy-loaded module are available in the module, but not to the entire application.

The example application injects only a service, and it doesn’t illustrate the use of element injectors. In the browser, each component instance can be represented by a Shadow DOM, which has one or more elements depending on what’s defined in the component’s template. Each element in the Shadow DOM has an ElementInjector that follows the same parent-child hierarchy as the DOM elements themselves.

Say you want to add an autocomplete feature to the HTML <input> element component. To do that, you can define a directive as follows:

@Directive({
  selector: '[autocomplete]'
})
class AutoCompleter {
  constructor(element: ElementRef) {
    // Implement the autocomplete logic here
  }
}

The square brackets mean autocomplete can be used as an attribute of the HTML element. The reference to this element will be automatically injected into the constructor of the AutoCompleter class by the element injector.

Now take another look at the code from section 4.4. The Product2Component class had a provider of MockProductService at the component level. The Product1-Component class didn’t specify any providers for the type ProductService, so Angular performed the following actions:

  • Checked its parent AppComponent—no providers there.
  • Checked the AppModule and found providers:[ProductService] there.
  • Used the app-level injector and created an instance of ProductService on the app level.

If you remove the providers line from Product2Component and rerun the application, it’ll still work using the app-level injector and the same instance of the ProductService for both components. If providers for the same token were specified on both parent and child components, and each of these components had a constructor requesting an object represented by the token, two separate instances of such an object would be created: one for the parent and another for the child.

4.5.1. viewProviders

If you want to ensure that a particular injectable service won’t be visible to the component’s children or other components, use the viewProviders property instead of providers. Say you’re writing a reusable library that internally uses a service that you don’t want to be visible from the applications that use this library. Using viewProviders instead of providers will allow you to make such a service private for the library.

Here’s another example. Imagine that you have the following hierarchy of components:

<root>
  <product2>
     <luxury-product></luxury-product>
  </product2>
</root>

Both AppModule and Product2Component have providers defined using the token ProductService, but Product2Component uses a special class that you don’t want to be visible to its children. In this case, you can use the viewProviders property with the Product2Component class; when the injector of LuxuryProductComponent doesn’t find a provider, it’ll go up the hierarchy. It won’t see the provider in Product2Component, and it will use the provider for ProductService defined in RootComponent.

Note

An instance of the injectable object is created and destroyed at the same time as the component that defines the provider for this object.

4.6. Hands-on: using DI in the online auction application

In chapter 3, you added routing to the online auction action so it can render a simplified Product Details view. In this hands-on exercise, you’ll implement the Product-Detail component to show actual product details.

The Home page of the auction is shown in figure 4.10. If you click any of the links, such as First Product or Second Product, the app will show you a pretty basic detail view, as we showed in figure 3.16.

Figure 4.10. The auction Home page

Your goal is to render the details of the selected product, providing yet another illustration of DI in action. Figure 4.11 shows how the Product Details view will look at the end of this hands-on exercise when First Product is selected.

Figure 4.11. The auction Product Details view

Tip

We’ll use the auction application developed in chapter 3 as a starting point for this exercise. If you prefer to see the final version of this project, browse the source code in the auction folder from chapter 4. Otherwise, copy the auction folder from chapter 3 to a separate location, run npm install, and follow the instructions in this section.

Now that you’ve learned about provider and dependency injection, let’s quickly review some code fragments from the auction created in the previous chapter, focusing on the DI-related code. The script in the app.module.ts file specifies the app-level service providers, as shown here:

@NgModule({
    ...
    providers:    [ProductService,
                   {provide: LocationStrategy, useClass: HashLocationStrategy}],
    bootstrap:    [ ApplicationComponent ]
})
export class AppModule { }

Because the ProductService provider is specified in the module, it can be reused by all children of ApplicationComponent. The following fragment from HomeComponent (see home.ts) doesn’t specify providers to be used for the injection of ProductService via the constructor—it reuses the instance of ProductService created in its parent:

@Component({
  selector: 'auction-home-page',
  styleUrls: ['/home.css'],
  template: `...`
})
export default class HomeComponent {
  products: Product[] = [];

  constructor(private productService: ProductService) {
    this.products = this.productService.getProducts();
  }
}

As soon as HomeComponent is instantiated, ProductService is injected, and its getProducts() method populates the products array, which is bound to the view.

The HTML fragment that displays the content of this array uses the *ngFor loop to display one <auction-product-item> template for each element of the array:

<div class="row">
  <div *ngFor="let product of products" class="col-sm-4 col-lg-4 col-md-4">
    <auction-product-item [product]="product"></auction-product-item>
  </div>
</div>

The template for <auction-product-item> contains the following line:

<h4><a [routerLink]="['/products', product.title]">{{ product.title }}</a>
 </h4>

Clicking this link instructs the router to render ProductDetailComponent and provides the value of product.title as the route parameter. You want to modify this code to pass the product ID instead of the title.

This brief overview of the existing code was intended to remind you how the Product Details page is requested. Next, let’s implement the code to produce the view shown in figure 4.11.

4.6.1. Changing the code to pass the product ID as a parameter

Open the product-item.html file, and modify the line with [routerLink] so it looks like this:

<h4><a [routerLink]="['/products', product.id]">{{ product.title }}</a></h4>

The product-item.html file contains the template used to display products in the Home view. Now, clicking the product title will pass the product.id to the route configured for the path products.

4.6.2. Modifying ProductDetailComponent

Before you begin coding, look at figure 4.12, which shows the parent-child relationship between the components of the auction. Understanding parent-child relations can help you decide whether some of the parent injectors can be reused by their children.

Figure 4.12. Parent-child relations in the auction

In chapter 3, you injected an instance of ProductService in HomeComponent, but you’ll need it in ProductDetailComponent as well. You can define the provider of ProductService during the bootstrap of the application to make it available in all children of ApplicationComponent. To do so, follow these steps:

  1. Modify the code in app.module.ts to change the route configuration from products/:prodTitle to products/:productId. The first lines of the @NgModule decorator should look like this.
    Listing 4.8. Modifications in app.module.ts
    @NgModule({
        imports:[ BrowserModule,
                  RouterModule.forRoot([
                    {path: '',  component: HomeComponent},
                    {path: 'products/:productId',
                          component: ProductDetailComponent}
        ]) ],
    Because you’re passing the product ID to ProductDetailComponent, its code should be modified accordingly.
  2. Open the product-detail.ts file, and modify its code as shown next.
    Listing 4.9. Modifications in product-detail.ts
    import {Component} from '@angular/core';
    import { ActivatedRoute} from '@angular/router';
    import {Product, Review, ProductService} from
     '../../services/product-service';
    
    @Component({
      selector: 'auction-product-page',
      templateUrl: 'app/components/product-detail/product-detail.html'
    })
    export default class ProductDetailComponent {
      product: Product;
      reviews: Review[];
    
      constructor(route: ActivatedRoute, productService: ProductService) {
    
        let prodId: number = parseInt(route.snapshot.params['productId']);
        this.product = productService.getProductById(prodId);
    
        this.reviews = productService.getReviewsForProduct(this.product.id);
      }
    }
    Angular will inject the ProductService instance into ProductDetailComponent. When ProductDetailComponent is created, it invokes the getProductsById() method, which returns one product with an id that matches the productId passed from the Home view via the constructor’s argument of type Activated-Route. This is how you populate the product variable. Then the constructor calls the getReviewsForProduct() method to populate the reviews array. You’ll see the declaration of this method as well as the Review class later in this section.
  3. Create the following product-detail.html file in the product-detail folder.
    Listing 4.10. product-detail.html
    <div class="thumbnail">
        <img src="http://placehold.it/820x320">
        <div>
            <h4 class="pull-right">{{ product.price }}</h4>
            <h4>{{ product.title }}</h4>
            <p>{{ product.description }}</p>
        </div>
        <div class="ratings">
            <p class="pull-right">{{ reviews.length }} reviews</p>
            <p><auction-stars [rating]="product.rating"></auction-stars></p>
        </div>
    </div>
    <div class="well" id="reviews-anchor">
        <div class="row">
            <div class="col-md-12"></div>
        </div>
         <div class="row" *ngFor="let review of reviews">
            <hr>
            <div class="col-md-12">
                <auction-stars [rating]="review.rating"></auction-stars>
                <span>{{ review.user }}</span>
                <span class="pull-right">
                 {{ review.timestamp | date: 'shortDate' }}</span>
                <p>{{ review.comment }}</p>
            </div>
        </div>
    </div>
    This HTML template uses local binding to the properties of the product variable. Note how you use square brackets to pass the rating input to Stars-Component (represented by <auction-stars>), introduced in chapter 2. In this version of the auction, the user can only see the reviews; you’ll implement the Leave a Review functionality in chapter 6. The pipe operator (|) allows you to create filters that can transform a value. The expression review.timestamp | date: 'shortDate' takes the timestamp from a Review object and displays it in a shortDate form. You can find other date formats in the Angular documentation at http://mng.bz/CX8F. Angular comes with several classes that can be used with the pipe operator, and you can create custom filters (explained in chapter 5). In chapter 8, you’ll see how to use the async pipe to automatically unwrap the server’s responses.
  4. To save you some typing, copy into your project the app/services/product-service.ts file provided with the code of the auction application for this chapter. This file contains three classes—Product, Review, and ProductService—and hard-coded data for products and reviews. The HTML template from listing 4.10 uses the following Product and Review classes.
    Listing 4.11. Product and Review classes
    export class Product {
      constructor(
        public id: number,
        public title: string,
        public price: number,
        public rating: number,
        public description: string,
        public categories: string[]) {
      }
    }
    
    export class Review {
      constructor(
        public id: number,
        public productId: number,
        public timestamp: Date,
        public user: string,
        public rating: number,
        public comment: string) {
      }
    }
    The ProductService class is shown in the next listing.
    Listing 4.12. ProductService class
    export class ProductService {
      getProducts(): Product[] {
        return products.map(p => new Product(p.id, p.title, p.price, p.rating,
         p.description, p.categories));
      }
    
      getProductById(productId: number): Product {
        return products.find(p => p.id === productId);
      }
    
      getReviewsForProduct(productId: number): Review[] {
        return reviews
          .filter(r => r.productId === productId)
          .map(r => new Review(r.id, r.productId, Date.parse(r.timestamp),
           r.user, r.rating, r.comment));
      }
    }
    
    var products = [
      {
        "id": 0,
        "title": "First Product",
        "price": 24.99,
        "rating": 4.3,
        "description": "This is a short description. Lorem ipsum dolor sit
         amet, consectetur adipiscing elit.",
        "categories": ["electronics", "hardware"]},
      {
        "id": 1,
        "title": "Second Product",
        "price": 64.99,
        "rating": 3.5,
        "description": "This is a short description. Lorem ipsum dolor sit
         amet, consectetur adipiscing elit.",
        "categories": ["books"]}];
    
    var reviews = [
      {
        "id": 0,
        "productId": 0,
        "timestamp": "2014-05-20T02:17:00+00:00",
        "user": "User 1",
        "rating": 5,
        "comment": "Aenean vestibulum velit id placerat posuere. Praesent..."},
      {
        "id": 1,
        "productId": 0,
        "timestamp": "2014-05-20T02:53:00+00:00",
        "user": "User 2",
        "rating": 3,
        "comment": "Aenean vestibulum velit id placerat posuere. Praesent... "
      }];
    This class has three methods: getProducts(), which returns an array of Product objects; getProductById(), which returns one product; and getReviewsForProduct(), which returns an array of Review objects for the selected product. All the data for products and reviews is hard-coded in the products and reviews arrays, respectively. (For brevity, we’ve shown fragments of these arrays.) The getReviewsForProduct() method filters the reviews array to find reviews for the specified productId. Then it uses the map() function to turn an array of Object elements into a new array of Review objects.
    Using the ES6 API while compiling into ES5 syntax

    If your IDE shows the find() function in red, it’s because your tsconfig.json file specifies ES5 as a target for compilation, and find() wasn’t supported in ES5 arrays. To remove the red, you can install the type definition file for ES6 shim:

    npm i @types/es6-shim --save-dev

    For details, see section B.10.1.

  5. Start the server in the auction directory by entering the command npm start. When you see the auction’s home page, click the product title to see the Product Details view shown in figure 4.11.

4.7. Summary

In this chapter, you’ve learned what the Dependency Injection pattern is and how Angular implements it. The online auction will use DI on every page. These are the main takeaways from this chapter:

  • Providers register objects for future injection.
  • You can create a provider not only for an object, but for a string value as well.
  • Injectors form a hierarchy, and if Angular can’t find the provider for the requested type at the component level, it’ll try to find it by traversing parent injectors.
  • The value of the providers property is visible in the child components, whereas viewProviders is only visible at the component level.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.252.87