Content Projection in Angular

Reusable UI structures to simplify building complex views

Francois J Rossouw
DVT Software Engineering
11 min readAug 23, 2021

--

Transclusion more commonly known as Content Projection is a vastly powerful and useful directive within AngularJS and upwards. Directives are classes that add additional behaviour to elements in your Angular applications.

Content projection allows a directive to make use of templates while still being able to clone the original content and add it to the Document Object Model, also referred to as the DOM, which is a programming interface for HTML and XML documents and it represents a page where programs can change the document structure, style, and content.

Basically, it allows directives to generate dynamic data-driven DOM structures that create compelling user experiences. Many projects or applications have user interface components that look and work in similar ways. These from an implementational point of view can have duplicated or redundant implementations. Examples include form-based inputs, data displays and photo galleries.

These use cases that work and look similar are often written as duplicated code blocks, which results in redundant implementations. This is where data-driven DOM structures are extremely useful because we can create one directive or component that will allow us to insert data dynamically.

This makes the directive reusable, minimizing the time and/or duplication to code, copy and paste or minimizes redesigning any part of the application that is already created. We now have the ability to change the data dynamically in runtime as well as the ability to reuse these directives throughout our application, without having to change that much code.

Thus, you can make reusable components for scalable applications, by inserting content into already created components or directives.

In this article, I would like to show you how to use Content Projection in your simple to enterprise applications. A beginner’s understanding of Angular will help you follow the practical implementations, however, the coding examples should be easy to follow.

Angular by default allows you to use the @Input decorator to pass data to a component and by using this method you can still pass complex structures and objects to any of your Angular components. Content projection allows you to pass the following data types that you cannot pass with the @Input decorator:

  1. Inner HTML
  2. HTML Elements
  3. Styled HTML
  4. Other Components

Now I will show you how to implement Content Projection. You can follow the steps in an existing Angular project if you want to try it practically. Please refer to GitHub for an implemented project.

Content Projection

Content Projection has three different methods or use cases that we will be covering:

  1. Single Slot Content Projection
  2. MultiSlot Content Projection
  3. Conditional Content Projection

Scenario

Imagine an application where you need to ask a person a couple of questions whether it is for an interview skills assessment or to set up an online exam. You will need a component that will be used to ask questions with different types of input.

Content Projection is the perfect solution for this scenario. You can start by having a component called Test that will hold all our questions like a question paper. You will also need a Question component, which will be continually reused.

The Test component will reuse the Question component for every different question that needs to be answered. Then every question will be injected using Content Projection to inject content into the Test Component. This allows different questions with different input types to be used as reusable code pieces and injected into a single component or into multiple components.

Let’s dive right in!

Single Slot Content Projection

Single Slot Content Projection refers to creating a component into which you can project custom elements or components into one slot or directive. For example: If we want to ask a user their name, surname, birth year, we would normally write a directive for each as below.

<div>
<label for="single-standard-name">1. What is your name?</label>
<input type="text" name="single-standard-name"
id="single-standard-name">
</div>
<div>
<label for="single-standard-surname">2. What is your surname?
</label>
<input type="text" name="single-standard-surname"
id="single-standard-surname">
</div>
<div>
<label for="single-standard-year">3. What is your birth year?
</label>
<input type="number" name="single-standard-year"
id="single-standard-year">
</div>

The code is duplicated for each question directive. Therefore we can safely assume that some parts of the code will be reused, for instance, the label. The input will change depending on the question because a question can use one of many different input types ranging from text boxes to radio buttons.

With content projection, we can remove the redundancy of having to manually redo all the questions and it gives us the ability to only have to focus on the input of each question respectively.

<div>
<label>{{questionNumber}}. {{question}}?</label>
<ng-content select=".single-slot"></ng-content>
</div>

In this code snippet, we created a component called dvt-question-single-slot, which will make use of content projection to insert inputs into the questions.

This is achieved by making use of an ng-content directive and assigning it a class of single-slot, although when using Single Slot Content Projection it is not required to name the content directives, it is good practice to do so.

