Angular: Dependency Injection and Resolution modifiers
Dependency injection (DI) is a design pattern that Angular uses to provide a way to inject dependencies into a component or service at runtime, rather than hardcoding them into the component or service. This allows components and services to be more modular, reusable, and testable.
In Angular, DI is implemented through providers, which are responsible for creating and providing instances of a service or value. Providers can be registered at different levels of an Angular application, such as at the component level or the module level. When a component or service requests a dependency, Angular looks up the provider hierarchy to find the nearest provider for the requested dependency, and then injects an instance of the dependency into the component or service.
Here are some examples of dependency injection in Angular: Imagine you are building a zoo management app with Angular. You need a ZooService
to manage the animals in the zoo, but you also want to make sure that the animals are well-fed. You can inject a FoodService
into the ZooService
, so that every time a new animal is added to the zoo, the ZooService
automatically feeds the animal.
import { Injectable } from '@angular/core';
import { FoodService } from './food.service';
@Injectable()
export class ZooService {
constructor(private foodService: FoodService) {}
addAnimal(animal: string) {
console.log(`Adding ${animal} to the zoo...`);
this.feedAnimal(animal);
}
private feedAnimal(animal: string) {
console.log(`Feeding ${animal}...`);
this.foodService.feed(animal);
}
}
Here are some different ways of implementing dependency injection in Angular:
Constructor Injection:
This is the most common way of injecting dependencies in Angular, where dependencies are passed to a component’s constructor. To illustrate this, imagine you are building a fitness app with Angular. You need a WorkoutService
to manage different workouts in the app, and you want to inject it into your HomeComponent
import { Component } from '@angular/core';
import { WorkoutService } from './workout.service';
@Component({
selector: 'app-home',
template: `{{ message }}`,
})
export class HomeComponent {
message: string;
constructor(private workoutService: WorkoutService) {
this.message = `Today's workout is ${this.workoutService.getRandomWorkout()}!`;
}
}
Setter Injection:
With this type of dependency injection, dependencies are injected through a setter method. To illustrate this, imagine you are building a game with Angular. You need a ScoreService
to keep track of the player's score, and you want to inject it into your GameComponent
import { Component } from '@angular/core';
import { ScoreService } from './score.service';
@Component({
selector: 'app-game',
template: `Score: {{ score }}`,
})
export class GameComponent {
private _score: number;
constructor() {}
set scoreService(scoreService: ScoreService) {
this._score = scoreService.getScore();
}
get score() {
return this._score;
}
}
Property Injection:
This is where dependencies are injected directly into a component’s property. To illustrate this, imagine you are building a weather app with Angular. You need a WeatherService
to get the current weather, and you want to inject it into your WeatherComponent
import { Component } from '@angular/core';
import { WeatherService } from './weather.service';
@Component({
selector: 'app-weather',
template: `Current temperature: {{ temperature }}°C`,
})
export class WeatherComponent {
temperature: number;
constructor() {}
private _weatherService: WeatherService;
get weatherService(): WeatherService {
return this._weatherService;
}
set weatherService(weatherService: WeatherService) {
this._weatherService = weatherService;
this.temperature = this.weatherService.getCurrentTemperature();
}
}
Angular’s dependency injection (DI) system provides several ways to modify the resolution of dependencies. These resolution modifiers can be used to control how and when dependencies are provided to components and services. In this blog, we’ll take a closer look at some of the most common resolution modifiers in Angular’s DI system.
@Inject:
The @Inject
decorator is used to specify a token that should be used to resolve a dependency. Tokens are unique identifiers that Angular's DI system uses to locate and provide dependencies. By default, the token used to resolve a dependency is the type of the dependency itself. However, in some cases, you may want to use a different token. For example, if you have two services that implement the same interface, you can use the @Inject
decorator to specify which service to use.
@Injectable()
class MyService1 implements MyInterface {}
@Injectable()
class MyService2 implements MyInterface {}
@Component({
selector: 'app-my-component',
template: '<h1>{{message}}</h1>',
})
export class MyComponent {
constructor(@Inject(MyService2) private myService: MyInterface) {
this.message = this.myService.getMessage();
}
}
@Optional:
The @Optional
decorator is used to indicate that a dependency is optional. If the dependency cannot be resolved, Angular's DI system will not throw an error but instead will provide null
. This can be useful in cases where a component or service can function without a specific dependency.
Example: Imagine you are building a toy car with Angular. You need a WheelService
to create the wheels for your car, but you want to have the option to build the car without wheels (maybe it's a hover car!). You can use the @Optional
decorator to make the WheelService
dependency optional.
import { Component, Optional } from '@angular/core';
import { WheelService } from './wheel.service';
@Component({
selector: 'app-toy-car',
template: '<h1>My Toy Car</h1>'
})
export class ToyCarComponent {
constructor(@Optional() private wheelService: WheelService) {}
hasWheels() {
return this.wheelService !== null && this.wheelService !== undefined;
}
}
@Self:
The @Self
decorator is used to restrict the scope of a dependency resolution to the current component or directive. This means that Angular's DI system will only look for the dependency in the component or directive's own injector, rather than searching up the component tree.
Example : Imagine you are building a virtual pet game with Angular. You need a FoodService
to feed your pet, but you want to make sure that your pet doesn't overeat. You can use the @Self
decorator to make sure that your pet only eats the food provided by its own injector.
import { Component, Self } from '@angular/core';
import { FoodService } from './food.service';
@Component({
selector: 'app-virtual-pet',
template: '<button (click)="feed()">Feed</button>'
})
export class VirtualPetComponent {
constructor(@Self() private foodService: FoodService) {}
feed() {
this.foodService.eat();
this.foodService.checkWeight(); // make sure pet doesn't overeat
}
}
@SkipSelf:
The @SkipSelf
decorator is used to exclude the current component or directive's injector from the dependency resolution process. This means that Angular's DI system will skip the current component or directive's injector and search for the dependency in its parent injector.
Example: Imagine you are building a treasure hunt game with Angular. You need a MapService
to create the game map, but you want to make sure that the map is only revealed to players who have found the treasure. You can use the @SkipSelf
decorator to search for the MapService
in the parent injector, so that the map is only visible to players who have reached the parent component.
import { Component, SkipSelf } from '@angular/core';
import { MapService } from './map.service';
@Component({
selector: 'app-treasure-hunt',
template: '<h1>{{message}}</h1>'
})
export class TreasureHuntComponent {
message: string;
constructor(@SkipSelf() private mapService: MapService) {}
findTreasure() {
// logic to find treasure
this.mapService.revealTreasureMap(); // only visible to players who have found the treasure
this.message = 'Congratulations! You found the treasure!';
}
}
@Host:
The @Host
decorator is used to restrict the scope of a dependency resolution to the host component or directive. This means that Angular's DI system will only look for the dependency in the host component or directive's injector, rather than searching up the component tree.
Example: Imagine you are building a pizza delivery app with Angular. You need a PizzaService
to make pizzas, but you want to make sure that customers can only order pizzas with their favorite toppings. You can use the @Host
decorator to make sure that the PizzaService
dependency is provided by the nearest parent component that has a PizzaService
instance.
import { Component, Host } from '@angular/core';
import { PizzaService } from './pizza.service';
@Component({
selector: 'app-pizza-order',
template: '<button (click)="order()">Order {{customerFavorite}} Pizza</button>'
})
export class PizzaOrderComponent {
customerFavorite = 'Pepperoni';
constructor(@Host() private pizzaService: PizzaService) {}
order() {
this.pizzaService.makePizza(this.customerFavorite);
}
}
Thank you for reading if you have any doubts. drop the message in comments.
Follow me on Medium or LinkedIn to read more about Angular and TS!