Superpowers with Directives and Dependency Injection: Part 5
Tags: angular, javascript, typescript
Published: 2023-04-03 09:50:18

_Original cover photo by [Pawel Czerwinski](https://unsplash.com/@pawel_czerwinski) on Unsplash._

Here we are, part 5 of our series dedicated to Angular's directives! In [previous articles](https://dev.to/armandotrue/series/22266) we have already explored various applications of directives and dependency injection. This time around we shall see how we can use directives to help components communicate on the most hard-to-reuse level - the template.

So, let's get started with today's use case.

## Dynamic shared templates

Imagine a fairly standard application UI structure: we have a header, a footer, maybe a sidebar, and some content in a `main` tag. The header component is of high interest to us, because while it is the same for all pages, for certain pages it might require the addition of some custom templates. For example, if we are on the "Order Details" page, it may display the relevant product list, while on the "Shopping Cart" page it may display the cart summary. In other words, we need to be able to dynamically add some content to the header.

A relatively naive thing to do would be to subscribe in some way to the router and change the header template accordingly. But this has a couple of downsides:

1. Header component will become bloated
2. There won't be a clear way for components to  communicate data to the header for their related pieces of the template
3. We might need this sort of solution for other pages, meaning more bloat

What if we could just create the template in the component itself, and then somehow tell it to display that content in the header instead of its own template?

Turns out, this is entirely possible!

Let's see how

## The Idea

For this example, we are going to use [Angular Material](https://material.angular.io/), and specifically, its [Portals](https://material.angular.io/cdk/portal/overview) feature. Portals come from the `@angular/cdk` package and allow us to render a template outside of its original context. In our case, we will use them to render a template in the header component.

> Note: this could be done without portals, or, anyway, without the `@angular/cdk` package, but this approach would simplify a couple of things. You are welcome to try this out with just `ng-template`-s

So, what is the general idea behind our solution? Three things

1. An `ng-template` in the header in the correct place where want the dynamic content to be rendered, with the portal directive added to it
2. Our own custom directive that will capture a template from any other component
3. A service that would communicate from the directive instance to any component (the header in our particular place) that wants to use the template

Let's start with the service, that actually shares the portal between consumers:

## The Implementation

### The Service

```typescript
@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 understand what goes on here. First of all, we have the `portal$` subject, which will take an object that describes the portal; it will receive a name (where we want to show the template, say, `header`), and the portal itself. The `sendPortal` method is used to send the portal to the service so that subscribers can use it, and the `getPortal` method is used to get a particular portal from the service. The `getPortal` method is quite simple, but it makes the service (and directive that will use it) very reusable so that we can send different templates to different places throughout the application.

So now, that we have the service, let's create the header component and use this service to display the content:

### The Header Component

```typescript
@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 you can see, the component selects its specific portal template via our service, then uses the [`cdkPortalOutlet`](https://material.angular.io/cdk/portal/api#PortalOutlet) directive to render it. We then use the `async` pipe to subscribe to the portal observable and render the template when it is available. (*note*: if we pass `null` to `cdkPortalOutlet`, it will render nothing, that is going to be important in the directive).

As now we have ourselves on the receiving side of things, we can go on and create the directive that does the heavy lifting.

### The Directive

As we are going to work with templates, the directive will be a structural one. We will call it `portal`, and it will take an input with the same name, which will be the name of the portal we want to send the template to. 

```typescript
@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);
    }
}
```

As you can see, we inject both `TemplateRef` and `ViewContainerRef` to create a `TemplatePortal` instance, which we then send to the service in the `ngAfterViewInit` lifecycle hook. Actually, we do not do any manipulations on the portal, or the template, we delegate it all to the `TemplatePortal` constructor. On `ngOnDestroy`, we send `null` to the service, so that the header component will remove the now obsolete template. 

Now, we can try this in action:

### The Usage

```typescript
@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 example, the "Custom header content" text will not be rendered in this component, but rather, in the header component. Notice we did **not** import the `HeaderComponent`, we did not put it in the template of the `SomePageComponent`, or do anything else boilerplate-ish, we just dropped the `portal` directive on some template, and that's it.

Another cool aspect of this is that the template that was "teleported" is still "owned" by the component in which it was written, meaning data bindings work as expected so that we can have dynamically changing data "portal-ed" somewhere else, like this:

```typescript
@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';
    }
}
```

Now, if we go on and click on the button, the header will change its content to "New content".

You can view this example in action here:

{% embed https://stackblitz.com/edit/github-vybw3y?ctl=1&embed=1&file=src/app/app.component.ts %}

Click on the links to navigate from one page to another, and notice how the content in the header is changed dynamically

## Conclusion

This time, we explored a more specific use case for an Angular directive. Directives, as mentioned multiple times throughout this series, are a very powerful tool, and one that is criminally underused. I hope this article will help you to understand how to use them, and how to create your own custom directives. Stay tuned for more use cases in the future!