Re-usable Angular components — Part II

What is a structural directive?

A structural directive is defined like any other directive but with one major difference: In the constructor TemplateRefand/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-templatethe 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 ViewContainerRefwhich 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.

ngTemplateOutletexpects a TemplateRefas 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 templateRefand 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 Inputparameters works differently as for normal directives. All Inputs have to be prefixed with the directive selector name.

Also Inputscan 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. *ngIffor 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 $implicitis 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 iand 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 colorbecomes available as appOptionColor.

5: *appOption="let xyz setItFrom [1, 2, 3]; let i = index"

This is basically how *ngForworks 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 templateRefcan 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 templateRefat 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.

Solution

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store