Providers
In the previous examples, you should have noticed that to import and use a class decorated with @Injectable
one needs to declare its Type in the providers
array of the main application Module.
That makes Angular "know" about the services when instantiating components for your web application.
You can have numerous services that perform various sets of functionality, all registered within the root module:
@NgModule({
// ...,
providers: [
...
LogService,
AuthenticationService,
AvatarService,
UserService,
ChatService
],
// ...
})
export class AppModule { }
The concept of providers
in Angular goes beyond the collection of classes, in fact,
it supports several powerful ways to control how dependency injection behaves at runtime.
Besides strings, the framework supports an object-based notation for defining providers.
Using a Class
Earlier in this chapter, we have been using a string value to define a LogService dependency in the providers
section.
We can express the same value with the help of the following notation:
{ provide: <key>, useClass: <class> }
Let's take a look at the next example:
@NgModule({
// ...
providers: [
{ provide: LogService, useClass: LogService }
],
//...
})
export class AppModule { }
We are using LogService
both as a "key" for injection and as a "value" to build a new instance for injection.
The main feature of this approach is that "key" and "value" can be different classes. That allows swapping the value of the service with a custom implementation if needed, or with a Mock object for an improved unit testing experience.
Now, let's create a custom logger implementation CustomLogService
:
ng g service services/custom-log
Next, implement the info
method, but this time it should contain some different output for us to distinguish both implementations:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CustomLogService {
constructor() { }
info(message: string) {
console.log(`[custom]: [info] ${message}`);
}
}
Finally, you need to import this class into the main application module and declare as a new provider with the "LogService" key. Don't forget to comment out or remove the former logger declaration as in the example below:
import { LogService } from './services/log.service';
import { CustomLogService } from './services/custom-log.service';
@NgModule({
// ...,
providers: [
// LogService
{ provide: LogService, useClass: CustomLogService }
],
// ...
})
export class AppModule { }
The code above means that all the components that inject the LogService
as part of the constructor parameters
are going to receive the CustomLogService
implementation at runtime.
Essentially we are swapping the value of the logger, and no component is going to notice that. That is the behavior developers often use for unit testing purposes.
If you now run the web application and navigate to browser console, you should see the following output:
[custom]: [info] Component 1 created
[custom]: [info] Component 2 created
Strings now contain "[custom]: " as a prefix, which proves the Component1
and Component2
are now dealing with the CustomLogService
code that has been successfully injected using the LogService
key.
Using a Class Factory
Previously we have been relying on the Angular framework to create instances of the injectable entities.
There is also a possibility to control how the class gets instantiated if you need more than just a default constructor calls. Angular provides support for "class factories" for that very purpose.
You are going to use the following notation for class factories:
{ provide: <key>, useFactory: <function> }
Before we jump into configuration details, let's extend our newly introduced CustomLogService
service with the custom "prefix" support.
We are going to implement a special setPrefix
method:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CustomLogService {
private prefix = '[custom]';
setPrefix(value: string) {
this.prefix = value;
}
info(message: string) {
console.log(`${this.prefix}: [info] ${message}`);
}
}
As you can see from the code above the info
method is going to use a custom prefix for all the messages.
Next, create an exported function customLogServiceFactory
that is going to control how the CustomLogService instance gets created.
In our case we are going to provide a custom prefix like in the example below:
export function customLogServiceFactory() {
const service = new CustomLogService();
service.setPrefix('(factory demo)');
return service;
}
As you can imagine, there could be more sophisticated configuration scenarios for all application building blocks, including services, components, directives and pipes.
Finally, you can use the factory function for the LogService
.
In this case, we both replace the real instance with the CustomLogService
, and pre-configure the latter with a custom prefix for info messages:
@NgModule({
// ...
providers: [
{ provide: LogService, useFactory: customLogServiceFactory }
],
// ...
})
export class AppModule { }
This time, when the application runs, you should see the following output:
(factory demo): [info] Component 1 created
(factory demo): [info] Component 2 created
Class Factories With Dependencies
When you use the default provider registration for a service, directive or component,
the Angular framework automatically manages and injects dependencies if needed.
In the case of factories, you can use additional deps
property to define dependencies
and allow your factory function to access corresponding instances during execution.
Let's imagine we need to manually bootstrap the AuthenticationService
that depends on the RoleService
and LogService
instances.
@Injectable({ providedIn: 'root' })
export class AuthenticationService {
constructor(private roles: RoleService,
private log: LogService) {
}
// ...
}
We should declare our factory-based provider the following way now:
@NgModule({
// ...,
providers: [
{
provide: AuthenticationService,
useFactory: authServiceFactory,
deps: [ RoleService, LogService ]
}
],
// ...
})
export class AppModule { }
With the code above we instruct Angular to resolve the RoleService
and LogService
and use with our custom factory function when the AuthenticationService
singleton instance gets created for the first time.
Finally, your factory implementation should look similar to the following one:
export function authServiceFactory(roles: RoleService, log: LogService) {
const service = new AuthenticationService(roles, log);
// do some additional service setup
return service;
}
Using @Inject Decorator
Another important scenario you might be interested in is the @Inject
decorator.
The @Inject
decorator instructs Angular that a given parameter must get injected at runtime.
You can also use it to get references to "injectables" using string-based keys.
To demonstrate @Inject
decorator in practice let's create a dateFactory
function to generate current date:
// src/app/app.module.ts
export function dateFactory() {
return new Date();
}
Now we define a custom provider with the key DATE_NOW
that is going to use our new factory.
@NgModule({
// ...,
providers: [
{ provide: 'DATE_NOW', useFactory: dateFactory }
],
// ...
})
export class AppModule { }
For the next step, you can import the @Inject
decorator from the angular/core
namespace
and use it with the Component1
we created earlier in this chapter:
import { /*...,*/ Inject } from '@angular/core';
@Component({...})
export class Component1Component {
constructor(logService: LogService, @Inject('DATE_NOW') now: Date) {
logService.info('Component 1 created');
logService.info(now.toString());
}
}
There are two points of interest in the code above. First, we inject LogService
instance
as a logService
parameter using its type definition: logService: LogService
.
Angular is smart enough to resolve the expected value based on the providers
section in the Module, using LogService
as the key.
Second, we inject a date value into the now
parameter.
This time Angular may experience difficulties resolving the value based on the Date
type,
so we have to use the @Inject
decorator to explicitly bind now
parameter to the DATE_NOW
value.
The browser console output, in this case, should be as the following one:
(factory demo): [info] Component 1 created
(factory demo): [info] Sun Aug 06 2017 08:45:36 GMT+0100 (BST)
(factory demo): [info] Component 2 created
Another important use case for the @Inject decorator is using custom types in TypeScript when the service has different implementation class associated with the provider key, like in our early examples with LogService and CustomLogService.
Below is an alternative way you can use to import CustomLogService
into the component and use all the API exposed:
@Component({/*...*/})
export class Component1Component {
constructor(@Inject(LogService) logService: CustomLogService) {
logService.info('Component 1 created');
}
}
In this case, you are getting access to real CustomLogService
class that is injected by Angular for all the LogService
keys.
If your custom implementation has extra methods and properties, not provided by the LogService
type, you can use them from within the component now.
This mechanism is often used in unit testing when the Mock classes expose additional features to control the execution and behavior flow.
Using a Value
Another scenario for registering providers in the Angular framework is providing instances directly, without custom or default factories.
The format, in this case, should be as following:
{ provide: <key>, useValue: <value> }
There are two main scenarios of providing the values.
The first scenario is pretty much similar to the factory functions you can create and initialize the service instance before other components and services use it.
Below is the basic example of how you can instantiate and register custom logging service by value:
const logService = new CustomLogService();
logService.setPrefix('(factory demo)');
@NgModule({
// ...
providers: [
{ provide: LogService, useValue: logService }
],
// ...
})
export class AppModule { }
The second scenario is related to configuration objects you can pass to initialize or setup other components and services.
Let's now register a logger.config
provider with a JSON object value:
@NgModule({
// ...,
providers: [
{
LogService,
{
provide: 'logger.config',
useValue: {
logLevel: 'info',
prefix: 'my-logger'
}
}
}
],
// ...
})
export class AppModule { }
Now any component, service or directive can receive the configuration values by injecting it as logger.config
.
To enable static type checking you can create a TypeScript interface describing the settings object:
export interface LoggerConfig {
logLevel?: string;
prefix?: string;
}
Finally, proceed to the LogService
code and inject the JSON object
using the logger.config
token and LoggerConfig
interface like in the following example:
@Injectable({ providedIn: 'root' })
export class LogService {
constructor(@Inject('logger.config') config: LoggerConfig) {
console.log(config);
}
info(message: string) {
console.log(`[info] ${message}`);
}
}
For the sake of simplicity we just log the settings content to the browser console. Feel free to extend the code with configuring the log service behavior based on the incoming setting values.
If you run the web application right now and open the browser console you should see the next output:
{
logLevel: 'info',
prefix: 'my-logger'
}
Registering providers with exact values is a compelling feature when it comes to global configuration and setup. Especially if you are building redistributable components, directives or services that developers can configure from the application level.
Using an Alias
The next feature we are going to see in action is "provider alias".
You are probably not going to use this feature frequently in applications, but it is worth taking a look at what it does if you plan to create and maintain redistributable component libraries.
Let's imagine a scenario when you have created a shared component library with an AuthenticationService
service
that performs various login and logout operations that you and other developers
can reuse across multiple applications and other component libraries.
After some time you may find another service implementation with the same APIs,
or let's assume you want to replace the service with a newer SafeAuthenticationService
implementation.
The main issue you are going to come across when replacing Types is related to breaking changes. The old service might be in use in a variety of modules and applications, many files import the Type, use in constructor parameters to inject it, and so on.
For the scenario above is where "alias" support comes to the rescue. It helps you to smooth the transition period for old content and provide backwards compatibility with existing integrations.
Let's now take a look at the next example:
@NgModule({
// ...
providers: [
SafeAuthenticationService,
{ provide: AuthenticationService, useExisting: SafeAuthenticationService }
],
// ...
})
export class AppModule { }
As you can see from the example above, we register a new SafeAuthenticationService
service
and then declare an AuthenticationService
that points to the same SafeAuthenticationService
.
Now all the components that use AuthenticationService
are going to receive the instance of the SafeAuthenticationService
service automatically.
All the newly introduced components can now reference new service without aliases.
Difference with the "useClass"
You may wonder what's the difference with the
useClass
provider registration compared to theuseExisting
one.When using
useClass
, you are going to end up with two different instances registered at the same time. That is usually not a desirable behavior as services may contain events for example, and various components may have issues finding the "correct" instance.The
useExisting
approach allows you to have only one singleton instance referenced by two or more injection tokens.