Enhance Components with Directives
Part 3b of Advanced Angular Component Patterns
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:
- Allow the base
toggle
to be a tag<toggle>
or attribute<div toggle>
. - Allow the
toggle
to be explicitly set using awithToggle
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.