ngTemplateOutlet tricks
I mentioned in a previous article that the limitations of projecting content from a parent element to its child element, for example:
<!-- template -->
<ng-content></ng-content>
<!-- usage -->
<my-component>
<p>I am a paragraph.</p>
</my-component>
<!-- result -->
<p>I am a paragraph.</p>
were that you can't repeat the projection multiple times or access properties
on the child class from the parent template. However, one of
Günter Zöchbauer's many contributions to the Angular community on Stack
Overflow is this answer outlining how the ngTemplateOutlet
can be
used to do this. It's somewhat more complex than simply adding an ng-content
element, but allows for more complex behaviour too.
To give a more specific case, imagine you're writing an autocomplete input, where suggestions are fetched as the user types and shown beneath the text box. A simple approach might define the child template like this:
<div>
<input>
<div class="suggestions">
<div class="suggestion" *ngFor="let suggestion of suggestions">
<h2>{{ suggestion.name }}</h2>
<p>{{ suggestion.email }}</p>
</div>
</div>
</div>
This is limiting, though; the structure of each suggestion is fixed, making the
component less reusable. It would be more flexible if we could define the
suggestion template in the parent component then inject each suggestion
into
it to provide the display values.
Accessing an ng-template
defined in the parent template as follows:
@Component({
selector: 'my-child',
...
})
export class ChildComponent {
@ContentChild(TemplateRef) parentTemplate;
suggestions: { name: string, email: string }[] = [...];
}
and using an ngTemplateOutlet
to render it out, with the child template
defined as:
<div>
<input>
<div class="suggestions">
<div *ngFor="let suggestion of suggestions">
<ng-container *ngTemplateOutlet="parentTemplate; context: { $implicit: suggestion }">
</ng-container>
</div>
</div>
</div>
allows you to provide the structure of each suggestion in the parent, but using
the suggestion
variable defined in the ngFor
loop in the child:
<my-child>
<ng-template let-suggestion>
<div class="suggestion">
<h2>{{ suggestion.name }}</h2>
<p>{{ suggestion.email }}</p>
</div>
</ng-template>
</my-child>
The key points here are:
-
Using the
ContentChild
withTemplateRef
in the child to access theng-template
element defined in the parent; -
Providing a
context
to inject into the template and binding thesuggestion
variable to the$implicit
key to make that the default value of the context; and -
Placing
let-suggestion
on the parent'sng-template
element to bind that default value context to the namesuggestion
in the parent (you can also uselet-nameInParent="nameInContext"
to bind non-default context values;let-nameInParent
without a value is effectively shorthand forlet-nameInParent="$implicit"
).
Here is the above example as a Plunkr so you can play with it:
The downside of this is that the parent needs to have a specific layout; it has
to define an ng-template
with the appropriate let-
attribute to gain access
to the child property. If you read the previous article, you may be thinking
that an ng-template
with an attribute on it looks exactly like the de-sugared
versions of structural directives. I thought the same thing, and wondered if I
could write a structural directive of my own that would be applied as follows:
<my-child>
<div class="suggestion" *suggestion>
<h2>{{ suggestion.name }}</h2>
<p>{{ suggestion.email }}</p>
</div>
</my-child>
and would generate the appropriate template in a simplified way. Sadly, it seems like you can't access the actual template element from the directive, as it never gets rendered. A no-op directive as follows:
@Directive({
selector: '[suggestion]',
})
export class SuggestionDirective { }
can be used to simplify the markup slightly:
<my-child>
<div class="suggestion" *suggestion="let suggestion">
<h2>{{ suggestion.name }}</h2>
<p>{{ suggestion.email }}</p>
</div>
</my-child>
but you still need to explicitly include the let
. I am yet to figure out how
to get around this (or if you even can).
Comments !