inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext` - angular compiler
This article has three key sections. The first is understanding what Angular inject is. The next will be understanding what the constructor phase means in Angular. Finally, a short code demo on how to go about this and fix the Angular inject constructor phase error you most likely are facing.
Since Angular 14, the Angular inject was a streamlined method for injecting DI tokens, the likes of Services for example. It makes it easy for us Angular engineers, to actually create shareable DI tokes, without the need to create classes.
Simply put, Angular inject offers a few benefits:
Removing code boilerplate.
Shareable functions.
Cleaner code base.
So, how and when should you use Angular inject? Well, it can be used in a couple of contexts, such as factory functions, but for the purpose of this tutorial, we will focus on Angular constructor phase.
Simply put anything that precedes or is within the constructor of a class. This basically covers all things member attributes of a class, or anything variable that is within the scope of a constructor i.e. instantiated inside a constructor.
Well, now for the issue at hand, why does this happen? Well, now that you know when and how to use Angular inject, you probably have an idea of where to use it now.
You must have been using it within a scope of a component method. Let's see a classical example problem.
Fix the below code to have it work with inject on click.
<button #buttonElement (click)="hideThisElementOnClick(buttonElement)">
Hide On Click
</button>
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'playground';
public hideThisElementOnClick(element: HTMLElement): void {
const renderer2 = inject(Renderer2);
}
}
Well, this will not work, but why? Well, the section where renderer2 is injected is not within the constructor phase (which is what we are focusing on in this piece)
But how can you tap into the constructor phase? Well, JavaScript closures! But what is a JavaScript closure? This is a story for another day, but I will tell you this, it creates for use a constructor scope that we can work with
So, let’s change our code
import { Component, inject, Renderer2 } from '@angular/core';
export const hideElement = () => {
const renderer2: Renderer2 = inject(Renderer2);
return (element: HTMLElement) => {
renderer2.setProperty(element, 'hidden', true);
};
};
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
private _hideElement = hideElement();
public hideThisElementOnClick(element: HTMLElement): void {
this._hideElement(element);
}
}
So, why does this code work? Well, let’s see.
export const hideElement = () => {
const renderer2: Renderer2 = inject(Renderer2);
return (element: HTMLElement) => {
renderer2.setProperty(element, 'hidden', true);
};
};
This is our closure function. It will create for us an DI token for Renderer2, then return for us a function which we can use. In this inner function, we have access to both the outer and the inner scope, meaning, we can call the hideElement from constructor phase and still have a reference to the DI token for Renderer2
private _hideElement = hideElement();
Now, here, is where we call hideElement() getting a reference to the closure scope, which will be available and ready when we need it.
public hideThisElementOnClick(element: HTMLElement): void {
this._hideElement(element);
}
Finally, we can call the inner scope of hideElement() with the needed parameter!
And that is it!
This is a really awesome effort by Angular, so please share it with your friends!