Advanced Techniques For Dependency Injection In Angular

Web Development | 19-01-2024 | Jessica Bennett

use injection tokens

How do you navigate the labyrinth of Dependency Injection in Angular? By using Injection tokens, different dependency injection modules, resolution mechanisms, decorators, and more. There is so much to know and understand. It is enough to confuse even developers of a top web development company. Here, we will attempt to ease some of your confusion and struggle. So, use this article as a comprehensive guide to understanding dependency injection in angular.

Is your objective to write clean, modular, and testable codes in Angular? Then, knowing about dependency injection is a prerequisite. So, without much ado, let us dive deep into this topic.

Dependency Injection In Angular: A Quick Refresher

Dependency injection, or DI, is a distinguishing feature of Angular. It is inbuilt and so intuitively convenient that we use it without thinking. But to enhance the quality of Angular codes, we must have advanced knowledge of DI. For example, with this advanced knowledge, we can:

  • Troubleshoot weird dependency injection errors
  • Manually configure dependencies for unit testing
  • Understand unusual third-party module DI configurations
  • Create third-party modules for use in multiple applications
  • Develop more modular codes
  • Isolate the different parts of your app so they function individually without interfering with each other

Before navigating the advanced terrains, let us explore the basics of dependency injection in angular. Understand how the Angular dependency injection works. We will cover its configuration options and features later in the article with a particular focus on Injection tokens and custom decorators.

So, what does the DI mean to a web development company in the USA? Consider a small requirement and see how codes written with and without DI affect the output.

Sample code without DI

If you have to develop a small part, module, or class of an app, you will need external dependencies. For example, you need to develop an HTTP service that makes backend calls. It might also need some other dependencies. If you create your dependencies locally every time you need them, your code will look like:

import { HttpClient, HttpHandler } from '@angular/common/http';

export class CoursesService {
private http: HttpClient;

constructor() {
// Manually creating the dependencies
const httpHandler = new HttpHandler();
this.http = new HttpClient(httpHandler);
}

// Method to use the HttpClient
getCourses() {
return this.http.get('/api/courses');
}
}

Looks simple, doesn't it? But it is super difficult to test. Here, the code is aware of its dependencies and creates them directly. This makes it impossible to replace an actual HTTP client with a mock HTTP client. As a result, unit testing this class becomes challenging.

Further, this class is also aware of the dependencies of its dependencies. This means it is also aware of the HTTPClient class dependencies. Hence, replacing this dependency with an alternate version at runtime is almost impossible. Additionally, you might also need different HTTP clients to test in different runtime environments. Angular is meant for easy code testing through mock services, singleton management, etc. These obstacles defeat the basic benefits that Angular offers.

Code with dependency injection

Let us see an alternate version of the same code. This time, we will write it using the dependency injection.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
providedIn: 'root' // This service is available application-wide
})
export class CoursesService {

constructor(private http: HttpClient) {
// HttpClient is injected by Angular's DI system
this.http = http;
}

getCourses() {
return this.http.get('/api/courses');
}

// ... other methods for handling courses
}

Here, the class is unaware of how to create its HTTP dependency. Does not know the internal workings of the dependency or even what are its dependencies. It receives all the required dependencies as input arguments in the constructor.

You will notice we have used the @Injectable() at the beginning of the code. This removes the code to create the dependency and places it somewhere else in your code.

The code snippet,
“@Injectable({
providedIn: 'root' // This service is available application-wide
}) “
indicates that a root injector will provide this service. Thus “CoursesService” becomes a singleton and universally available throughout the app.

With this code, you can:

  • Easily replace a dependency implementation for testing purposes
  • Offer support for multiple runtime environments
  • Provide a third party that uses your services in their codebase with new versions of the service, etc.

Effectively, in the second code, you have used the technique of Dependency injection in Angular. Fundamentally, DI in Angular is a design pattern. It defines the relationship between components and their dependencies. This makes the Angular code more modular, testable, and easy to maintain. Injecting dependencies further facilitates component decoupling and offers a top web development company greater flexibility to create scalable apps. As a developer, you benefit the most. Now, you can easily create service instances outside the component and use a constructor to provide it to the component.

Types Of Dependency Injection In Angular

As an Angular developer, you must know about the three types of dependency injections. We will briefly recapitulate the same.

Type 1: Constructor Injection

Most commonly used DI. Here, the class constructor provides all the required dependencies or services. It works by analyzing the constructor parameter types required. Further, it determines what dependencies a class will need based on this analysis. When you create a class in Angular, the constructor injection calls the class constructor to provide all the required dependencies.
Providing a sample code to make it easy for you to understand.