After this component has been created we can now effectively use and implement it. For each question, we have added two inputs that are the question number and the actual question. The @Input decorator is perfect for adding these structures and will work perfectly.

@Input() question: string;
@Input() questionNumber?: number;

The code snippet below will demonstrate the code refactored to use content projection. Always remember to add the assigned class name assigned in the dvt-question-single-slot component to the content that is being projected single-slot.

<dvt-question-single-slot
[questionNumber]="1"
[question]="'What is your name'">
<div class="single-slot">
<input type="text" name="single-standard-name"
id="single-standard-name">
</div>
</dvt-question-single-slot>
<dvt-question-single-slot
[questionNumber]="2"
[question]="'What is your surname'">
<div class="single-slot">
<input type="text" name="single-standard-surname"
id="single-standard-surname">
</div>
</dvt-question-single-slot>
<dvt-question-single-slot
[questionNumber]="3"
[question]="'What is your birth year'">
<div class="single-slot">
<input type="number" name="single-standard-year"
id="single-standard-year">
</div>
</dvt-question-single-slot>

MultiSlot Content Projection

MultiSlot Content Projection refers to creating a component into which you can project into multiple slots or directives. We have now seen that we can have a single input assigned to a question, but what if the question requires more than one input?

For instance, what if we ask a user for their contact information? This would consist of an email, a telephone number, and a postal address. Firstly I am going to show you how we would achieve this without MultiSlot Content Projection.

<div>4. What is your contact details?</div><label for="multi-standard-phone">What is your contact number?
</label>
<input type="tel" id="multi-standard-phone"
name="multi-standard-phone" placeholder="071 071 0717"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
<label for="multi-standard-email">What is your email?</label>
<input type="email" name="multi-standard-email"
id="multi-standard-email">
<label for="multi-standard-address">What is your postal address?
</label>
<input type="text" name="multi-standard-address"
id="multi-standard-address">
<div>What is your preferred contact means?</div><input type="radio" id="multi-standard-phone-cbx"
name="contact" value="phone">
<label for="multi-standard-phone-cbx">Call Me</label>
<input type="radio" id="multi-standard-email-cbx"
name="contact" value="email">
<label for="multi-standard-email-cbx">E-mail Me</label>
<input type="radio" id="multi-standard-postal-cbx"
name="contact" value="postal">
<label for="multi-standard-postal-cbx">Post Me</label>
<input type="checkbox" id="multi-standard-terms"
name="multi-standard-terms">
<label for="terms">I accept the terms and conditions</label>

MultiSlot Content Projection is very useful because we can project multiple directives into the component and DOM.

Please note for this example that I have used many different input types such as text, radio, email, tele, and checkbox. This is to demonstrate that you can insert any element into your content slot.

<div class="question-m-slot">
<div>{{questionNumber}}. {{question}}?</div>
<ng-content select=".part-1-label"></ng-content>
<ng-content select=".part-1-input"></ng-content>
<ng-content select=".part-2-label"></ng-content>
<ng-content select=".part-2-input"></ng-content>
<ng-content select=".part-3"></ng-content>
<div>{{subQuestion}}?</div>
<ng-content select=".part-sub-question"></ng-content>
<ng-content select=".part-terms-and-conditions"></ng-content>
</div>

In this code snippet, we created a component called dvt-question-multi-slot, which will make use of content projection to insert multiple inputs into the questions.

@Input() question: string;
@Input() questionNumber: number;
@Input() subQuestion: string;

I have split the question content for parts one and two, to show that it would not matter in which order we project content because the component will insert the DOM elements into the correct slots by using the class names.

Part three on the other hand did not split the content and is a good example that we do not need to split the directives, because you can project multiple elements.

There was also a sub-question part added, that does also makes use of the @Input decorator for the question, as well as a content slot where the input can be injected.

In total there are 7 content slots, each not bound to a specific type of element, thus meaning that we can inject any type of content into any of these 7 slots respectively.

