Using Angular’s ng-content in conjunction with ngProjectAs attribute
Introduction
I recently ran into some troubles when trying to project content across several component scopes. It took me quite a bit of searching until I found a way to make my solution work. Turns out content projection in Angular is a pretty complex and not-that-well documented topic. I came across and highly recommend reading Eudes Petonnet’s ng-content: the hidden docs — it provides a great and concise explanation on how Angular’s ng-content
works in different scenarios.
All in all, documentation on the topic of content projection in Angular seems a bit lacking, so I thought I’d try my hand at providing a comprehensive example on how to use it and what pitfalls you might encounter.
Setup
So here’s the basic example I’ve been tweaking: I want to create a simple questionnaire component that can render both single and multiple choice questions. The component is data bound to a feed of questions. The questions, depending on their type, are then rendered either as a single or multi select component. You can find the working example here.
I’ve also created a q-validator
component, used to display an error message in a question (both q-single
and q-multi
). The validator is rendered inside of the question component’s host and will display an error if the question is not valid (in the example — if the value is null
).
Using <ng-content>
Inside of the questionnaire template, I’ve defined 3 sections (list back-to-front for dramatic effect):
- Footer — Contains the survey submit button
- Body — Contains all of the questions rendered in resp. components (
q-single
orq-multi
) - Header — Contains any content that is passed inside of the
q-questionnaire
tags
The header part of the survey is an example of the most basic use of ng-content
— projecting everything that is inside of the element’s selector into the element’s host.
In the demo, I’m just passing some text inside of a h6
tag in the container, but for a more complex ‘greeting’, you can pass anything you like — standard tags, other components, template outlets.
Using <ng-content select=”*”>
If you need to further structure the content that is passed, you can use ng-content
`’s select
attribute. This selects all elements passed in the content that match the criteria of the select
argument.
In the example, I’ll change the structure of the q-questionnaire
header to render content as follows: everything passed with a qHeader
attribute will be rendered inside of a h6
tag at the top of the .questionnaire__header
section. Everything else will be rendered below it.
With these changes, the questionnaire template looks like this:
The <ng-content select="[qHeader]">
always ‘gets’ content first. When all projections with `select` have passed, the rest is dumped in the <ng-content>
catch all tag (if any). So now, if some header text is passed, it will always render on top of the questionnaire header in a h6
tag. Even if passed like this:
Interlude — content projection woes
Disclaimer: This is a pretty messy way to pass content down through several components. For instance, having a reference to the questionnaire in the QValidator
component is way easier (and won’t interfere, as the validator is really a questionnaire only control).
My survey view is working properly — the question data is rendered correctly and the validator error messages show up when the questions are not valid. But since my error message is not tied to my questions data, I might want to make it customizable (for example, if the questions were all in German, currently my error messages will still be in English).
I add an input to my questionnaire
component, accepting an object with error messages for each type of question (conveniently, only 2). I then pass the input down to the question components:
and project it inside of the question components’ templates using ng-content select="[qError]"
:
Inside of the validator, I want to handle the projected content with another content projection:
The content projection is handled with a select
argument, in case we want to pass multiple content blocks down to the validator (which we will!).
In my survey
view, I pass down custom error messages to my q-questionnaire
via the errorMsg
input:
When I run the app, no errors are thrown, but my validators do not show any error messages. The error icons are still visible, so the q-validator
component is properly rendered for invalid questions.
If I debug my app, I can see that the error messages are properly passed to the q-questionnaire
component (the instance of q-questionnaire.errorMsgs
return the proper messages, in German). The question components are also getting a reference to the questionnaire (q-single.questionnaire
is not null
) and they are properly getting the errorMsgs (as they are public
). If I change my q-multi.component.html
to the following, I can even see the error message on my last question:
So whatever’s going wrong must be happening in the last step of the projection, when the message is being passed to the q-validator
component.
Using <ng-content ngProjectAs=”*”>
The ng-content
tag inside of the q-validator
component does not seem to be properly rendering the error message.
If I change the template of the q-validator
component to use a catch-all ng-content
tag, the error message is displayed properly:
But what if I want to pass multiple content blocks and render them in pre-specified positions? I’d really like it if I could keep the select
attribute on that ng-content
tag.
I search around for any additional documentation regarding content projection in order to find something that can help me with my case. Eudes Petonnet’s post does a great job of explaining how ng-content
works and has examples for different use cases.
In my example, the fault is in the projection, inside q-single
/q-multi.component.html
:
Since the q-validator
component is awaiting for a text passed with <ng-content select="[qError]">
, it doesn’t recognize the block passed from q-single/q-multi
(as the projection has not yet been replaced and the <ng-content>
element itself does not match the criteria of `select=”[qError]”`). This can be solved by using the ngProjectAs
attribute — this attribute can be placed on any element and allows it to ‘mask’ itself as the criteria passed, for the sake of content projection.
After adding the ngProjectAs="[qError]"
to the ng-content tag
, the error messages appear correclty and are in German. Now the survey has customizable error messages that can be passed from the q-questionnaire
component. It would be great if we could customize the error icon as well.
Using <ng-template ngProjectAs=’*’>
Okay, I want to pass an icon down to the validator component. I’ll create an @Input()
property in the q-questionnaire
that accepts a TemplateRef
of the icon.
The icon is defined in the survey.component.html
and the template ref is passed to the q-questionnaire
via the input:
I pass a reference to the questionnaire inside of the q-single
/q-multi
class and pass the questionnaire error icon down to the q-validator
(using ngTemplateOutlet
)
And, depending on whether the control has a custom icon passed or not, it renders the custom or default icon (resp.) in the validator:
Disclaimer: Again, this is a far-from-ideal approach, but I think it is a good way to illustrate how content can be passed.
The QValidatorComponent.errorIcon
gets a ContentChild
by checking for an instance of QErrorIconDirective
(using a ViewChild
would always get a reference, due to the default icon). However, the ContentChild
query never seems to return anything. I tried debugging and could see that the template was in fact passed to the questionnaire
component (so QQuestionnaire.errorIcon
was not undefined
), thus was properly passed inside of the q-single
/q-multi
components and reached all the way down to the q-validator
component (if the decorator on QValidatorComponent.errorIcon
is changed to @ContentChild(TemplateRef, { read: TemplateRef })
, the component properly gets a reference to the template and the custom icon is rendered).
Conclusion
I hope that this article and the examples will prove helpful to someone trying to better understand the use <ng-content>
.
You can find the examples here:
Initial State
Error Messages
Error Icon
And here is the GitHub repo, if you want to download and play with the demo:
https://github.com/ViktorSlavov/DemoSurveyApp
Feel free to share your thoughts on anything that comes to mind in the comments below.
Thank you for reading!