Using Angular’s ng-content in conjunction with ngProjectAs attribute

Viktor Slavov
Ignite UI
Published in
6 min readFeb 8, 2019
Photo by Jeremy Yap on Unsplash

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 or q-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:

Updated Example

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!

--

--