In the previous chapter, I introduced services and explained how they are distributed using dependency injection. When using dependency injection, the objects that are used to resolve dependencies are created by service providers, known more commonly as providers. In this chapter, I explain how providers work, describe the different types of providers available, and demonstrate how providers can be created in different parts of the application to change the way that services behave. Table 20-1 puts providers in context.
Table 20-1.
Putting Service Providers in Context
Question
Answer
What are they?
Providers are classes that create service objects the first time that Angular needs to resolve a dependency.
Why are they useful?
Providers allow the creation of service objects to be tailored to the needs of the application. The simplest provider just creates an instance of a specified class, but there are other providers that can be used to tailor the way that service objects are created and configured.
How are they used?
Providers are defined in the providers property of the Angular module’s decorator. They can also be defined by components and directives to provide services to their children, as described in the “Using Local Providers” section.
Are there any pitfalls or limitations?
It is easy to create unexpected behavior, especially when working with local providers. If you encounter problems, check the scope of the local providers you have created and make sure that your dependencies and providers are using the same tokens.
Are there any alternatives?
Many applications will require only the basic dependency injection features described in Chapter 19. You should use the features in this chapter only if you cannot build your application using the basic features and only if you have a solid understanding of dependency injection.
Why You Should Consider Skipping this Chapter
Dependency injection provokes strong reactions in developers and polarizes opinion. If you are new to dependency injection and have yet to form your own opinion, then you might want to skip this chapter and just use the features that I described in Chapter 19. That’s because features like the ones I describe in this chapter are exactly why many developers dread using dependency injection and form a strong preference against its use.
The basic Angular dependency injection features are easy to understand and have an immediate and obvious benefit in making applications easier to write and maintain. The features described in this chapter provide fine-grained control over how dependency injection works, but they also make it possible to sharply increase the complexity of an Angular application and, ultimately, undermine many of the benefits that the basic features offer.
If you decide that you want all of the gritty detail, then read on. But if you are new to the world of dependency injection, you may prefer to skip this chapter until you find that the basic features from Chapter 19 don’t deliver the functionality you require.
As with the other chapters in this part of the book, I am going to continue working with the project created in Chapter 11 and most recently modified in Chapter 19. To prepare for this chapter, I added a file called log.service.ts to the src/app folder and used it to define the service shown in Listing 20-1.
Tip
You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/pro-angular-9. See Chapter 1 for how to get help if you have problems running the examples.
The Contents of the log.service.ts File in the src/app Folder
This service writes out log messages, with differing levels of severity, to the browser’s JavaScript console. I will register and use this service later in the chapter.
When you have created the service and saved the changes, run the following command in the example folder to start the Angular development tools:
ng serve
Open a new browser window and navigate to http://localhost:4200 to see the application, as shown in Figure 20-1.
Using Service Providers
As I explained in the previous chapters, classes declare dependencies on services using their constructor arguments. When Angular needs to create a new instance of the class, it inspects the constructor and uses a combination of built-in and custom services to resolve each argument. Listing 20-2 updates the DiscountService class so that it depends on the LogService class created in the previous section.
Creating a Dependency in the discount.service.ts File in the src/app Folder
The changes in Listing 20-2 prevent the application from running. Angular processes the HTML document and starts creating the hierarchy of components, each with their templates that require directives and data bindings, and it encounters the classes that depend on the DiscountService class. But it can’t create an instance of DiscountService because its constructor requires a LogService object, and it doesn’t know how to handle this class.
When you save the changes in Listing 20-2, you will see an error like this one in the browser’s JavaScript console:
NullInjectorError: No provider for LogService!
Angular delegates responsibility for creating the objects needed for dependency injection to providers, each of which managed a single type of dependency. When it needs to create an instance of the DiscountService class, it looks for a suitable provider to resolve the LogService dependency. Since there is no such provider, Angular can’t create the objects it needs to start the application and reports the error.
The simplest way to create a provider is to add the service class to the array assigned to the Angular module’s providers property, as shown in Listing 20-3. (I have taken the opportunity to remove some of the statements that are no longer required in the module.)
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
Creating a Provider in the app.module.ts File in the src/app Folder
When you save the changes, you will have defined the provider that Angular requires to handle the LogService dependency, and you will see messages like this one shown in the browser’s JavaScript console:
Message (INFO): Discount 10 applied to price: 16
You might wonder why the configuration step in Listing 20-3 is required. After all, Angular could just assume that it should create a new LogService object the first time it needs one.
In fact, Angular provides a range of different providers, each of which creates objects in a different way to let you take control of the service creation process. Table 20-3 describes the set of providers that are available, which are described in the sections that follow.
Table 20-3.
The Angular Providers
Name
Description
Class provider
This provider is configured using a class. Dependencies on the service are resolved by an instance of the class, which Angular creates.
Value provider
This provider is configured using an object, which is used to resolve dependencies on the service.
Factory provider
This provider is configured using a function. Dependencies on the service are resolved using an object that is created by invoking the function.
Existing service provider
This provider is configured using the name of another service and allows aliases for services to be created.
Using the Class Provider
This provider is the most commonly used and is the one I applied by adding the class names to the module’s providers property in Listing 20-3. This listing shows the shorthand syntax, and there is also a literal syntax that achieves the same result, as shown in Listing 20-4.
Using the Class Provider Literal Syntax in the app.module.ts File in the src/app Folder
Providers are defined as classes, but they can be specified and configured using the JavaScript object literal format, like this:
...
{ provide: LogService, useClass: LogService }
...
The class provider supports three properties, which are described in Table 20-4 and explained in the sections that follow.
Table 20-4.
The Class Provider’s Properties
Name
Description
provide
This property is used to specify the token, which is used to identify the provider and the dependency that will be resolved. See the “Understanding the Token” section.
useClass
This property is used to specify the class that will be instantiated to resolve the dependency by the provider. See the “Understanding the useClass Property” section.
multi
This property can be used to deliver an array of service objects to resolve dependencies. See the “Resolving a Dependency with Multiple Objects” section.
Understanding the Token
All providers rely on a token, which Angular uses to identify the dependency that the provider can resolve. The simplest approach is to use a class as the token, which is what I did in Listing 20-4. However, you can use any object as the token, which allows the dependency and the type of the object to be separated. This has the effect of increasing the flexibility of the dependency injection configuration because it allows a provider to supply objects of different types, which can be useful with some of the more advanced providers described later in the chapter. As a simple example, Listing 20-5 uses the class provider to register the log service created at the start of the chapter using a string as a token, rather than a class.
Registering a Service with a Token in the app.module.ts File in the src/app Folder
In the listing, the provide property of the new provider is set to logger. Angular will automatically match providers whose token is a class, but it needs some additional help for other token types. Listing 20-6 shows the DiscountService class updated with a dependency on the logging service, accessed using the logger token.
import { Injectable, Inject } from "@angular/core";
Using a String Provider Token in the discount.service.ts File in the src/app Folder
The @Inject decorator is applied to the constructor argument and used to specify the token that should be used to resolve the dependency. When Angular needs to create an instance of the DiscountService class, it will inspect the constructor and use the @Inject decorator argument to select the provider that will be used to resolve the dependency, resolving the dependency on the LogService class.
Using Opaque Tokens
When using simple types as provider tokens, there is a chance that two different parts of the application will try to use the same token to identify different services, which means that the wrong type of object may be used to resolve dependencies and cause errors.
To help work around this, Angular provides the InjectionToken class, which provides an object wrapper around a string value and can be used to create unique token values. In Listing 20-7, I have used the InjectionToken class to create a token that will be used to identify dependencies on the LogService class.
import { Injectable, InjectionToken } from "@angular/core";
export const LOG_SERVICE = new InjectionToken("logger");
export enum LogLevel {
DEBUG, INFO, ERROR
}
@Injectable()
export class LogService {
minimumLevel: LogLevel = LogLevel.INFO;
// ...methods omitted for brevity...
}
Listing 20-7.
Using the InjectionToken Class in the log.service.ts File in the src/app Folder
The constructor for the InjectionToken class accepts a string value that describes the service, but it is the InjectionToken object that will be the token. Dependencies must be declared on the same InjectionToken that is used to create the provider in the module; this is why the token has been created using the const keyword, which prevents the object from being modified. Listing 20-8 shows the provider configuration using the new token.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
import { Model } from "./repository.model";
import { LogService, LOG_SERVICE } from "./log.service";
Declaring a Dependency in the discount.service.ts File in the src/app Folder
There is no difference in the functionality offered by the application, but using the InjectionToken means that there will be no confusion between services.
Understanding the useClass Property
The class provider’s useClass property specifies the class that will be instantiated to resolve dependencies. The provider can be configured with any class, which means you can change the implementation of a service by changing the provider configuration. This feature should be used with caution because the recipients of the service object will be expecting a specific type and a mismatch won’t result in an error until the application is running in the browser. (TypeScript type enforcement has no effect on dependency injection because it occurs at runtime after the type annotations have been processed by the TypeScript compiler.)
The most common way to change classes is to use different subclasses. In Listing 20-10, I extended the LogService class to create a service that writes a different format of message in the browser’s JavaScript console.
import { Injectable, InjectionToken } from "@angular/core";
export const LOG_SERVICE = new InjectionToken("logger");
Creating a Subclassed Service in the log.service.ts File in the src/app Folder
The SpecialLogService class extends LogService and provides its own implementation of the logMessage method. Listing 20-11 updates the provider configuration so that the useClass property specifies the new service.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
import { Model } from "./repository.model";
import { LogService, LOG_SERVICE, SpecialLogService } from "./log.service";
Configuring the Provider in the app.module.ts File in the src/app Folder
The combination of token and class means that dependencies on the LOG_SERVICE opaque token will be resolved using a SpecialLogService object. When you save the changes, you will see messages like this one displayed in the browser’s JavaScript console, indicating that the derived service has been used:
Special Message (INFO): Discount 10 applied to price: 275
Care must be taken when setting the useClass property to specify a type that the dependent classes are expecting. Specifying a subclass is the safest option because the functionality of the base class is guaranteed to be available.
Resolving a Dependency with Multiple Objects
The class provider can be configured to deliver an array of objects to resolve a dependency, which can be useful if you want to provide a set of related services that differ in how they are configured. To provide an array, multiple class providers are configured using the same token and with the multi property set as true, as shown in Listing 20-12.
Configuring Multiple Service Objects in the app.module.ts File in the src/app Folder
The Angular dependency injection system will resolve dependencies on the LOG_SERVICE token by creating LogService and SpecialLogService objects, placing them in an array, and passing them to the dependent class’s constructor. The class that receives the services must be expecting an array, as shown in Listing 20-13.
import { Injectable, Inject } from "@angular/core";
import { LogService, LOG_SERVICE, LogLevel } from "./log.service";
Receiving Multiple Services in the discount.service.ts File in the src/app Folder
The services are received as an array by the constructor, which uses the array’s find method to locate the first logger whose minimumLevel property is LogLevel.Debug and assign it to the logger property. The applyDiscount method calls the service’s logDebugMessage method, which results in messages like this one being displayed in the browser’s JavaScript console:
Special Message (INFO): Discount 10 applied to price: 275
Using the Value Provider
The value provider is used when you want to take responsibility for creating the service objects yourself, rather than leaving it to the class provider. This can also be useful when services are simple types, such as string or number values, which can be a useful way of providing access to common configuration settings. The value provider can be applied using a literal object and supports the properties described in Table 20-5.
Table 20-5.
The Value Provider Properties
Name
Description
provide
This property defines the service token, as described in the “Understanding the Token” section earlier in the chapter.
useValue
This property specifies the object that will be used to resolve the dependency.
multi
This property is used to allow multiple providers to be combined to provide an array of objects that will be used to resolve a dependency on the token. See the “Resolving a Dependency with Multiple Objects” section earlier in the chapter for an example.
The value provider works in the same way as the class provider except that it is configured with an object rather than a type. Listing 20-14 shows the use of the value provider to create an instance of the LogService class that is configured with a specific property value.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
import { Model } from "./repository.model";
import { LogService, LOG_SERVICE, SpecialLogService, LogLevel } from "./log.service";
Using the Value Provider in the app.module.ts File in the src/app Folder
This value provider is configured to resolve dependencies on the LogService token with a specific object that has been created and configured outside of the module class.
The value provider—and, in fact, all of the providers—can use any object as the token, as described in the previous section, but I have returned to using types as tokens because it is the most commonly used technique and because it works so nicely with TypeScript constructor parameter typing. Listing 20-15 shows the corresponding change to the DiscountService, which declares a dependency using a typed constructor argument.
import { Injectable, Inject } from "@angular/core";
import { LogService, LOG_SERVICE, LogLevel } from "./log.service";
Declaring a Dependency Using a Type in the discount.service.ts File in the src/app Folder
Using the Factory Provider
The factory provider uses a function to create the object required to resolve a dependency. This provider supports the properties described in Table 20-6.
Table 20-6.
The Factory Provider Properties
Name
Description
provide
This property defines the service token, as described in the “Understanding the Token” section earlier in the chapter.
deps
This property specifies an array of provider tokens that will be resolved and passed to the function specified by the useFactory property.
useFactory
This property specifies the function that will create the service object. The objects produced by resolving the tokens specified by the deps property will be passed to the function as arguments. The result returned by the function will be used as the service object.
multi
This property is used to allow multiple providers to be combined to provide an array of objects that will be used to resolve a dependency on the token. See the “Resolving a Dependency with Multiple Objects” section earlier in the chapter for an example.
This is the provider that gives the most flexibility in how service objects are created because you can define functions that are tailored to your application’s requirements. Listing 20-16 shows a factory function that creates LogService objects.
Using the Factory Provider in the app.module.ts File in the src/app Folder
The function in this example is simple: it receives no arguments and just creates a new LogService object. The real flexibility of this provider comes when the deps property is used, which allows for dependencies to be created on other services. In Listing 20-17, I have defined a token that specifies a debugging level.
import { Injectable, InjectionToken } from "@angular/core";
export const LOG_SERVICE = new InjectionToken("logger");
export const LOG_LEVEL = new InjectionToken("log_level");
export enum LogLevel {
DEBUG, INFO, ERROR
}
@Injectable()
export class LogService {
minimumLevel: LogLevel = LogLevel.INFO;
// ...methods omitted for brevity...
}
@Injectable()
export class SpecialLogService extends LogService {
// ...methods omitted for brevity...
}
Listing 20-17.
Defining a Logging-Level Service in the log.service.ts File in the src/app Folder
In Listing 20-18, I have defined a value provider that creates a service using the LOG_LEVEL token and used that service in the factory function that creates the LogService object.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
Using Factory Dependencies in the app.module.ts File in the src/app Folder
The LOG_LEVEL token is used by a value provider to define a simple value as a service. The factory provider specifies this token in its deps array, which the dependency injection system resolves and provides as an argument to the factory function, which uses it to set the minimumLevel property of a new LogService object.
Using the Existing Service Provider
This provider is used to create aliases for services so they can be targeted using more than one token, using the properties described in Table 20-7.
Table 20-7.
The Existing Provider Properties
Name
Description
provide
This property defines the service token, as described in the “Understanding the Token” section earlier in the chapter.
useExisting
This property is used to specify the token of another provider, whose service object will be used to resolve dependencies on this service.
multi
This property is used to allow multiple providers to be combined to provide an array of objects that will be used to resolve a dependency on the token. See the “Resolving a Dependency with Multiple Objects” section earlier in the chapter for an example.
This provider can be useful when you want to refactor the set of providers but don’t want to eliminate all the obsolete tokens to avoid refactoring the rest of the application. Listing 20-19 shows the use of this provider.
Creating a Service Alias in the app.module.ts File in the src/app Folder
The token for the new service is the string debugLevel, and it is aliased to the provider with the LOG_LEVEL token. Using either token will result in the dependency being resolved with the same value.
Using Local Providers
When Angular creates a new instance of a class, it resolves any dependencies using an injector. It is an injector that is responsible for inspecting the constructors of classes to determine what dependencies have been declared and resolving them using the available providers.
So far, all the dependency injection examples have relied on providers configured in the application’s Angular module. But the Angular dependency injection system is more complex: there is a hierarchy of injectors corresponding to the application’s tree of components and directives. Each component and directive can have its own injector, and each injector can be configured with its own set of providers, known as local providers.
When there is a dependency to resolve, Angular uses the injector for the nearest component or directive. The injector first tries to resolve the dependency using its own set of local providers. If no local providers have been set up or there are no providers that can be used to resolve this specific dependency, then the injector consults the parent component’s injector. The process is repeated—the parent component’s injector tries to resolve the dependency using its own set of local providers. If a suitable provider is available, then it is used to provide the service object required to resolve the dependency. If there is no suitable provider, then the request is passed up to the next level in the hierarchy to the grandparent of the original injector. At the top of the hierarchy is the root Angular module, whose providers are the last resort before reporting an error.
Defining providers in the Angular module means that all dependencies for a token within the application will be resolved using the same object. As I explain in the following sections, registering providers further down the injector hierarchy can change this behavior and alter the way that services are created and used.
Understanding the Limitations of Single Service Objects
Using a single service object can be a powerful tool, allowing building blocks in different parts of the application to share data and respond to user interactions. But some services don’t lend themselves to being shared so widely. As a simple example, Listing 20-20 adds a dependency on LogService to one of the pipes created in Chapter 18.
import { Pipe, Injectable } from "@angular/core";
import { DiscountService } from "./discount.service";
Adding a Service Dependency in the discount.pipe.ts File in the src/app Folder
The pipe’s transform method uses the LogService object, which is received as a constructor argument, to generate logging messages when the price value it transforms is greater than 100.
The problem is that these log messages are drowned out by the messages generated by the DiscountService object, which creates a message every time a discount is applied. The obvious thing to do is to change the minimum level in the LogService object when it is created by the module provider’s factory function, as shown in Listing 20-21.
Changing the Logging Level in the app.module.ts File in the src/app Folder
Of course, this doesn’t have the desired effect because the same LogService object is used throughout the application and filtering the DiscountService messages means that the pipe messages are filtered too.
I could enhance the LogService class so there are different filters for each source of logging messages, but that quickly becomes complicated. Instead, I am going to solve the problem by creating a local provider so that there are multiple LogService objects in the application, each of which can then be configured separately.
Creating Local Providers in a Component
Components can define local providers, which allow separate servers to be created and used by part of the application. Components support two decorator properties for creating local providers, as described in Table 20-8.
Table 20-8.
The Component Decorator Properties for Local Providers
Name
Description
providers
This property is used to create a provider used to resolve dependencies of view and content children.
viewProviders
This property is used to create a provider used to resolve dependencies of view children.
The simplest way to address my LogService issue is to use the providers property to set up a local provider, as shown in Listing 20-22.
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { DiscountService } from "./discount.service";
import { LogService } from "./log.service";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html",
providers:[LogService]
})
export class ProductTableComponent {
constructor(private dataModel: Model) { }
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
taxRate: number = 0;
dateObject: Date = new Date(2020, 1, 20);
dateString: string = "2020-02-20T00:00:00.000Z";
dateNumber: number = 1582156800000;
selectMap = {
"Watersports": "stay dry",
"Soccer": "score goals",
"other": "have fun"
}
numberMap = {
"=1": "one product",
"=2": "two products",
"other": "# products"
}
}
Listing 20-22.
Creating a Local Provider in the productTable.component.ts File in the src/app Folder
When Angular needs to create a new pipe object, it detects the dependency on LogService and starts working its way up the application hierarchy, examining each component it finds to determine whether they have a provider that can be used to resolve the dependency. The ProductTableComponent does have a LogService provider, which is used to create the service used to resolve the pipe’s dependency. This means there are now two LogService objects in the application, each of which can be configured separately, as shown in Figure 20-2.
The LogService object created by the component’s provider uses the default value for its minimumLevel property and will display LogLevel.INFO messages. The LogService object created by the module, which will be used to resolve all other dependencies in the application, including the one declared by the DiscountService class, is configured so that it will display only LogLevel.ERROR messages. When you save the changes, you will see the logging messages from the pipe (which receives the service from the component) but not from DiscountService (which receives the service from the module).
Understanding the Provider Alternatives
As described in Table 20-8, there are two properties that can be used to create local providers. To demonstrate how these properties differ, I added a file called valueDisplay.directive.ts to the src/app folder and used it to define the directive shown in Listing 20-23.
import { Directive, InjectionToken, Inject, HostBinding} from "@angular/core";
export const VALUE_SERVICE = new InjectionToken("value_service");
The Contents of the valueDisplay.directive.ts File in the src/app Folder
The VALUE_SERVICE opaque token will be used to define a value-based service, on which the directive in this listing declares a dependency so that it can be displayed in the host element’s content. Listing 20-24 shows the service being defined and the directive being registered in the Angular module. I have also simplified the LogService provider in the module for brevity.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
import { PaDiscountDisplayComponent } from "./discountDisplay.component";
import { PaDiscountEditorComponent } from "./discountEditor.component";
import { DiscountService } from "./discount.service";
import { PaDiscountPipe } from "./discount.pipe";
import { PaDiscountAmountDirective } from "./discountAmount.directive";
import { SimpleDataSource } from "./datasource.model";
Registering the Directive and Service in the app.module.ts File in the src/app Folder
The provider sets up a value of Apples for the VALUE_SERVICE service. The next step is to apply the new directive so there is an instance that is a view child of a component and another that is a content child. Listing 20-25 sets up the content child instance.
<div class="row m-2">
<div class="col-4 p-2">
<paProductForm>
<span paDisplayValue></span>
</paProductForm>
</div>
<div class="col-8 p-2">
<paProductTable></paProductTable>
</div>
</div>
Listing 20-25.
Applying a Content Child Directive in the template.html File in the src/app Folder
Listing 20-26 projects the host element’s content and adds a view child instance of the new directive.
Adding Directives in the productForm.component.html File in the src/app Folder
When you save the changes, you will see the new elements, as shown in Figure 20-3, both of which show the same value because the only provider for VALUE_SERVICE is defined in the module.
Creating a Local Provider for All Children
The @Component decorator’s providers property is used to define providers that will be used to resolve service dependencies for all children, regardless of whether they are defined in the template (view children) or projected from the host element (content children). Listing 20-27 defines a VALUE_SERVICE provider in the parent component for two new directive instances.
import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core";
import { Product } from "./product.model";
import { Model } from "./repository.model";
import { VALUE_SERVICE } from "./valueDisplay.directive";
Defining a Provider in the productForm.component.ts File in the src/app Folder
The new provider changes the service value. When Angular comes to create the instances of the new directive, it begins its search for providers by working its way up the application hierarchy and finds the VALUE_SERVICE provider defined in Listing 20-27. The service value is used by both instances of the directive, as shown in Figure 20-4.
Creating a Provider for View Children
The viewProviders property defines providers that are used to resolve dependencies for view children but not content children. Listing 20-28 uses the viewProviders property to define a provider for VALUE_SERVICE.
import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core";
import { Product } from "./product.model";
import { Model } from "./repository.model";
import { VALUE_SERVICE } from "./valueDisplay.directive";
Defining a View Child Provider in the productForm.component.ts File in the src/app Folder
Angular uses the provider when resolving dependencies for view children but not for content children. This means dependencies for content children are referred up the application’s hierarchy as though the component had not defined a provider. In the example, this means that the view child will receive the service created by the component’s provider, and the content child will receive the service created by the module’s provider, as shown in Figure 20-5.
Caution
Defining providers for the same service using both the providers and viewProviders properties is not supported. If you do this, the view and content children both will receive the service created by the viewProviders provider.
Controlling Dependency Resolution
Angular provides three decorators that can be used to provide instructions about how a dependency is resolved. These decorators are described in Table 20-9 and demonstrated in the following sections.
Table 20-9.
The Dependency Resolution Decorators
Name
Description
@Host
This decorator restricts the search for a provider to the nearest component.
@Optional
This decorator stops Angular from reporting an error if the dependency cannot be resolved.
@SkipSelf
This decorator excludes the providers defined by the component/directive whose dependency is being resolved.
Restricting the Provider Search
The @Host decorator restricts the search for a suitable provider so that it stops once the closest component has been reached. The decorator is typically combined with @Optional, which prevents Angular from throwing an exception if a service dependency cannot be resolved. Listing 20-29 shows the addition of both decorators to the directive in the example.
import { Directive, InjectionToken, Inject,
HostBinding, Host, Optional} from "@angular/core";
export const VALUE_SERVICE = new InjectionToken("value_service");
Adding Dependency Decorators in the valueDisplay.directive.ts File in the src/app Folder
When using the @Optional decorator, you must ensure that the class is able to function if the service cannot be resolved, in which case the constructor argument for the service is undefined. The nearest component defines a service for its view children but not content children, which means that one instance of the directive will receive a service object and the other will not, as illustrated in Figure 20-6.
Skipping Self-Defined Providers
By default, the providers defined by a component are used to resolve its dependencies. The @SkipSelf decorator can be applied to constructor arguments to tell Angular to ignore the local providers and start the search at the next level in the application hierarchy, which means that the local providers will be used only to resolve dependencies for children. In Listing 20-30, I have added a dependency on the VALUE_SERVICE provider that is decorated with @SkipSelf.
Skipping Local Providers in the productForm.component.ts File in the src/app Folder
When you save the changes and the browser reloads the page, you will see the following message in the browser’s JavaScript console, showing that the service value defined locally (Oranges) has been skipped and allowing the dependency to be resolved by the Angular module:
Service Value:pples
Summary
In this chapter, I explained the role that providers play in dependency injection and explained how they can be used to change how services are used to resolve dependencies. I described the different types of providers that can be used to create service objects and demonstrated how directives and components can define their own providers to resolve their own dependencies and those of their children. In the next chapter, I describe modules, which are the final building block for Angular applications.