In the world of code development, creating reusable and flexible components is a fundamental aspect of building robust applications. One powerful feature that Angular provides to achieve this is content projection.
Content projection, also known as transclusion, revolutionizes component design by allowing developers to pass content from a parent component to its child component, creating dynamic and customizable compositions.
In this blog post, we will explore the benefits of content projection in Angular projects and how it can greatly enhance your development experience. We will dive into the concept of content projection, its practical applications, and the advantages it brings to the table.
Whether you’re a seasoned Angular developer or just starting your journey, understanding and harnessing the power of content projection will undoubtedly take your component design skills to new heights.
Read this post to discover how this feature empowers you to build modular and reusable components, improve code readability, simplify component interactions, and achieve greater flexibility in your Angular projects. Let’s dive in!
Benefits of Reusable Angular Components
One of our goals as developers is to try to make components as flexible and reusable as possible and content projection is a pattern that can help us in this regard.
The idea behind content projection is that you can project the content you want to use inside another component.
Content projection is useful when you want to create reusable components that can receive different content based on the context or requirements of the parent component. It enables you to separate the structure and layout of a component from its content.
For example, you could have a reusable ItemCard component that accepts content provided by another component. We will now see some practical examples but before, let me define some Angular concepts that you need to be familiar with:
ng-container
(element): A special element that can hold structural directives without adding new elements to the DOM.ng-content
(element): specifies where to project content inside a component template.ng-template
(element): defines a template that is not rendered by default.TemplateRef
(class): Represents an embedded template that can be used to instantiate embedded views.ViewContainerRef
(class): Represents a container where one or more views can be attached to a component.NgTemplateOutlet
(directive): Inserts an embedded view from a prepared TemplateRef.
If you have already worked on an Angular project, you have surely seen many of these elements. There are different types of content projection, we will go through each of them.
Single-slot content projection
The most basic form of content projection is single-slot content projection and it refers to creating a component into which you can project one component.
To do this you will add an element in the template of your component in the place where you want the projected content to appear.
For example, the following component uses an element to display a message.
<pre>@Component({ selector: ‘app-container-component’, template: ` <h2>Single-slot content projection</h2> <div>Here we can have more code.</div> ` }) export class ContainerBasicComponent {}
With the element in place, users of this component can now project their own message into the component. For example:
Somewhere in the html:
This content will be projected where the ng-content slot is.The resulting code will be
<pre><h2>Single-slot content projection</h2> This content will be projected where the ng-content slot is. <div>Here we can have more code.</div></pre>Note that the
element is a placeholder that does not create a real DOM element.Also, custom attributes applied to
are ignored. For example if you add a class
to a
, it will be ignored.
An example you can relate to
Imagine that your application shows content from different categories such as: events, restaurants and hotels. Let’s call them “items”. You could have a home page listing the most popular items near you. The items in each of these categories have properties in common and also things that are particular to each category.
Now, also imagine that we need to create the detail page of an item where we need to display more information about it, such as images, location, reviews, contact, etc. These things are the common elements, but then the restaurants must have the menu and a pre-order option, the events must have the services provided and the hotels must have information about the type of rooms they offer.
What kind of structure would you use to solve this use case?
One option could be to create a different and independent detail page for each item category, such as EventDetailPage, HotelDetailPage, RestaurantDetailPage. If we follow this path, you will quickly realize that you will be repeating a lot of code as shown in the following image.
A better option may be to have a common details page with all the shared code related to things all Items have in common (images, location, reviews, contact) and then a dedicated html for the particular details. To achieve this, we could use content projection.
So we could create a ItemDetailComponent:
@Component({ selector: “app-item-detail”, template: ` <h1>{{ item.title }}</h1> <div class="image-slider"><img src="image" /></div> <div class="reviews-section"> <div>{{ review.rate }} – {{ review.message }}</div> </div> `, }) export class ItemDetailComponent { @Input() item: Item; }And then create specific components that use it. For example, for restaurants we will create a RestaurantComponent as follows:
@Component({ selector: “app-restaurant-page”, template: ` <div class="menu-section">{{ restaurant.menu }}</div> <div class="pre-order-section"></div> `, }) export class RestaurantComponent { // Restaurant extends Item public restaurant: Restaurant = { //your restaurant data // … }; }For events:
@Component({ selector: “app-event-page”, template: ` <div class="services-section">{{ event.services }}</div> `, }) export class EventComponent { // Event extends Item public event: Event = { //your event data // … }; }And the same for Hotels. As you can see in the code examples from above, with this approach we avoid repeating the code for the common elements.
Multi-slot content projection
A component can also have multiple slots. Each slot can specify a CSS selector that determines which content goes into that slot.
With this pattern, you must specify where you want the projected content to appear. You can accomplish this task by using the select
attribute of .
Angular supports selectors for any combination of:
- tag name
- attribute
- CSS class
- and also the
:not
pseudo-class
Let’s go back to our example and modify the ItemDetailComponent
to hold two elements, one for the main content and the other for the footer:
@Component({ selector: “app-item-detail”, template: ` <h1>{{ item.title }}</h1> <div class="image-slider"><img src="image" /></div> <div class="reviews-section"> <div>{{ review.rate }} – {{ review.message }}</div> </div> `, }) export class ItemDetailComponent { @Input() item: Item; }Content that uses the mainContent attribute is projected into the
element with the select=[mainContent]
attribute and the same happens with the footer content.Let’s image that the Restaurant page now needs to have the Pre order section at the bottom of the page, something like this:
Images and reviews are the sections common to all items, menu and pre order are the sections only needed for Restaurants.
We could use multi slot content projection to handle this use case:
<div><!-- restaurant menu section --></div> <div><!-- restaurant pre order section --></div>
Conditional content projection
If your component needs to conditionally render content, or render content multiple times, you should configure that component to accept an element that contains the content you want to conditionally render.
Angular’s ng-template element defines a template that is not rendered by default.
With ng-template
, you can define template content that is only being rendered by Angular when you, whether directly or indirectly, specifically instruct it to do so, allowing you to have full control over how and when the content is displayed.
One of the main uses for ng-template
is to hold template content that will be used by Structural directives. Those directives can add and remove copies of the template content based on their own logic.
The ng-template
directive and the related ngTemplateOutlet
directive are very powerful Angular features that support a wide variety of advanced use cases.
With the ng-template
tag we are simply defining a template, but we are not using it yet.
Spoiler! Angular is already using ng-template
under the hood in many of the structural directives that we use all the time such as ngIf
, ngFor
and ngSwitch
.
Let’s see an example:
<div><!-- lessons content --></div> <div>Loading…</div>This can be a use of the above
ngIf/else
functionality: display an alternative loading template while waiting for the data to arrive from the backend.As we can see, the else clause is pointing to a template, which has the name “loading”. The name was assigned to it via a template reference, using the
#loading
syntax.But besides that
else
template, the use of ngIf
also creates a second implicit ng-template
! Let’s have a look at what is happening under the hood:<!-- lessons content --> <div>Loading…</div>This is what happens internally as Angular desugars the more concise *ngIf structural directive syntax:
- the element onto which the structural directive
ngIf
was applied has been moved into anng-template
- The expression of
*ngIf
has been split up and applied to two separate directives, using the[ngIf]
and[ngIfElse]
template input variable syntax
The element defines a block of content that a component can render based on its own logic. A component can get a reference to this template content by using either the @ContentChild or @ContentChildren decorators.
If you are familiar with Angular you already know that it’s not possible to apply two structural directives to the same element so in order to avoid having to create an extra div, we can instead use ng-container
directive.
Ng-container
The ng-container
directive provides us with an element that we can attach a structural directive to a section of the page, without having to create an extra element just for that. It can also provide a placeholder for injecting a template dynamically into the page.
The ng-container
element is a logical construct that is used to group other DOM elements; however, the ng-container
itself is not rendered in the DOM tree. For this reason, it can for example enable us to use structural directives without breaking styling dependent on a precise DOM structure (as for example the ones we get when using flex containers, margins, the child combinator selector, etc.).
If you need, you can visit this link to learn more about ng-container
and see some usage examples.
Want more examples? Check this link to see different examples of how to use content projection in an angular project.
Wrapping up
Content projection in Angular has proven to be a powerful tool for achieving component reusability, flexibility, and customization. By allowing content to flow from parent components to their child components, content projection enables developers to create dynamic and adaptable compositions.
Throughout this blog post, we’ve explored the concept of content projection and its various benefits in Angular projects. We’ve seen how content projection promotes code reusability, simplifies component interactions, and enhances the overall development experience. We also saw the different types of content projection and how to use them in a real use case.
By separating the structure of components from their content, Angular empowers us to build modular, maintainable, and scalable applications.
Whether you’re building a complex application with intricate component hierarchies or creating a set of reusable UI components, content projection provides a valuable strategy for managing and composing your components effectively. It allows you to customize and inject content into components, making them more versatile and adaptable to different use cases.
With content projection, you have the tools to create highly reusable and customizable components, reduce code duplication, and improve the overall maintainability of your Angular applications. So go ahead, harness the power of content projection and elevate your Angular development to new heights. Happy coding!