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:

We only need a boolean Input to determine whether to show the animation

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 with ng-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 precede div.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.