What Dependency Injection Solves
Dependency Injection from an Angular perspective, pragmatically only solves one real issue. That one issue is Unit Testing. That is, because in an Angular/Typescript setting a developer will have the ability to import and export a file. So there already is a way of decoupling different services/ classes from each other.
import { PostsFacade } from '@razroo/razroo/data-access/posts';
@Component({
selector: 'razroo-blog',
templateUrl: './blog.component.html',
styleUrls: ['./blog.component.scss']
})
export class BlogComponent implements OnInit {
posts: any[];
allPosts$: Observable<Post[]> = new PostsFacade().allPosts$;
constructor() {}
ngOnInit() {}
}
In the above, our class doesn’t control how this value in injected. It goes straight from the import into our class. However, if we were to do something like this:
import { PostsFacade } from '@razroo/razroo/data-access/posts';
@Component({
selector: 'razroo-blog',
templateUrl: './blog.component.html',
styleUrls: ['./blog.component.scss']
})
export class BlogComponent implements OnInit {
posts: any[];
allPosts$: Observable<Post[]> = this.postFacade.allPosts$;
constructor(private postsFacade: PostsFacade) {}
ngOnInit() {}
}
Now it is our BlogComponent class that is controlling how the postsFacade is getting passed through. This makes things particularly easy when it comes to unit testing. It allows us to override the value in our unit test, and create mocks for all services. We will discuss more on this in the chapters in the chapters involving unit testing, but I just wanted to bring up the main reason behind unit testing here.
Less obvious, and specifically if the providedIn
is used, is dependency injection also helps keep bundle sizes compact. This is done by tree shaking, which refers to the compiler removing code from the final app if it is not actually referenced by the app. If we do not use providedIn, tree shaking will not be done.
In addition, as we discussed above, it allows us to keep our configurations separate. While the ability to export/import is an option within Typescript, dependency injection, allows the framework to be completely aware of everything being used within the framework. So, besides unit testing, this architecture can be useful for using tokens that contain a particular value, and then being overridden depending on the environment of application(e.g. development vs. production).
Real World Example
Creating Injectable Service
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class PxCodeService {
constructor() { }
}
This code right here is doing two things:
- It is saying that this service is an Injectable.
- It is saying that this injectable should be provided in the root(aka the AppModule).
Now would be a good time to discuss what the providedIn
property does. It accomplishes two things:
- Angular creates a single, shared instance of the service and injects it into any class that asks for it. So, there is no need to insert it as a provider for your module, and you can simply pull it into your class whenever you want. a
- It also allows Angular to optimize an app, by removing the service from the compiled app, if it isn’t used.
Including Injectable Service in Component
Now if we would like to include this service in our component, we would do the following:
// code-box.component.html
<div *ngFor="let codeBox of codeBoxes">
{{codeBox.data}}
</div>
import { Component } from '@angular/core';
import { CodeBox } from './code-box.interfaces';
import { PxCodeFacade } from './px-code.facade';
@Component({
selector: 'px-code-box',
template: './code-box.component.html',
styles: ['./code-box.component.scss'],
})
export class HeroListComponent {
codeBoxes: CodeBox[];
constructor(pxCodeFacade: PxCodeFacade) {
this.codeBoxes = pxCodeFacade.getCodeBoxes();
}
}
Now would technically be a good time on discussing how to test these mocked dependencies, however it gets a little bit complicated. How to mock dependencies, will be discussed in the section on unit testing.
Services that need other services
Services can have their own dependencies. If we wanted to inject a service, into our service, it would be as simple as doing the following:
import { Injectable } from '@angular/core';
import { Logger } from '../logger.service';
@Injectable({
providedIn: 'root',
})
export class PxCodeService {
constructor(private logger: Logger) { }
getLog () {
this.logger.log('getting codeboxes');
}
}
As we can see in the above, our injected service is taking another injected service. By simply passing it into the constructor, similar to how we do for our components, we can use it within our application.
Dependency Injection Token
Internally Angular uses dependency injection tokens for injectable services, to reference what injectable it is using. As an Angular developer, we also have the option to use these tokens directly within our app. There are two different ways of providing tokens in Angular:
- Strings
{provide: 'EmailService', useClass: MandrillService}
- Type Tokens
{provide: EmailService , useClass: MandrillService}
- Injection Tokens
new InjectionToken{provide: EmailService , useClass:}
The benefit of this approach is that it avoids name clashes.
Tokens can be a useful way of providing a default value to be used across the app, hijacking the value, and providing something else, in the scenarios where it is not of use. An example of such is within the Angular Material Components. By default, the MAT_DATE_LOCALE
will use the existing LOCALE_ID
. However, if you would like to override the date locale across the app, all that needs to be done is to override the MAT_DATE_LOCALE
token.
@NgModule({
providers: [
{provide: MAT_DATE_LOCALE, useValue: 'en-GB'},
],
})
export class MyApp {}
This allows for numerous components across the app, using the same token to be overridden.
Wrapping Up
The intent of this chapter, is to introduce the concept of dependency injection in Angular. In addition, present the scenarios in which it is used in a regular enterprise application. Dependency injection as a pattern can be very complex, and I feel looking at the documentation, the majority of use cases tend not to be used. So I wanted to keep this simple. Unit testing, the other widely used part of dependency injection, will be discussed in a separate chapter on unit testing.