<dvt-question-multi-slot
[questionNumber]="4"
[question]="'What is your contact details'"
[subQuestion]="'What is your preferred contact means'">
<div class="part-sub-question">
<input type="radio" id="multi-slot-phone-cbx"
name="contact-multi" value="phone">
<label for="multi-slot-phone-cbx">Call Me</label>
<input type="radio" id="multi-slot-email-cbx"
name="contact-multi" value="email">
<label for="multi-slot-email-cbx">E-mail Me</label>
<input type="radio" id="multi-slot-postal-cbx"
name="contact-multi" value="postal">
<label for="multi-slot-postal-cbx">Post Me</label>
</div>
<div class="part-terms-and-conditions">
<input type="checkbox" id="multi-slot-terms"
name="multi-slot-terms" value="accept">
<label for="multi-slot-terms">I accept the terms and
conditions</label>
</div>
<div class="part-1-label">
<label for="multi-slot-phone">What is your contact number?
</label>
</div>
<div class="part-2-label">
<label for="multi-slot-email">What is your email?</label>
</div>
<div class="part-1-input">
<input type="tel" id="multi-slot-phone"
name="multi-slot-phone" placeholder="071 071 0717"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
</div>
<div class="part-2-input">
<input type="email" name="multi-slot-email"
id="multi-slot-email">
</div>
<div class="part-3">
<label for="multi-slot-address">What is your postal address?
</label>
<input type="text" name="multi-slot-address"
id="multi-slot-address">
</div>
</dvt-question-multi-slot>

The content in this example will always be injected in the correct order as placed in dvt-question-multi-slot. This is another benefit of using Content Projection.

It also demonstrates the fact that we can inject any elements, directives, or even components into any of the content slots.

Reuseable Components combined with Content Projection

Although the previous example was good to demonstrate MultiSlot Content Projection there is a simpler solution where the code can be refactored and components can be reused, with a demonstration in the code snippet below.

<div>
<label>
<ng-container *ngIf="questionNumber">
{{questionNumber}}.
</ng-container>
{{question}}?
</label>
<ng-content select=".question-insert"></ng-content>
</div>

For this example, we refactored our question to be a SingleSlot Content Projection as dvt-question-single-slot-refactor. We will add a condition to check if we would like to display a question number or not, because if a sub-question forms part of a question we do not always want to add a number.

<dvt-question-single-slot-refactor
[questionNumber]="4"
[question]="'What is your contact details'">
<div class="question-insert"> <dvt-question-single-slot-refactor
[question]="'What is your contact number'">
<div class="question-insert">
<input type="tel" id="multi-ref-phone"
name="multi-ref-phone" placeholder="071 071 0717"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
</div>
</dvt-question-single-slot-refactor>
<dvt-question-single-slot-refactor
[question]="'What is your email'">
<div class="question-insert">
<input type="email" name="multi-ref-email"
id="multi-ref-email">
</div>
</dvt-question-single-slot-refactor>
<dvt-question-single-slot-refactor
[question]="'What is your postal address'">
<div class="question-insert">
<input type="text" name="multi-ref-address"
id="multi-ref-address">
</div>
</dvt-question-single-slot-refactor>
<dvt-question-single-slot-refactor
[question]="'What is your preferred contact means'">
<div class="question-insert">
<input type="radio" id="multi-ref-phone-cbx"
name="contact" value="phone">
<label for="multi-ref-phone-cbx">Call Me</label>
<input type="radio" id="multi-ref-email-cbx"
name="contact" value="email">
<label for="multi-ref-email-cbx">E-mail Me</label>
<input type="radio" id="multi-ref-postal-cbx"
name="contact" value="postal">
<label for="multi-ref-postal-cbx">Post Me</label>
</div>
</dvt-question-single-slot-refactor>
<dvt-question-single-slot-refactor
[question]="'Do you accept the terms and conditions'">
<div class="question-insert">
<input type="checkbox" id="multi-ref-terms"
name="multi-ref-terms">
<label for="multi-ref-terms">I accept the terms and
conditions</label>
</div>
</dvt-question-single-slot-refactor>
</div></dvt-question-single-slot-refactor>

Ultimately we want to be able to reuse code, and as seen above, we reuse the component dvt-question-single-slot-refactor for every question, as well as a container question.

Conditional Content Projection