@Injectable({ providedIn: 'root' })
export class MyService {
constructor(private httpClient: HttpClient) { }
}

Advantages of this DI type include greater clarity on the required dependencies and their immutability.

Type 2: Setter Injection

Here, you leverage the setter method to inject the dependency using the "@Input()" decorator. The setter injection is less commonly used in Angular.

Sample code for injecting this dependency type.

@Component({/* ... */})
export class MyComponent {
private _service: MyService;

@Input()
set service(value: MyService) {
this._service = value;
// additional logic
}
}

Advantages include the flexibility to change component dependencies dynamically.

Type 3: Interface injection

Here, the dependency provides the required injector method. This method will inject the dependency into its clients. But there is a rider here. You must expose a setter method and make it accept the dependency. Further, you must implement an interface to facilitate this.
Sample code for this will look like:

@Injectable({ providedIn: 'root' })
export class ConfigService {
constructor(@Inject('ConfigToken') private config) { }
}

Advantages include flexibility and explicitness. Hence, this injection is ideal for complex injection cases. Typically, developers use them for dependencies that lack a TypeScript-type token. Some examples include objects, strings, and numbers. Very useful when injecting values using Injection tokens.

Advantages Of Using Dependency Injection In Angular

This section will only seem complete if we include this. A top web development company will prefer to use Angular because most know the advantages. So, we will just list them briefly. They include:

  1. Modularity because components and their dependencies are decoupled
  2. Testability because of enhanced unit testing ease
  3. Flexibility because you can change component behavior by changing its dependencies

When you use Angular to build web applications, your app automatically becomes robust and maintainable.

Now, what happens if you do not use dependency injection in Angular? Your code lacks flexibility. You cannot test them easily. You have no control over your code.

Examining Advanced Concepts Of Dependency Injection In Angular

As promised, we will now explore certain advanced concepts for managing dependency injection in Angular. One essential concept that you must know about is the Injection Token. Another is a Custom Constructor. Read on to know more details about these concepts.

Injection tokens and their utility

We have talked in detail about what Dependency injections are. But we have yet to find answers to some vital questions. For example, how do you know what dependency to inject and where? Or which provider factory to call to create a specific dependency?

Angular must classify dependencies using proper logic. Only then can you identify them and distinguish their type. You can uniquely identify a dependency class by using Angular Injection tokens. Hence, an injection token is a unique identifier. A dependency injection system can use It to classify and resolve dependencies. Unlike the regular class dependencies, injection tokens offer greater flexibility and control.

Further, an injection value does not only represent a class. It can represent any object or value and successfully maps with a provider. These providers are responsible for creating and providing the dependency required.

Defining or creating an Injection token

For example, we can create an injection token manually for a "CoursesService" dependency. We will first define the "CoursesService" class using the following code.

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class CoursesService {
// Properties and service methods
}

Then, we create the "InjectionToken" for this service or dependency using the code snippet below.

import { InjectionToken } from '@angular/core';
import { CoursesService } from './path/to/courses.service';

export const COURSES_SERVICE_TOKEN = new InjectionToken<CoursesService>('COURSES_SERVICE_TOKEN');

Here, you will import the “Injectiontoken” from Angular’s core package and “CoursesService” from its file location. Further, you can use the string “COURSES_SERVICE_TOKEN” as a token identifier or description.

Using the injection token to provide a dependency

The “CoursesService” is provided in a module using the “COURSES_SERVICE_TOKEN” within the “AppModule” or an app module, typically the “providers” array of the “@NgModule” decorator. A sample code for this is given below.

import { NgModule } from '@angular/core';
import { CoursesService } from './path/to/courses.service';
import { COURSES_SERVICE_TOKEN } from './path/to/injection.token';

@NgModule({
// other module properties
providers: [
{ provide: COURSES_SERVICE_TOKEN, useClass: CoursesService }
]
})
export class AppModule { }

Here, the dependency injection in angular will provide an instance of “CoursesService” the moment the “COURSES_SERVICE_TOKEN” is injected.

Injecting the dependency using the injection token

A sample code to generate the most common way to inject the dependency using the constructor.

import { Component, Inject } from '@angular/core';
import { COURSES_SERVICE_TOKEN } from './path/to/injection.token';
import { CoursesService } from './path/to/courses.service';

@Component({
// Component metadata
})
export class SomeComponent {
constructor(@Inject(COURSES_SERVICE_TOKEN) private coursesService: CoursesService) {}

// Use this.coursesService as needed in the component
}

