Use cases: ng-content
Loading spinners, form controls, and more
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
Content projection is not a concept that one may use everyday, and to be honest, I have met Angular developers that didn’t even know about it (heck — I could not even find the corresponding page in the official docs — would appreciate if someone shares a link, or otherwise I think that the docs really miss a page on this). In reality it is a tool that can help us solve very frequent use cases in a dynamic and flexible way.
If you are not familiar with
ng-content
and content projection in general, I suggest you read on that before diving into this article.
Loading spinners
Loading spinners or progress bars are a very familiar and popular element of the UX. Whenever a user submits a form, for example, or loads a chunk of new data, we display a loading animation to signify that an HTTP request is in process and the user should wait.
Of course there are cases when we don’t want to cover the entire page, but rather disable a part of it (in which the form that is being submitted now is located, for example) and display a spinning GIF over it. Because we may have dozens of forms across our app, we want a reusable solution.
Of course we could write a directive, which will receive the supposed form
element which encloses all the inputs, and add child elements on it using an Input
, but that would be too much DOM handling inside a .ts
file. We want a solution based on an HTML template. And this is where content projection will help us.
Here is what we want to accomplish:
Now we have a form inside a custom loader
component, which receives a loading
property as an Input
, which, naturally, disables the form by covering it under an opaque veil, and displays the loading animation over it. When user clicks the Submit button, the submit
method will be called, which will change the loading
property to true
before the HTTP request starts and then set it back to false
once the request finishes.
The .ts
code for the loader
component is fairly simple:
HTML
is not really more complex:
So we project the child content inside a div
, which will receive a loading
class depending on the Input
from a parent component, and that, in its case, will determine whether to display or not to display the animation. I used PrimeIcons in my example but you may load any sort of icon you want.
But the main thing is going on inside the .css
file for our component:
So, the parent div
with class .content
has position: relative
, and .blocked
is invisible until the parent div
receives a loading
class. After that the game is changes: an opaque background is displayed over the form with the spinning animation in the middle.
It was that easy! Now we can reuse our loader
component on any piece of HTML. For example:
You can find the full code for the loader component on StackBlitz.
Form Controls
Take a look at this particular template:
Notice how cluttered this looks. Here we have two forms with lots of different inputs, but essentially the entire template is the same thing: a form
inside a div
with a certain class on it; inside it, form controls within similarly structured div
-s, decorated with labels.
Let’s declutter this using content projection. We are going to create two components: form-container
and form-control-wrapper
. Let’s start with the first one:
And the HTML:
Basically this component will receive the title of the form and project its content into other HTML elements. We can also apply styles here to the overall form. By the way, we can also wrap the form into a loader
component from the previous example if we wanted to without having to repeat ourselves.
Here is the individual control wrapper:
And then again, just content projection into a predefined template:
Notice how both this components are extremely simple, no magic involved. Let’s see how the original template will look like after we apply content projection:
Wow, we are down from 62 lines to 37: almost twofold reduction! This is a significant number, and it would be even bigger if we had more forms. This not only reduces the number of lines (reducing our final bundle size!), but also makes the code much more readable and understandable. Essentially we managed to declutter the mess we had at the beginning, without employing any sophisticated logic.
You can find the full code on StackBlitz.
Increasing complexity
Let’s take a look at Angular Material’s Card Component. Say, we are building a UI using this component, with frequently repeating patterns, like this one:
Of course, we can declutter this the same way we did the form, but now our to-be-projected content is separated — we cannot use just one ng-content
tag, we need more, and a way to explain Angular into which one of them what piece of UI is going to be projected. Here comes the select
attribute of the ng-content
.
In this case the HTML file is of more interest:
Notice the select
attribute in the ng-content
tag. This attribute tells Angular that only elements with a certain class should be projected into that particular ng-content
. Here’s what the previous component look like now:
Notice how tidier the HTML looks without lots of custom component selectors.
One thing to be cautious about when using
select
withng-content
is the fact that it does not matter in which order the elements appear in the parent component, it only matters how they are projected. In out example,div.content
will always precedediv.actions
, even if we place the latter before the former. This isn’t a big deal, but can be confusing for people who are not yet familiar with content projection.
You can find the full source code for this example on StackBlitz.
Summary
Content projection is a powerful, yet underestimated tool in the arsenal of an Angular developer. When used wisely, it can help declutter the code, minimize complexity, increase readability, and reduce the final bundle size.