Why Directives and Dependency Injection Matter
For quite some time now, I have championed the idea that directives are the most overlooked feature in Angular. They offer a powerful way to manipulate templates, yet in the majority of codebases, they are only used as simple attribute-based utilities for minor tasks. Dependency injection is another concept that deserves more attention. While it is an excellent mechanism for creating reusable code, most projects only use it to provide services.
I have published several articles on both topics, which I have listed below. While it is not mandatory, I suggest reviewing them before proceeding, as they provide useful context:
Throughout this series, we will investigate how these two concepts, frequently used in tandem, can drastically simplify our templates. We will work through practical examples in a step-by-step manner.
Note: These examples are chosen not because they are the most common or critical, but because they are excellent for learning. They demonstrate many different techniques within a small amount of code. You might find that similar third-party libraries already solve these problems.
Disclaimer: For the majority of this article, we will use the traditional
@Inputdecorator because most developers are familiar with it. However, these examples can be easily adapted to use signal inputs. There is one specific example where we will use signal inputs to illustrate how they simplify directive development.
With that out of the way, let's begin.
Creating a Password Strength Indicator
Many web applications today include a feature to assess the strength of a user's password. While public solutions are available, let's build our own, focusing on making it highly adaptable.
We will start with the most basic version: adding a CSS class to an input element to provide a visual representation of the password's strength:
type PasswordStrength = 'weak' | 'medium' | 'strong';
@Directive({
selector: '[appPasswordStrength]',
standalone: true,
host: {
'(input)': 'onInput($event)',
},
})
export class PasswordStrengthDirective {
private readonly el = inject(ElementRef);
onInput(event: InputEvent) {
const input = event.target as HTMLInputElement;
const value = input.value;
const strength = this.evaluatePasswordStrength(value);
this.el.nativeElement.classList.add(
`password-strength-${strength}`
);
}
evaluatePasswordStrength(password: string): PasswordStrength {
if (password.length < 6) {
return 'weak';
} else if (password.length < 10) {
return 'medium';
}
return 'strong';
}
}
This directive can be used in a template like so:
<input type="password" appPasswordStrength>
This is quite straightforward. (The logic for evaluating the password itself is unimportant here; we can put any logic in that spot. Our primary goal is to make the directive as customizable as possible.)
However, this initial version has several limitations:
- Why do we need a specific selector? If the
[appPasswordStrength]attribute is missing, the directive won't apply. Can we make it activate automatically on all password inputs? - What if we need to do more than just change a class, for instance, display a text message? Can the directive simply expose the strength level and let the template handle the presentation?
- How can we allow customization of the evaluation logic? Can we let the developer pass their own function to determine password strength?
- If custom logic is allowed, can we set a default at the application level and still override it for individual inputs?
Let's tackle these issues one by one, starting with the first and simplest:
@Directive({
selector: 'input[type="password"]',
standalone: true,
})
// directive implementation
With this change, we no longer need to add the attribute explicitly:
<input type="password">
Now it applies automatically. But what if we want to disable it for a specific input? We can add an input property for that:
@Directive({
selector: 'input[type="password"]',
standalone: true,
host: {
'(input)': 'onInput($event)',
},
})
export class PasswordStrengthDirective {
@Input() noStrengthCheck = false;
private readonly el: inject(ElementRef);
onInput(event: InputEvent) {
if (this.noStrengthCheck) {
return;
}
// logic goes here
}
// the other methods
}
And we can use it in the template like this:
<input type="password" [noStrengthCheck]="true">
The first improvement is complete. Now, let's modify the directive so that, instead of adding a class, it merely informs the template of the password's strength and lets the template decide how to display it. One approach would be to add an @Output, but that creates boilerplate as the developer would need to capture the value in a variable. Instead, we will use exportAs to get direct access to the directive instance:
@Directive({
selector: 'input[type="password"]',
standalone: true,
exportAs: 'passwordStrength',
host: {
'(input)': 'onInput($event)',
},
})
export class PasswordStrengthDirective {
@Input() noStrengthCheck = false;
// property to capture in the template
strength: PasswordStrength = 'weak';
// no need for ElementRef anymore
onInput(event: InputEvent) {
if (this.noStrengthCheck) {
return;
}
this.strength = this.evaluatePasswordStrength(value);
}
evaluatePasswordStrength(password: string): PasswordStrength {
if (password.length < 6) {
return 'weak';
} else if (password.length < 10) {
return 'medium';
}
return 'strong';
}
}
Now we simply assign the strength to a property so the developer can access it in the template. Here's how:
<input type="password" #evaluator="passwordStrength">
@switch (evaluator.strength) {
@case ('weak') {
<div>Weak password</div>
}
@case ('medium') {
<div>Medium password</div>
}
@case ('strong') {
<div>Strong password</div>
}
}
The exportAs attribute allows us to capture the directive instance in a template variable, giving us access to its properties. More information can be found in the official documentation.
Next, we want to let the developer supply their own evaluation logic. We could use a standard Input property, but that would require providing the function each time we use a password input, which is repetitive and easy to forget. A better approach is to use an InjectionToken and a helper function to allow the logic to be provided at the application level:
type PasswordEvaluatorFn = (password: string) => PasswordStrength;
export const EVALUATOR_FN_TOKEN = new InjectionToken<
PasswordEvaluatorFn
>(
'PasswordEvaluatorFn',
);
export function providePasswordEvaluatorFn(
evaluatorFn: PasswordEvaluatorFn,
) {
return [{
provide: evaluatorFnToken,
useValue: evaluatorFn,
}];
}
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'input[type="password"]',
exportAs: 'passwordEvaluator',
standalone: true,
host: {
'(input)': 'onInput($event)',
},
})
export class PasswordEvaluatorDirective {
strength: PasswordStrength = 'weak';
@Input() evaluatorFn = inject(evaluatorFnToken);
@Input() noStrengthCheck = false;
onInput(event: InputEvent) {
if (this.noStrengthCheck) {
return;
}
const input = event.target as HTMLInputElement;
const value = input.value;
this.strength = this.evaluatorFn(value);
}
}
This allows us to provide a custom evaluator function across the whole application:
export function customPasswordEvaluator(password: string) {
if (password.length < 6) {
return 'weak';
} else if (password.length < 10) {
return 'medium';
}
return 'strong';
}
bootstrapApplication(AppComponent, {
providers: [
providePasswordEvaluatorFn(customPasswordEvaluator),
],
// the rest of the application
});
We can then use this as needed.
But there is a catch: what if no custom evaluator is provided? We could make the directive throw an error, but that's not always ideal. Instead, let's make the directive fall back to a default evaluator. Currently, if the token isn't provided, the dependency injection system will throw a NullInjectorError. We can handle this with the optional flag:
@Directive({
//...
})
export class PasswordEvaluatorDirective {
//...
evaluatorFn = inject(evaluatorFnToken, { optional: true });
//...
}
Now, the inject function will return null instead of throwing an error when the token is not found. We can use this to apply a default evaluator:
export const defaultEvaluatorFn: PasswordEvaluatorFn = (
password: string,
): PasswordStrength => {
if (password.length < 6) {
return 'weak';
} else if (password.length < 10) {
return 'medium';
}
return 'strong';
}
@Directive({
//...
})
export class PasswordEvaluatorDirective {
//...
evaluatorFn = inject(
evaluatorFnToken,
{ optional: true },
) ?? defaultEvaluatorFn;
//...
}
This way, developers are not required to provide a custom evaluator. They can rely on the default or override it at either the component or application level.
So, the final question is: how do we allow custom evaluators on a per-input basis? Suppose we have several password inputs in a component, but want them to behave differently. Thanks to how inject works, we can simply decorate our evaluatorFn with @Input, and it will function correctly:
@Directive({
//...
})
export class PasswordEvaluatorDirective {
//...
@Input() evaluatorFn = inject(
evaluatorFnToken,
{ optional: true },
) ?? defaultEvaluatorFn;
//...
}
Our directive can now be used like this:
<input type="password"
#evaluator="passwordEvaluator"
[evaluatorFn]="myEvaluatorFn"/>
Here is the final component with a live demo:
Up Next
In this section, we learned how to use InjectionToken to inject custom logic into a directive, how to export a directive instance, and how to use a specific selector for matching. In the next part, we will explore structural directives and advanced DOM manipulation.
We will focus on structural directives and how they enable components and directives to work together, further decluttering our .html files.
Let's create a loader component!
The Scenario
Consider this common situation: we have a component that loads data, and we need to show a loading indicator during this process. The requirements are as follows:
- We should be able to wrap any template within a loader, which shows a spinner when necessary.
- The loader should accept an input property that signals whether data is loading.
- The wrapped content should be covered by an overlay to prevent user interaction, which could trigger additional HTTP requests.
Here is a simple implementation:
@Component({
selector: 'app-loader',
template: `
<div class="loading-container">
<ng-content/>
@if (loading) {
<div class="blocker">
<p-progressSpinner/>
</div>
}
</div>`,
standalone: true,
styles: [
`
.loading-container {
position: relative;
}
.blocker {
background-color: black;
position: absolute;
top: 0;
z-index: 9999;
width: 100%;
height: 100%;
opacity: 0.4;
}
`,
],
imports: [NgIf, ProgressSpinnerModule],
})
export class LoaderComponent {
@Input() loading = false;
}
Note: The examples here use PrimeNG, but you can easily adapt them for any other component library.
This component projects any content into an ng-content slot, with some CSS and the PrimeNG ProgressSpinner providing the visual feedback. The loading input toggles the spinner's visibility.
In the template, usage looks like this:
<app-loader [loading]="loading">
<p>Some content</p>
</app-loader>
Wait, isn't this article about directives?
Indeed, it is. But what's wrong with the component approach? The example we saw was fairly straightforward. Real-world templates are often more complex. Take a look at this template snippet:
<app-loader [loading]="loading">
<div class="p-grid">
<div class="p-col-12">
<p>Some content</p>
</div>
<app-loader [loading]="otherLoading">
<div class="p-col-12">
<p>Some other content</p>
<app-loader [loading]="evenMoreLoading">
<div class="p-col-12">
<p>Even more content</p>
</div>
</app-loader>
</div>
</app-loader>
</div>
</app-loader>
When you have several nested elements, the template grows, with more indentation levels and closing tags. What I would really like to achieve is something like this:
<p *appLoading="loading">Some content</p>
How can we accomplish this? We need a directive that:
- Creates a
LoaderComponentinstance dynamically. - Projects the nested template into that instance.
- Keeps them synchronized by updating the
LoaderComponentwhen theloadinginput changes. - Renders the entire result.
Let's explore the solution!
Understanding Structural Directives
Structural directives are powerful because they let us reference templates using TemplateRef and perform a variety of manipulations. We also have access to ViewContainerRef, which allows us to create components dynamically. All that's left is projecting the template into that component. And yes, this is achievable! Let's start with the basic directive structure:
@Directive({
selector: '[appLoading]',
standalone: true,
})
export class LoaderDirective {
private readonly templateRef = inject(TemplateRef);
private readonly vcRef = inject(ViewContainerRef);
@Input() appLoading = false;
templateView: EmbeddedViewRef<any>;
loaderRef: ComponentRef<LoaderComponent>;
}
Here, we've injected the necessary dependencies (TemplateRef and ViewContainerRef), added a loading input property, and defined two properties: templateView and loaderRef. The first stores the template reference, and the second holds the ComponentRef after creating the loader component. Both need to be retained.
Now, we need to set up the initial logic in the ngOnInit lifecycle hook:
@Directive({
selector: '[appLoading]',
standalone: true,
})
export class LoaderDirective implements OnInit {
private readonly templateRef = inject(TemplateRef);
private readonly vcRef = inject(ViewContainerRef);
@Input() appLoading = false;
templateView: EmbeddedViewRef<any>;
loaderRef: ComponentRef<LoaderComponent>;
ngOnInit() {
this.templateView = this.templateRef.createEmbeddedView({});
this.loaderRef = this.vcRef.createComponent(LoaderComponent, {
injector: this.vcRef.injector,
projectableNodes: [this.templateView.rootNodes],
});
this.loaderRef.setInput('loading', this.appLoading);
}
}
This method performs four actions:
- Creates an embedded view from the provided template, allowing us to render it dynamically.
- Instantiates a
LoaderComponent. - Projects the embedded view into the loader component using
projectableNodes. This is the key step. - Sets the
loadinginput property on the created loader component.
This setup will work, but two additional steps are needed for proper functionality:
- We must update the
LoaderComponentwhenever theloadinginput on our directive changes. - We need to ensure change detection functions correctly on the projected template, even though it's detached and moved into a new component. The
ngDoChecklifecycle hook will help us here.
Let's finalize the directive:
@Directive({
selector: '[appLoading]',
standalone: true,
})
export class LoaderDirective implements OnInit, DoCheck, OnChanges {
private readonly templateRef = inject(TemplateRef);
private readonly vcRef = inject(ViewContainerRef);
@Input() appLoading = false;
templateView: EmbeddedViewRef<any>;
loaderRef: ComponentRef<LoaderComponent>;
ngOnInit() {
this.templateView = this.templateRef.createEmbeddedView({});
this.loaderRef = this.vcRef.createComponent(LoaderComponent, {
injector: this.vcRef.injector,
projectableNodes: [this.templateView.rootNodes],
});
this.loaderRef.setInput('loading', this.appLoading);
}
ngOnChanges() {
this.loaderRef?.setInput('loading', this.appLoading);
}
ngDoCheck() {
this.templateView?.detectChanges();
}
}
These additions are straightforward: we sync the directive's loading input with the LoaderComponent instance, and in ngDoCheck, we call templateView.detectChanges() to update the child template when the directive's own change detection runs. This is crucial for avoiding issues with components using the OnPush strategy. If you're unfamiliar with ngDoCheck, you can refer to the official docs or this tutorial.
Now we can use the directive in a template, even when we have multiple nested elements:
<p *appLoading="loading">
Some content
<span *appLoading="otherLoading">
Some other content
</span>
<p *appLoading="evenMoreLoading">
Even more content
</p>
</p>
This means no nested templates, no increased indentation, and fewer closing tags. Just a clean directive handling the task.
You can view the full example on StackBlitz with a live demo:
Charting the path ahead
As I have noted before, directives carry remarkable potential, yet they remain largely overlooked across the developer community. In the forthcoming section, we will look at leveraging directives to penetrate existing components.
Earlier segments covered maintaining and exporting template-local state via directives, along with using structural directives to streamline markup. Now we shift our attention to commandeering existing elements and components, enhancing or altering their behavior.
Let’s dive into concrete scenarios.
Scenario 1: Commanding existing elements
Consider this situation: we oversee a content-heavy website with numerous internal pages. The content frequently references external resources, while internal links facilitate navigation within the site. Our goal is to ensure external links nearly always open in a fresh browser tab—preserving the user’s reading experience and overall usability. The most obvious fix is manually adding target="_blank" to every external link. While workable, this becomes tedious and demands that every developer recalls this practice and passes it along to newcomers. Could this be automated?
Clearly, what we need is a directive that can:
- Attach itself to every
aelement - Evaluate whether the
hrefpoints to an external destination - Apply
target="_blank"when appropriate - Offer an opt-out mechanism for specific links
- React to dynamically changing link values
First, we need a reliable way to distinguish external URLs. A handful of straightforward functions can help:
function getHostFromUrl (url: string) {
return new URL(url).hostname.replace("www.", "");
};
function isAbsoluteUrl (url: string) {
const formatedUrl = url.toLowerCase();
return formatedUrl.startsWith("http") || formatedUrl.startsWith("https");
};
function isUrlExternal (url: string, host = window.location.hostname) {
if (isAbsoluteUrl(url)) {
const providedHost = getHostFromUrl(url);
return providedHost !== host;
}
else {
return false;
}
};
Here we rely on the URL constructor to parse the address and compare its origin.
Next, we construct a lean directive that appends target="_blank" to external links. It employs signal inputs to monitor both the current target attribute and the href:
type Target = '_blank' | '_self' | '_parent' | '_top' | '';
@Directive({
selector: 'a:not([noBlank])',
standalone: true,
host: {
'[target]': 'target()',
'[href]': 'href()',
},
})
export class ExternalLinkDirective {
targetRef = input<Target>('');
href = input.required<string>();
target = computed<Target>(
() => isUrlExternal(this.href()) ? '_blank' : this.targetRef()
);
}
This directive is remarkably compact—just 14 lines—and accomplishes everything we asked for: it reads the href, determines whether the link is external, and calculates the appropriate target. There’s also a clever trick to selectively bypass this behavior.
The :not() selector, supported by Angular’s Directive API, lets us exclude certain elements. Here, any link with a noBlank attribute is ignored. So we can mark specific links with noBlank to keep them in the same tab:
<a href="https://google.com">Google</a>
<a href="https://google.com" noBlank>Google</a>
Below is a fully functional example with a live preview:
Scenario 2: Commanding existing components
This portion draws heavily from a blog post by Tim Deschryver about extending components we don’t own using directives. His example hijacks the Calendar component from the PrimeNG suite. Here’s the snippet he shared:
import { Directive } from '@angular/core';
import { Calendar } from 'primeng/calendar';
@Directive({
selector: 'p-calendar',
})
export class CalenderDirective {
constructor(private calendar: Calendar) {
this.calendar.dateFormat = 'dd/mm/yy';
this.calendar.showIcon = true;
this.calendar.showButtonBar = true;
this.calendar.monthNavigator = true;
this.calendar.yearNavigator = true;
this.calendar.yearRange = '1900:2050';
this.calendar.firstDayOfWeek = 1;
}
}
This means we no longer need to supply default inputs every time we use p-calendar—the directive already fills them in. It’s a compelling illustration of extending existing components through directives.
Be sure to check out Tim’s article; it’s packed with other fascinating use cases.
The identical technique can also enhance components we’ve built ourselves.
Looking forward
As demonstrated, directives excel at augmenting existing functionality, sparing us from wrapping everything in custom components. In the upcoming section, we’ll see how directives can manage events—both native and bespoke.
We’ve covered template-local logic, structural directives in place of components, and directive-driven enhancements for existing elements and components. Next, we’ll investigate how directives can handle events and introduce events to components that technically don’t exist.
Here are two intriguing cases to start!
A click-away directive
It’s often essential to detect when a user clicks outside a specific element. This pattern appears in dropdowns, modals, and similar components that should dismiss on outside interaction. It can also apply to gaming or video apps, where clicking elsewhere might pause playback. While we could implement this within the component itself, that wouldn’t yield a reusable piece of logic for other contexts. So, let’s craft a directive!
Its responsibilities:
- capture the target element
- inject the
Renderer2service to subscribe to events - listen for all click events on the document
- if the clicked element is not a descendant of the target (meaning the click happened outside), fire an event
- clean up the listener when the directive is destroyed
Here’s the implementation:
@Directive({
selector: '[clickAway]',
standalone: true,
host: {
'(window:click)': 'handleKeyDown($event)',
}
})
export class ClickOutsideDirective {
private readonly elRef: ElementRef<HTMLElement> = inject(
ElementRef,
);
@Output() clickAway = new EventEmitter<void>();
handleKeyDown(event: KeyboardEvent) {
if (!this.elRef.nativeElement.contains(event.target as HTMLElement)) {
this.clickAway.emit();
}
}
}
The logic is fairly transparent. We inject ElementRef to obtain the target, and use host to listen to window events. Every click on the window is checked: if the target isn’t a descendant of the clicked element—verified via the Node.contains method—we emit a clickAway event. The listener is removed in ngOnDestroy.
A neat trick: by naming the EventEmitter identically to the directive selector, we can attach this custom event to any element just like a built-in one:
<div (clickAway)="onOutsideClick()">
<h1>Click outside of me!</h1>
</div>
It functions as seamlessly as (click) or (mouseover)—like magic!
Try the working demo below:
Now, onto our next case.
Managing scroll behavior
A frequent feature across web apps is triggering actions—like loading additional data—when elements scroll into view. This powers infinite scroll, lazy loading, and similar techniques. Just like before, let’s aim for a reusable solution. Essentially, we want a custom event that fires the moment an element enters the viewport.
We’ll leverage an IntersectionObserver to watch whether an element intersects with another container or the viewport itself. This will be a stripped-down version—real-world scenarios can get intricate—but the essence is:
- identify the target element
- observe all its intersection changes
- when it intersects with the viewport, trigger an event
Here’s one possible implementation:
@Directive({
selector: '[scrollIntoView]',
standalone: true,
})
export class ScrollIntoViewDirective implements OnInit, OnDestroy {
@Input() threshold = 0.25;
@Output() scrollIntoView = new EventEmitter<void>();
elRef = inject(ElementRef);
observer: IntersectionObserver;
ngOnInit() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.scrollIntoView.emit();
}
});
},
{ threshold: this.threshold }
);
this.observer.observe(this.elRef.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
}
We instantiate an IntersectionObserver and monitor all its callbacks. When the target intersects with the viewport, we emit a scrollIntoView event. The observer is disconnected in ngOnDestroy. You’ll notice the pattern mirrors the ClickAway directive—only the core logic differs. Here’s how it appears in a template:
<div>
Really long content goes here
<div (scrollIntoView)="loadMoreContent()">
Dynamic content goes here
</div>
</div>
It behaves just like any native event—truly magical!
Here’s the preview:
Note: the demo features a lengthy list of
divelements; scroll to the bottom to observe a text message recorded in the console.
Next steps
Our investigation into directives keeps uncovering fresh possibilities. In the next installment, we’ll explore displaying templates outside our components using directives and the concept of Portals.
Earlier sections have touched on various directive and dependency injection applications. This time, we’ll see how directives facilitate component communication at the most challenging level—the template.
Let’s move to the next use case.
Shared dynamic templates
Picture a typical application layout: a header, a footer, perhaps a sidebar, plus content within a main element. The header stands out because, while consistent across pages, it may require extra templates on certain routes. For instance, the “Order Details” page might show a product list in the header, while the “Shopping Cart” page could display a cart summary. In essence, we need to inject content into the header dynamically.
A somewhat naive approach involves subscribing to router changes and swapping the header template accordingly. But this has drawbacks:
- The header component grows unwieldy
- No clean mechanism exists for components to pass data to their header-specific templates
- Similar needs on other pages would compound the bloat
What if we could define the template within the component and instruct it to render in the header instead of its usual location?
Believe it or not, this is entirely achievable!
Here’s how.
The Concept
We’ll utilize Angular Material, specifically its Portals feature. Portals, available from the @angular/cdk package, let us render templates outside their original context. For us, that means placing a template inside the header component.
Note: Portals and the
@angular/cdkpackage aren’t strictly required—this could be done with plainng-template-s, but portals simplify a few steps. Feel free to experiment with alternatives.
What’s the core idea? Three components:
- An
ng-templatepositioned in the header where dynamic content should appear, marked with a portal directive - A custom directive that captures a template from any other component
- A shared service that connects the directive’s template to any component (like the header) that wishes to consume it
Let’s begin with the service that distributes the portal among consumers:
The Implementation
The Service
@Injectable({providedIn: 'root'})
export class PortalService {
private readonly portal$ = new Subject<
{portal: Portal<unknown> | null, name: string}
>();
sendPortal(name: string, portal: Portal<unknown> | null) {
this.portal$.next({portal, name});
}
getPortal(name: string) {
return this.portal$.pipe(
filter(portalRef => portalRef.name === name),
map(portalRef => portalRef.portal),
);
}
}
Let's break down what's happening here. We have the portal$ subject, which accepts an object describing a portal—it gets a name (indicating where we want to show the template, say, header) and the portal itself. The sendPortal method pushes the portal into the service so subscribers can consume it, and the getPortal method retrieves a specific portal from the service. The getPortal method is straightforward, but it makes the service—and the directive that will consume it—highly reusable, letting us route different templates to different locations across the app.
With the service in place, let's build the header component and leverage this service to render the content:
The Header Component
@Component({
selector: 'app-header',
standalone: true,
template: `
<mat-toolbar>
<span>Header</span>
<ng-template [cdkPortalOutlet]="portal$ | async"/>
</mat-toolbar>
`,
imports: [MatToolbarModule, PortalModule, AsyncPipe],
})
export class HeaderComponent {
private readonly portalService = inject(PortalService);
portal$ = this.portalService.getPortal('header');
}
As shown, the component pulls its designated portal template through our service and uses the cdkPortalOutlet directive to render it. We then use the async pipe to subscribe to the portal observable and display the template as soon as it's available. (note: passing null to cdkPortalOutlet results in nothing being rendered—this detail matters for the directive we're about to build).
Now that we have the receiving end sorted, it's time to create the directive that handles the heavy lifting.
The Directive
Since we're dealing with templates, this will be a structural directive. We'll name it portal, and it will accept an input with the same name, which specifies the portal name to which the template should be sent.
@Directive({
selector: "[portal]",
standalone: true,
})
export class PortalDirective implements AfterViewInit, OnDestroy {
private readonly templateRef = inject(TemplateRef);
private readonly vcRef = inject(ViewContainerRef);
private readonly portalService = inject(PortalService);
@Input() portal!: string;
ngAfterViewInit() {
const portalRef = new TemplatePortal(
this.templateRef,
this.vcRef,
);
this.portalService.sendPortal(this.portal, portalRef);
}
ngOnDestroy() {
this.portalService.sendPortal(this.portal, null);
}
}
Notice that we inject both TemplateRef and ViewContainerRef to construct a TemplatePortal instance, which we then dispatch to the service within the ngAfterViewInit lifecycle hook. In practice, we don't manipulate the portal or the template ourselves—the TemplatePortal constructor handles all of that. On ngOnDestroy, we push null to the service, prompting the header component to discard the now-unused template.
Let's see this in action:
The Usage
@Component({
selector: 'app-some-page',
standalone: true,
template: `
<main>
<span *portal="'header'">
Custom header content
</span>
<span>Some content</span>
</main>
`,
imports: [PortalDirective],
})
export class SomePageComponent {}
So in this case, the "Custom header content" text won't render within this component; instead, it'll show up inside the header component. Note that we did not import the HeaderComponent, nor did we place it in the template of SomePageComponent, nor did we engage in any other boilerplate—we simply added the portal directive to a template, and that's all.
A particularly neat feature here is that the "teleported" template remains owned by the component where it was authored, meaning data bindings function as expected. This lets us move dynamically-updating data elsewhere, like this:
@Component({
selector: 'app-some-page',
standalone: true,
template: `
<main>
<span *portal="'header'">{{someData}}</span>
<button (click)="changeContent()">
Change Content
</button>
</main>
`,
imports: [PortalDirective],
})
export class SomePageComponent {
someData = 'Custom header content';
changeContent() {
this.someData = 'New content';
}
}
If we then click the button, the header will update its content to "New content".
You can see this example live here:
Try clicking the links to switch between pages and observe how the header content changes dynamically.
Final tasks
This time, we've examined a more specialized directive use case. Directives, as we've highlighted repeatedly throughout this series, are incredibly powerful tools—and are often overlooked. Next up, we'll tackle the most debated aspect: how directives can inject business logic directly into our templates.
First, let's review what we've accomplished so far and see how the upcoming scenarios diverge from them.
In earlier sections, we constructed directives that performed specific, repeatable, and useful jobs.
For instance, we built a directive that
- validated password strength
- emitted custom events when the host element scrolled into view
- moved a template view into a different component
- rendered a loader
- applied default inputs to existing components
These all share a common theme: they're highly reusable, in that they can be dropped into nearly any project and work immediately, without containing any real business logic. Yet in many practical scenarios, it's the business logic itself that we want to reuse and share easily. So what are the possibilities?
Using directives to handle permissions
Consider a scenario where our app enforces permissions to enable or disable certain features. For instance, users should be able to edit their profile, but only if they hold the appropriate permissions. We have a PermissionService with a hasPermission method that accepts a permission name and returns a boolean. We could use this service in a component like so:
@Component({
selector: 'app-profile',
template: `
@if (hasPermission('edit-profile')) {
<div>
<button (click)="editProfile()">Edit profile</button>
</div>
}
`
})
export class ProfileComponent {
constructor(private permissionService: PermissionService) {}
hasPermission(permissionName: string) {
return this.permissionService.hasPermission(permissionName);
}
editProfile() {
// ...
}
}
But now, every time we need to verify a permission, we must inject the service, potentially write a wrapper method, call it from the template, or deal with some other mildly cumbersome setup. Moreover, if the way permissions are accessed shifts (say the service's API changes), we might face a significant refactoring effort. And if we're using an Observables-based state management solution (like NgRx), the code gets even more tangled.
So how do we address this?
Wouldn't it be convenient if we could simply do this:
@Component({
selector: 'app-profile',
template: `
<div *hasPermission="'edit-profile'">
<button (click)="editProfile()">Edit profile</button>
</div>
`
})
export class ProfileComponent {
editProfile() {
// ...
}
}
We can pass a permission name as an input to the directive, and let the directive manage the rest. Let's see how that's done.
- First, we create a directive that injects the
PermissionServiceand has an input property to hold the permission name. - Next, we include
ngIfas a HostDirective to manage showing or hiding the template. - Finally, we inject a reference to the
NgIfdirective and feed the resulting boolean into it.
Let's build it:
@Directive({
selector: '[hasPermission]',
standalone: true,
hostDirectives: [NgIf],
})
export class HasPermissionDirective {
private readonly permissionService = inject(PermissionService);
private readonly ngIfRef = inject(NgIf);
@Input()
set hasPermission(permissionName: string) {
// we can use any other approach here
this.ngIfRef.ngIf = this.permissionService.hasPermission(
permissionName,
);
}
}
Our directive now works almost flawlessly. What's missing? Naturally, a way to handle situations where the user lacks the permission. We can add an else template to the directive:
@Directive({
selector: '[hasPermission]',
standalone: true,
hostDirectives: [NgIf],
})
export class HasPermissionDirective {
private readonly permissionService = inject(PermissionService);
private readonly ngIfRef = inject(NgIf);
@Input()
set hasPermission(permissionName: string) {
this.ngIfRef.ngIf = this.permissionService.hasPermission(
permissionName,
);
}
// the improtant part
@Input()
set hasPermissionElse(template: TemplateRef<any>) {
this.ngIfRef.ngIfElse = template;
}
}
Now we're essentially performing the business logic and forwarding the outcome to the NgIf directive. Here's a working example with a preview:
Using directives to handle shared data in components
Suppose we're using a UI library that provides dropdown components:
@Component({
selector: 'app-profile',
template: `
<div>
<third-party-dropdown
[items]="dropdownItems"></third-party-dropdown>
</div>
`
})
export class ProfileComponent {
dropdownItems = [
{
label: 'Item 1',
value: 1
},
{
label: 'Item 2',
value: 2
}
];
}
And in certain instances, we need to pass the same option list to multiple dropdowns. For example, the permissions list from earlier could prove useful in many places:
@Component({
selector: 'app-profile',
template: `
<div>
<third-party-dropdown
[items]="permissions"></third-party-dropdown>
</div>
<div>
`
})
export class SomeComponent {
permissionService = inject(PermissionService);
permissions = this.permissionService.getPermissions();
}
We could end up duplicating this exact code across the app. So how do we enhance reusability? We might create a wrapper component that accepts the item list as an input:
<app-wrapper-around-third-party-dropdown
[items]="permissions"></app-wrapper-around-third-party-dropdown>
But that introduces a set of issues:
- CSS encapsulation
- Needing to forward all of the third-party dropdown's inputs and outputs up and down
- Added template complexity
What's an alternative? We could create a directive that populates the item list into the third-party dropdown:
@Directive({
selector: 'third-party-dropdown[permissionList]',
standalone: true,
})
export class PermissionsDropdownDirective implements OnInit {
thirdPartyDropdown = inject(ThirdPartyDropdown);
permissionService = inject(PermissionService);
ngOnInit() {
this.thirdPartyDropdown.items =
this.permissionService.getPermissions();
}
}
And then use it like this:
<third-party-dropdown permissionList></third-party-dropdown>
That's it! Our template stays simple, the third-party-dropdown remains unchanged, and we've gained new reusable functionality on top of it.
Here's an example with a preview:
Last stop
We've delved into a vast array of directive capabilities. Though there's still more to explore, this coverage is ample for projects of various sizes, so this series is approaching its conclusion. In the final part, we'll explore directive selectors in detail—what kinds are available, how to combine them, and some pitfalls or restrictions on selector usage.
Directive Selectors
Every Angular directive has a selector that dictates which HTML elements it applies to. Most often (in my experience, roughly 95% of cases), it's an attribute selector, making the directive behave like a custom HTML attribute.
Yet, as we've seen earlier, directives can go far beyond custom attributes. So let's wrap up this series by diving into all the other selector types—how they work, when to use them, and some potential gotchas—all with practical, real-world examples.
Non-custom attribute selectors
Here, instead of inventing a new attribute, we use an existing, valid HTML attribute. This comes in handy when we want to augment the behavior of standard HTML elements without creating a new attribute.
For example, we might put ids on HTML elements to support E2E testing. However, some E2E frameworks rely on different attributes, like data-test-id or data-qa-id. Rather than manually adding the same id value to multiple attributes, we can build a directive that populates these attributes based on the id attribute:
@Directive({
selector: '[id]',
standalone: true,
host: {
'attr.data-qa-id': 'id',
'attr.data-test-id': 'id',
},
})
export class TestingIdDirective {
private readonly elRef = inject<ElementRef<HTMLElement>>(ElementRef);
get id() {
return this.elRef.nativeElement.id;
}
}
In this example, we target any element carrying an id property and set its value onto two new attributes, data-qa-id and data-test-id, using Host metadata. The element type doesn't matter—as long as it has an id, the directive will work on it. A live reproduction of this example is available here.
Naturally, this case is quite broad, which leads us to a more focused example.
Attribute selectors with values
As seen, the prior example targeted elements that merely had an id attribute, without concerning itself with the actual id value. But there are other reasons to use attribute selectors. Consider this: we have several inputs of type number, yet our app only wants positive numbers. If a user enters a negative number, we want to highlight the input in red. There's a CSS style that does exactly that when it sees an .invalid class. So our job is to target number inputs, inspect their value, and toggle the .invalid class as needed. Here's the directive to do that:
@Directive({
selector: 'input[type="number"]',
standalone: true,
host: {
'class.invalid': 'value',
'(input)': 'onInput()',
},
})
export class PositiveNumberDirective {
private readonly elRef = inject<ElementRef<HTMLInputElement>>(ElementRef);
value = false;
onInput() {
const value = this.elRef.nativeElement.value ?? 0;
this.value = (+value) < 0;
}
}
We don't have to do much besides import the directive into the component—it'll automatically bind to number inputs, and thanks to HostBinding, it'll apply the .invalid class when the value is negative. You can view a working reproduction of this example here.
Next, let's touch on an often-ignored use case for directive selectors.
Class selectors
Yes, you might not have heard, but directives can bind to CSS classes too! In the previous example, we added the .invalid class to inputs; now, suppose we want to attach logic to every .invalid element in the app. For instance, we might want to add a tooltip—which could be handled by an existing separate directive. With class selectors and Host Directives, we can achieve this:
@Directive({
selector: '.invalid',
standalone: true,
hostDirectives: [TooltipDirective],
})
export class InvalidTooltipDirective implements AfterViewInit {
private readonly tooltipRef = inject(TooltipDirective);
ngAfterViewInit() {
this.tooltipRef.title = 'The value is invalid';
}
}
This gets us kind of there, but it carries a noticeable problem, which we'll revisit shortly after covering all the selector types. For now, let's advance to the next tier of directive selector knowledge—more intricate and flexible ones.
Complex selectors
So far, we have focused on selectors that pinpoint specific HTML elements. That is useful, but there are situations where we need to target several element types at once, or possibly exclude certain elements from a selector that is otherwise too broad. Let’s explore how to do that.
Combining selectors
There are two main approaches to combine selectors and widen the "audience" of a directive: using a more specific attribute or listing multiple selectors. We have already covered the first case (the input[type="number"] in the PositiveNumberDirective from an earlier example), so we’ll focus on the multiple-selector approach here.
Imagine a social media app built with Angular where users chat with each other. Every user has an online status that depends on several factors (simply having the page open is not enough to be considered "online"). One of those factors is whether the user is currently typing. There are many places where a user can type, such as input elements and textarea elements. We want a directive that detects this typing activity and sends a quick HTTP request to the server to indicate the user is online (there are various optimizations we could apply, like skipping the call if the user is already marked as online or throttling requests to avoid one per keystroke, but those details are out of scope for this article). Let’s create such a directive:
@Directive({
selector: 'input, textarea',
standalone: true,
host: {
'(input)': 'onInput()',
},
})
export class OnlineStatusDirective {
private readonly userService = inject(UserService);
onInput() {
this.userService.setOnlineStatus(true);
}
}
As with other directives, simply importing it into the relevant component is all it takes for it to work across multiple element types. A working demo of this setup is available here. Now, let’s look at the final technique, which is about narrowing down a selector rather than broadening it.
Selector negation
Occasionally, a selector matches all the elements we want except for one particular case that it shouldn’t apply to, even though the element fits the criteria. For instance, in the second example of this article, the PositiveNumberDirective targeted every input of type number, but there might be a specific input where we do not want the directive to be active.
A natural reaction might be to add an Input property to control whether the directive should operate:
@Directive({
selector: 'input[type="number"]',
standalone: true,
host: {
'class.invalid': 'value',
},
})
export class PositiveNumberDirective {
@Input() enabled = true;
private readonly elRef = inject(ElementRef);
get value() {
const value = this.elRef.nativeElement.value ?? 0;
return this.enabled && (+value) < 0;
}
}
This approach works but comes with three downsides:
- You have to set the boolean value every time you do not want the directive to act, which is not a major issue but still adds a bit of friction.
- You may end up using the
enabledproperty in several places within the directive, such as across multiple host listeners or host bindings, which makes the directive more complex to read and maintain. - Finally, the directive is still applied to the element, which means Angular spends extra processing power on it even when it’s not needed.
Let’s see how the :not() selector, also known as selector negation, can help. Here’s how to use it:
@Directive({
selector: 'input[type="number"]:not([allowNegative])',
standalone: true,
host: {
'class.invalid': 'value',
},
})
export class PositiveNumberDirective {
private readonly elRef = inject(ElementRef);
get value() {
const value = this.elRef.nativeElement.value ?? 0;
return (+value) < 0;
}
}
As you can see, the directive’s logic remains unchanged; we only altered the selector to exclude any element that carries the allowNegative attribute. Now, we can add that "negative" attribute wherever negative numbers should be permitted, and the directive will skip those inputs.
<input type="number" allowNegative />
The :not() specifier can take any selector that is valid for a directive. You can find a working demo of this example here.
Now that we have covered all the scenarios, it’s worth briefly examining some common pitfalls associated with directive selectors.
The Pitfalls
Given the capabilities we have seen, it might be easy to think that directive selectors behave exactly like CSS selectors. However, that is not the case at all. Let’s look at three key areas where the behavior differs.
Cannot target child-parent relations
It would be nice to target elements that are children of certain parents, but this is not possible. Consider trying something like this:
@Directive({
selector: 'div > button',
standalone: true,
host: {
'(click)': 'onClick()',
},
})
export class ParentChildDirective {
onClick() {
console.log('clicked')
}
}
And then using this template:
<div>
<button>Directive is applied</button>
</div>
<button>Directive should not be applied</button>
When you click the first button, you might expect only "clicked" to appear. But clicking the second button, which is not inside a div, will also log that message. This happens because Angular selects the last element in what it sees as a complicated selector and applies the directive to that. So, forget about parent-child, sibling, ancestor, or descendant relationships for directive selectors. Keep in mind, Angular directive selectors are not exactly CSS selectors.
Directives cannot be applied dynamically
It might seem like directives are "added" or "removed" when an HTML element starts or stops matching a selector. This becomes clear when we use classes as selectors. When building the InvalidTooltipDirective, we noted a significant flaw: it might appear that as soon as an element picks up the invalid class, the directive would automatically attach to it. In reality, that does not happen. Angular applies directives at compile-time, so this directive only affects elements that already have the invalid class when compilation happens, not those that gain it later. You can see a demonstration of this behavior (or lack thereof) here.
Structural directives can only be attributes
With structural directives, Angular uses some convenient syntax sugar: the asterisk on an element causes it to be automatically wrapped in an <ng-template>. For example, typing <div *class="some-class">Text</div> is equivalent to <ng-template [class]="some-class"><div>Text</div></ng-template>. This is why structural directives must be attributes—they are applied to the <ng-template> wrapper, not to the element inside it. You cannot, for instance, target the div in the example above with a structural directive because it is not the element carrying the asterisk. While this does place some limits on what you can do, it’s rarely a burden since such scenarios are uncommon.
Conclusion
Writing this article series has been a pleasure, and I hope it has been beneficial to you as well. Angular directives are powerful and often underappreciated, so I hope this series encourages everyone to use them more frequently. In my experience, doing so greatly simplifies codebases, especially the template logic.
Small promotion

This article series took quite a while to produce (nearly a year!). During that time, I was also working on a book called "Modern Angular". It is a thorough guide to the exciting features introduced in recent versions (v14-v18), covering standalone components, improved inputs, signals (of course!), better RxJS interplay, SSR, and much more. If that sounds interesting, you can find it here. The book is currently in the copy-editing stage with a release expected soon, so it is available in Early Access with all 10 chapters online. To stay informed about the print release, you can follow me on Twitter or LinkedIn, where I will announce any news or promotions.
![[MEGA Article] - Superpowers with Directives and Dependency Injection — figure 2 [MEGA Article] - Superpowers with Directives and Dependency Injection — figure 2](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Screenshot-2024-08-08-at-09.31.49-1.png)
![[MEGA Article] - Superpowers with Directives and Dependency Injection [MEGA Article] - Superpowers with Directives and Dependency Injection — Signals article by Armen Vardanyan on Angular In Depth](/assets/covers/bcc511c3ef.png)