Further, you can also use the “Injector” class to inject a dependency outside the constructor. However, this is less common and used only in specific use cases.

import { Component, Injector } from '@angular/core';
import { COURSES_SERVICE_TOKEN } from './path/to/injection.token';
import { CoursesService } from './path/to/courses.service';

@Component({
// Component metadata
})
export class SomeComponent {
private coursesService: CoursesService;

constructor(private injector: Injector) {
this.coursesService = this.injector.get(COURSES_SERVICE_TOKEN);
}

// Use this.coursesService as needed in the component
}

Thus, by using the three steps mentioned above, you can successfully implement injection tokens.

As a developer associated with a top web development company, you can use this token to identify and classify dependency sets uniquely.

Let us now look at some use cases of these injection tokens.

Custom configuration

Leverage injection tokens to custom configure service or component options. For example, think that you have to apply some app-specific settings to a service. Your first thought would be to hardcode these settings, right?

But you can also create an injection token for it and provide the required configuration values using this token. Why? Because it allows easy swapping of configurations based on both user preferences and different environments. Let us generate a sample code to custom-configure service or component options.

import { InjectionToken, NgModule } from '@angular/core';

// Define the configuration interface
export interface NotificationConfig {
enableLogs: boolean;
defaultTimeout: number;
}

// Define the Injection Token for this configuration
export const NOTIFICATION_CONFIG = new InjectionToken<NotificationConfig>('notification.config');

// Use the Injection Token to provide the configuration
@NgModule({
providers: [
{ provide: NOTIFICATION_CONFIG, useValue: { enableLogs: true, defaultTimeout: 3000 } }
]
})
export class AppModule { }

Here, "NotificationConfig" is the interface that will outline configuration options for a hypothetical "NotificationService." The "InjectionToken" representing this configuration is "NOTIFICATION_CONFIG." The above code snippet helps to provide a specific configuration for "NotificationService" within the "AppModule." It achieves this by associating the "NOTIFICATION_CONFIG" with an object containing the required configuration settings.

External library integration

You can further use injection tokens in Angular to abstract dependencies and decouple them. Doing this lets you switch between different library implementations yet retain the consuming code. This will make integrations much easier. To understand this better, let us look at a code sample that defines an injection token for the external library.

import { InjectionToken, NgModule } from '@angular/core';
import { ChartingLibrary } from 'some-external-charting-library'; // Assuming this is the external library

// Define an Injection Token for the external charting library
export const CHARTING_LIBRARY_TOKEN = new InjectionToken<ChartingLibrary>('charting.library');

// Use the Injection Token to provide the library implementation
@NgModule({
providers: [
{ provide: CHARTING_LIBRARY_TOKEN, useClass: ChartingLibrary }
]
})
export class AppModule { }

Here, the "ChartingLibrary" is the object type of class. The external library provides it. When using this code to develop a web application, replace the "some-external-charting-library" with the actual imported library path. The "CHARTING_LIBRARY_TOKEN" mentioned here is the "InjectionToken." This entire Angular DI configuration is done within the "AppModule."

Multiple implementations

The third and very essential use case for developers. You can use injection tokens to facilitate multiple implementations of the same abstract class or interface. A web development company in USA can also ensure that the injected implementation aligns with the configuration or the context. It can use different injection tokens to enable this. Let us look at a hypothetical code sample to define injection tokens for different implementations.

import { InjectionToken, NgModule } from '@angular/core';
import { Logger, FileLogger, ConsoleLogger, DefaultLogger } from './loggers'; // Adjust the import path as needed

// Define Injection Tokens for different logger implementations
export const LOGGER_TOKEN = new InjectionToken<Logger>('logger');

// Provide different logger implementations using the same Injection Token
@NgModule({
providers: [
{ provide: LOGGER_TOKEN, useClass: FileLogger, multi: true },
{ provide: LOGGER_TOKEN, useClass: ConsoleLogger, multi: true },
{ provide: LOGGER_TOKEN, useClass: DefaultLogger, multi: true },
]
})
export class AppModule { }

This hypothetical code gets executed within the "AppModule." The "Logger, FileLogger, ConsoleLogger, DefaultLogger" depict different logging service classes or interfaces. Actual implementations must conform to the "Logger" interface or class. The "LOGGER_TOKEN" is used for all three logger types. You can register multiple providers under the same token using the "multi: true" flag.

Custom Decorators And How They Impact Dependency Injection In Angular

You are mistaken if you think that Decorators are inherent to Angular Syntax. They are a part of the Angular TypeScript.

