Re-usable Angular components — Part II
In this part we take a look at structural directives and how we can use them to build even more generic components. In the first part we discussed other approaches to build a re-usable component.
What is a structural directive?
A structural directive is defined like any other directive but with one major difference: In the constructor TemplateRef
and/or ViewContainerRef
is injected.
TemplateRef
allows to access the projected DOM content of the element where the structural directive is applied on, whereas ViewContainerRef
is the service that allows to render any given templateRef
.
As soon as TemplateRef
and/or ViewContainerRef
is injected into the constructor the directive becomes a structural directive.
This also changes how the directive is applied on an element: A structural directive can be applied only on ng-templa
elements or the asterix notation has to be used.
Asterix Notation - *directiveName
You might not have used ng-template
in your application but you definitely have used structural directives. This is because structural directives usually are used in a different way. As syntactic sugar angular allows to use a structural directive with an asterix prefix. So instead of writing <ng-template appOption></ng-template
the shorter asterix form can be used <div *appOption></div>.
Under the hood angular translates it back into the <ng-template *appOption></ng-template>
form.
Rendering
The template of a structural can be rendered by using the ViewContainerRef
which offers the method to render the template called createEmbeddedView
. The template can also be rendered in the template of a component by using the structural directive ngTemplateOutlet
.
ngTemplateOutlet
expects a TemplateRef
as input parameter which it will render.
In a nutshell this is how ngTemplateOutlet
works:
Of course if you check the source code it is a bit more complicated than that, but the core functionality is just rendering the templateRef
and use the element where it is applied on as container.
Input and Output
Another major difference to a directive is that structural directives can not have any Outputs
.
Also Input
parameters works differently as for normal directives. All Inputs
have to be prefixed with the directive selector name.
Also Inputs
can not be as attributes but by using an expression. In the example above we have specified two Inputs
.
However, <div *appOption="My text" appOptionOther="My Other text"></div>
will not work. Instead, we have to use the expression grammar.
The expression language of structural directives is quite powerful and allows you to do complex stuff.
Expression grammar
The expression grammar of a structural direction not only allows you to set parameters of your directive but also to receive input, called context when it is rendered.
Angular specifies the grammar like
Thus, the following inputs would be valid.
1: *appOption="'My Text'"
This is passing a string into our appOtionDirective
. It does not have to be necessarily be a string it can also be any other type. *ngIf
for example accepts a boolean as expression input. In order to work with the expression in the directive it has to be declared as input. However, all inputs of a structural directive have to be prefixed with the directive selector name.
2: *appOption="let i = index"
As mentioned earlier the expression language not only allows to set input on the structural directive but also to receive content back from it. The content received back is called context.
The context can be either passed as a second parameter when using the createEmbeddedView(templateRef, context)
or passed as context input when using *ngTemplateOutlet
<ng-content *ngTemplateOutlet="templateRef"; context:{}"></ng-content>
Both allow to pass an optional context object which exposes variables that can be used in the projected content. In this example the structural directive passes the context {index: 1}
On the element the context is received and mapped onto the variable ilet i = index
.
3: *appOption="let xyz, let i = index"
Angular also allows to receive any content and to map onto a custom variable name, in this case xyz
. This is done with the $implicit
convention. Content that is set in the context using the name $implicit
is mapped on all variables, that are not specified in the context, on the receiving side.
3b: *appOption="let xyz, index as i"
It is basically the same as above only that the as notation is used. index
is assigned to i
and can be used in the template as i
. One has to be careful because this does not work standalone. *appOption="index as i"
would result in an error.
4: *appOption="'My text'; color: 'red'; let xyz; let i = index"
In the first example a custom text is passed as input into the directive. This example is extended with a second input which is specified with a key expression. In order to access the color input, it also has to be prefixed with the directive selector name. So color
becomes available as appOptionColor
.
5: *appOption="let xyz setItFrom [1, 2, 3]; let i = index"
This is basically how *ngFor
works in a nutshell. setItFrom
becomes available by prefixing it with the directive selector name. We iterate over all options and render the projected content that is available via the injected templateRef
. The templateRef
can be seen as a blueprint from which a new instance with its own context is generated in each createEmbededView
call.
Using the structural directive for our appOption
As we now have a basic understanding of how structural directives, we can use that knowledge to apply it on our original appOptionsButton
problem.
A structural directive allows us to access the projected DOM content of the element where it is applied on. We can use this to register the templateRef
at our parent appOptionsComponent
.
In our parent component we have to adjust our registerOption
method to accept a template and take care of the rendering of the registered option template. In the template we iterate over the registered templateRef
and render each one of them by using *ngTemplateOutlet
.
Now, we have all the functionality and flexibility we were looking for:
Extend it with everything we learned
We can extend the appOptionsButton
with some more inputs in order to apply everything we learned.
Let's assume we got the following requirements from product management:
- Some options shall have a color
- Each entry shall be prefixed with an incremented number
- We get a list of actions from our backend like
Array<{label: string, icon: string}>
.
Our template how we use our structural directive shall look like this
You can try to implement the requirements by yourself as a small exercise, or you go straight to the solution.