Conditional Content Projection refers to creating a component into which you can conditionally project into one or more slots/directives. Sometimes we would need to display various DOM elements or content based on certain criteria.

We would like to ask our employees what Highschool they attended, we would first need to know whether they passed or achieved matriculation.

<div>
<h2>5. Have you matriculated?</h2>
<input type="checkbox" id="condi-terms"
name="condi-terms"
[checked]="( matriculated === true )"
(change)="matriculated = $event.target.checked;">
<label for="condi-terms">I have matriculated high school.</label>
<div *ngIf="matriculated">
<label for="condi-school">
From which highschool have you matriculated?
</label>
<input type="text" name="condi-school" id="condi-school">
</div>
</div>

There are two methods discussed in this article in how you can refactor this normal HTML to implement Content Projection.

Method 1

For method one we will use SingleSlot Content Projection to conditionally display the various parts of the question.

<div>
<label>
<ng-container *ngIf="questionNumber">
{{questionNumber}}.
</ng-container>
{{question}}?
</label>
<ng-content select=".question-insert"></ng-content>
</div>

We are going to refactor the code for the component dvt-conditional-slot-method-one-slot as seen below.

<dvt-conditional-slot-method-one-slot
[questionNumber]="5" [question]="'Have you matriculated'">
<div class="question-insert">
<input type="checkbox" id="mo-matriculated"
name="mo-matriculated"
[checked]="( matriculated === true )"
(change)="matriculated = $event.target.checked;">
<label for="mo-matriculated">I have matriculated.</label>
<dvt-conditional-slot-method-one-slot *ngIf="matriculated"
[question]="'From which highschool have you matriculated'">
<div class="question-insert">
<input type="text" name="mo-school" id="mo-school">
</div>
</dvt-conditional-slot-method-one-slot>
</div></dvt-conditional-slot-method-one-slot>

In the code snippet, we have reused the same component twice to display different content within itself with or without conditions.

Method 2

For method two we will use MultiSlot Content Projection to conditionally display the various parts of the question.

<div>
<label>
<ng-container *ngIf="questionNumber">
{{questionNumber}}.
</ng-container>
{{question}}?
</label>
<ng-content select=".part-1"></ng-content>
<ng-content select=".part-2"></ng-content>
</div>

We are going to refactor the code for the component app-conditional-slot-method-two-slot as seen below.

<app-conditional-slot-method-two-slot
[questionNumber]="5"
[question]="'Have you matriculated'">
<div class="part-1">
<input type="checkbox" id="mt-matriculated"
name="mt-matriculated"
[checked]="( matriculated === true )"
(change)="matriculated = $event.target.checked;">
<label for="mt-matriculated">I accept the terms and
conditions.</label>
</div>
<app-conditional-slot-method-two-slot class="part-2"
ngIf="matriculated"
[question]="'From which highschool have you matriculated'">
<div class="part-1">
<input type="text" name="mt-school" id="mt-school">
</div>
</app-conditional-slot-method-two-slot>
</app-conditional-slot-method-two-slot>

In the code snippet, we have reused the same component twice to display different content within itself with or without conditions. Also, note that you can place the condition on the reused component.

In Conclusion,

Content Projection is a powerful directive used within Angular and it allows us to encapsulate elements into components that are fully customisable. This is achieved by injecting custom HTML into these components. In other words, parts of the component would change based on the currently displayed screen and DOM.

This article demonstrates how Content Projection can simplify your code by breaking down complex views and creating reusable directives that remove duplication in your views.

I hope you can now see that Transclusion can help you improve your component reusability and ultimately to simplify dynamic DOM manipulation.

If you would like to learn more about this please have a look at the Official Documentation here, where you can learn other topics such as the default template.

Thank you for reading my article. I hope you learned something new and exciting, and that you now have a better understanding of Content Projection or Transclusion and the various types you can use to improve your code.

Please feel free to leave a comment if you need clarification or if you have something to add. You can also find a complete GitHub repository for this topic here.

If you enjoyed this article please also check the DVT Engineering space for more interesting articles and follow me for future topics.

--

--