Technically, a decorator is defined as a declaration of a special kind. You can attach this to any class declaration, parameter, property, accessor, or method. This helps Angular modify their call or class member behavior without altering their original implementation. For example, "class" is a plain typescript. When decorated with "@Component()," it becomes an Angular component. Now it supports many more features than it would as a plain typescript.

Decorators are used in many different forms. But based on their popularity and usage, we will restrict our exploring to only decorators attached to one method.

Custom decorator code with no arguments

Let us now create a custom decorator without argument to give you a fair idea of how it is done.

const logPrinter = (target: Object, propertyName: string, descriptor: PropertyDescriptor) => {
// Store the original method implementation
const originalMethod = descriptor.value;

// Overwrite the original method with new functionality
descriptor.value = function(...args) {
// Call the original function. Store its result
const result = originalMethod.apply(this, args);

// Execute custom logic (log the method name and its return value)
console.log(`-- ${propertyName}() returned: `, result);

// Return the result obtained in the original function
return result;
};

// Return the modified descriptor
return descriptor;
};

export { logPrinter as printLog };

Now apply this decorator to a method in a class using the following code snippet.

class SomeService {

@printLog
someMethod() {
// method implementation
return 'some result';
}
}

The decorator "printLog" intercepts whenever "someMethod" is called. Here, the return value of the executed "someMethod" gets logged to the console. The value is returned.

Custom decorator code with the argument

Now, we will create a custom decorator that will accept an argument. But you may find it challenging to get a decorator to accept a parameter. To ease the process, we will leverage "closures" for this.

export function printLog(message: string) {
return function(target: Object, propertyName: string, descriptor: PropertyDescriptor) {
// Store Original Method Implementation
const originalMethod = descriptor.value;

// Now, overwrite the original method
descriptor.value = function(...args) {
const result = originalMethod.apply(this, args);
message = message ? message : `-- ${propertyName}() returned: `;
console.log(message, result); // Execute custom logic
return result;
}

return descriptor;
}
}

Here, the “printLog” accepts a “message” and returns a decorator function. You can further use this decorator function through the code given below.

class SomeService {

@printLog('Custom message: ')
someMethod() {
// method implementation
return 'some result';
}
}

Again, when you call "someMethod," "printLog" intercepts it. The decorator function then leverages the provided "message" to customize the log. This will execute the original method. It will also return the value after logging it.

Types of decorators supported by Angular

As a developer associated with a top web development company, you must be aware that Angular supports 4 types of decorators. They are:

Class decorators that help Angular identify whether a particular class is a module or a component; widely used examples include "@Component" and "@NgModule"

Property decorators help decorate properties within defined classes; typical examples include "@Override," "@Input," "@Output," etc.
Method decorators help decorate a defined class method with functionality; an example includes “@HostListener,” a decorator placed before the “onHostClick()” method

Parameter decorators are applied to the constructor class and tell Angular which provider to inject within a constructor and parameter; an example includes "@Inject()"

You can select any of the above decorators and implement them in your Angular code.

Leveraging Custom Decorators With Dependency Injection In Angular

You can alter or extend the behavior of a class and its dependencies within the dependency injection system. We will now look at some ways you can further enhance dependency injection usage in Angular through custom decorators.

  1. Add additional logic like authentication checks, logging, caching, etc., before/after service and method execution
  2. Use custom decorators to dynamically configure dependencies to reflect specific conditions or runtime values
  3. Leverage them to determine which interface implementation to use depending on the user settings or other environmental variables
  4. Improve Angular code organization and make codes cleaner and maintainable
  5. Promote code reusability by standardizing behavior across components and services
  6. Allow you to mock dependencies thereby improving unit testing
  7. Help provide precise control over how a dependency is used without impacting others

Hence, with in-depth knowledge, you can maximize the impact of custom decorators. Further, we can use it as a powerful technique to improve the implementation of dependency injection in angular.

Conclusion

Both Injection tokens and custom decorators help enhance the effectiveness of dependency injection in Angular. But as a top web development company, you must have in-depth knowledge about its usage. Only then can you elevate the quality and effectiveness of the codes you write for your website development.

Share It

Author

Jessica Bennett

Hi! I’m Jessica Bennett, a full-time technical writer at Unified Infotech. My expertise is in taking in a lot of information and communicating the key ideas. I am skilled at simplifying difficult technical phrases and jargon such that the common audience can grasp it. I assist major technical organizations in effectively communicating their message across multiple platforms. I hold both a master's degree in Computer Applications and a bachelor's degree in English literature. I can write about a wide range of topics like Website Development, Software Development, eCommerce, Web Design, etc.