Enhance Components with Directives

Part 3b of Advanced Angular Component Patterns

Isaac Mann
Angular In Depth
4 min readJan 8, 2018

--

Photo by Samuel Zeller on Unsplash

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

03a Communicate Between Components Using Dependency Injection
04 Handling Namespace Clashes with Directives

One element of part 4 of Kent C. Dodds’ series that I didn’t touch on in the previous article is the fact that the withToggle higher order component is able to pull common logic out of the <toggle-on>, <toggle-off>, and <toggle-button> components. There wasn’t very much logic happening in those components in the last article, but what if there were? What if we wanted to be able explicitly specify which <toggle> component should be used instead of being forced to use the closest parent?

Also, there’s nothing happening in the <toggle> component’s template, so let’s switch that to a directive so we can use it in a more flexible way.

Goals:

  1. Allow the base toggle to be a tag <toggle> or attribute <div toggle>.
  2. Allow the toggle to be explicitly set using a withToggle directive.

Implementation:

(1) <toggle> as a Directive

This change is pretty straight-forward. Since the template of the component was just <ng-content></ng-content>, we can remove that line and switch the component to a directive.

@Directive({
exportAs: 'toggle',
selector: 'toggle, [toggle]',
})
export class ToggleDirective {}

You also notice that selector now allows the toggle directive to be either a tag name or an attribute. The exportAs line is required in order get a reference to the directive from the template of a component that is using this directive. I’ll get into more details about exportAs in Part 5: Handle Template Reference Variables with Directives.

(2) withToggle Directive

We’ll bundle up the logic for choosing which toggle directive to bind to in a new withToggle directive.

First, each component injects the withToggle directive instead of directly injecting the closest toggle directive. And each component use withToggle.toggle to access the toggle directive that withToggle provides for it.

@Component({
selector: 'toggle-off',
template: `<ng-content *ngIf="!withToggle.toggle?.on"></ng-content>`,
})
export class ToggleOffComponent {
constructor(public withToggle: WithToggleDirective) {}
}

Second, the withToggle directive binds itself to the same selector pattern as the toggle directive in addition to allowing itself to be created using a withToggle attribute.

@Directive({
exportAs: 'withToggle',
selector: 'toggle, [toggle], [withToggle]',
})
export class WithToggleDirective //...

Now the withToggle directive provides child components either with the explicitly set reference (via the [withToggle] attribute) or the toggle directive that it overloads. For specific details on how withToggle does this, see the postscript at the bottom of this article.

Outcome:

Our app.component.html now shows three different ways of consuming the toggle library.

Basic

<div toggle #firstToggle="toggle">
...
<toggle #secondToggle="toggle">
...
</toggle>
</div>

Notice how the firstToggle uses toggle as an attribute and the secondToggle uses toggle as the tag name. Both work just fine.

Also, the secondToggle is nested inside the firstToggle and the child components of secondToggle default to using secondToggle's state instead of firstToggle — just as we’d expect.

Explicit References

<p [withToggle]="firstToggle">
First:
<toggle-on>On</toggle-on>
<toggle-off>Off</toggle-off>
<toggle-button></toggle-button>
</p>

Here there is no toggle directive as an ancestor of the child components, but the withToggle directive instructs them to use firstToggle from above.

Custom Components

<div [withToggle]="firstToggle">
<labelled-state toggleName="First"></labelled-state>
<labelled-button toggleName="First"></labelled-button>
</div>
<labelled-state toggleName="Second" [withToggle]="secondToggle"> </labelled-state>
<labelled-button toggleName="Second" [withToggle]="secondToggle"> </labelled-button>

withToggle can even be injected down inside of custom components. <labelled-state> and <labelled-button> have no reference to withToggle or toggle. They don’t care where the state is coming from, they just handle how to interact with it using the child components.

Postscript

The withToggle directive is a fairly standard directive, except for the constructor:

constructor(
@Host() @Optional() private toggleDirective: ToggleDirective,
) {}

We’ll explain it one part at a time.

@Host() — This restricts the injector to only look for the ToggleDirective on the specific element or component that the withToggle directive is applied to. It won’t keep searching for a toggle up through the ancestor components.

@Optional() — This tells the compiler not to throw an error if no ToggleDirective is found. (i.e. If we’re manually specifying the reference.) Instead, toggleDirective will simply be undefined if it isn’t found.

Now, we can understand what the following line inside of ngOnChanges is doing.

this.toggle = this.withToggle || this.toggleDirective;

So, if the withToggle @Input() is specified, use that. Next, look for a toggle directive on the host component. If nothing is found, use undefined. The result of all that logic is stored in this.toggle, where the child components can reference it.

--

--