Angular ng-content and Content Projection - The Complete Guide

Content Projection and the ng-content directive are among the most valuable features Angular provides for building reusable components. This guide explores how to leverage this mechanism to craft components with an API that is both minimal and highly effective. The focus is as much on sound component architecture as it is on the mechanics of content projection.

To understand the practical application, we will build a small component known as a Font Awesome Input Box. Through this exercise, we will examine the mechanics of content projection, the rationale for its use, and the substantial design improvements it can bring to certain components.

The finished component we will create is available here, packaged in the Angular Package Format.

Table Of Contents

This guide encompasses the following topics:

  • What Problem is Content Projection Trying to Solve?
  • An example of a component that would benefit from content projection
  • Component Design Problem 1 - Supporting all the HTML Properties of an HTML Input
  • Component Design Problem 2 - Integration with Angular Forms
  • Component Design Problem 3 - Capturing plain browser events of elements inside a template
  • Component Design Problem 4 - Custom third party input properties
  • The Key Problem With The Initial Design
  • Designing the Same Component Using Content Projection
  • How To apply styles to elements projected via ng-content
  • Interacting with Projected Content (inside ng-content)
  • Multi-Slot Content Projection
  • Conclusions

It's worth noting that the @ContentChild and @ContentChildren decorators now have modern signal-based counterparts: the contentChild() and contentChildren() view queries. More details are available here:

Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren (Complete Guide)

What Problem is Content Projection Trying to Solve?

Let's start with the fundamentals. To truly grasp content projection, we must first identify the specific design challenges this feature addresses. A clear understanding of the problem ensures we use the tool appropriately and avoid misapplication.

To illustrate these challenges, we will construct a small component *without* using content projection. Observing the issues that arise will provide a clear context for the solution.

What We Are About To Build

Our goal is a Font Awesome Input Box that mimics the appearance and behavior of a standard HTML input, enhanced with a small icon inside its border. The icon can be any of the ones available in the Font Awesome open-source library. Let's preview what we are creating.

Encapsulating a common HTML Pattern

Integrating an icon within an input box is a widespread HTML pattern that significantly improves user recognition. For instance, consider these text boxes:

au-input examples

With both the icon and the placeholder text present, a separate field label to the left is largely unnecessary, which is particularly beneficial on mobile devices.

How Does This Component Work?

Standard HTML inputs cannot render images. Yet this component looks like a native input, complete with a blue focus border and keyboard support for Tab and Shift-Tab. How is this achieved? Internally, the component employs a very common HTML pattern:

  • It wraps a plain HTML input and an icon within a DIV.
  • The input's own border and outline are hidden, and a similar border is applied to the surrounding DIV.
  • The focus and blur events of the inner input are monitored to add or remove a focus border on the wrapper DIV.

Thus, even this seemingly simple component required certain techniques to look and function like a plain input.

Design Goals

Our objective is to encapsulate this common HTML pattern into a versatile Angular component with the following characteristics:

  • easy composability with other Angular pieces
  • seamless integration with Angular Forms

With these goals in mind, let's examine an initial implementation *without* content projection to identify its shortcomings. Let's preview how this version might be used:

Here, the component is a custom element, fa-input, which accepts an icon name as an input and emits changes in the text field's value. This is an intuitive design choice and is likely the first option many would consider. However, as we will see, this approach introduces significant problems.

Component API Design - An Initial Attempt

Below is the initial version of the component's implementation:

Take a guess at what the most severe issue with this design is while we walk through the code.

The template displays the core idea: a combination of an icon and a plain HTML input. Let's start by looking at the styling before moving to the class logic:

These styles reveal some key decisions:

  • the inner HTML input has had its borders and outline completely removed
  • however, the host element itself is given a border, creating the illusion of a standard input field
  • the input's focus state is simulated by applying a CSS class, focus, to the host element

Back in the component class, we can see how these elements are connected:

  • the public API includes an icon string property to determine which icon is displayed (e.g., an envelope, a lock)
  • a custom output event, value, is defined to emit new values whenever the text input is modified
  • the focus behavior is implemented by listening to the native input's focus and blur events and toggling the focus CSS class on the host via @HostBinding

The component functions as intended. Yet, when integrated into a real application, we will encounter a series of issues. Let's outline four of them, starting with:

Component Design Problem 1 - Supporting all the HTML Properties of an HTML Input

Designed as a replacement for a standard HTML input, our component fails to support any of its native properties. For example, consider a standard email input with autocomplete disabled and a placeholder. None of these standard browser features are available on our component, and these are just a few of many.

There are currently 31 HTML properties listed at W3schools for inputs, not to mention the full range of HTML ARIA Accessibility attributes.

To support all of these, we would need to implement something like this:

In essence, we would have to explicitly forward every single component input property to the internal HTML input within the template. This approach, while possible, would be extremely tedious. Worse, it doesn't address other more profound issues in the current design.

Component Design Problem 2 - Integration with Angular Forms

Another question arises: what if we need this input to be part of an Angular Form?
We would need to also forward all form-related directives, such as formControlName, to the inner plain input.

Component Design Problem 3 - Detection of plain browser events

What if we want to listen for a standard DOM event, like keydown, on the inner input field? While we could theoretically forward all events that don't bubble by default, it's a complex and messy solution.

It's cumbersome, but feasible. However, we would soon hit a wall with a problem that has no straightforward workaround.

Component Design Problem 4 - Custom third party properties

When building forms, third-party systems may require specific HTML custom data- attributes to be populated, especially in scenarios where a traditional page submission occurs rather than an Ajax request.

This is more problematic than the previous points, as we have no way to anticipate what these attribute names will be. At this point, it's clear that a significant number of common use cases are poorly served by this component.

So, what is the real, fundamental issue at the heart of this design?

The Key Problem With This Design

The core issue lies in our approach of hiding the HTML input within the component's template.

This creates a barrier between the external template, which knows the specific custom properties to apply, and the actual HTML input element.

this barrier is the root cause of every design flaw we've listed.

Concealing the input forces us to build intricate logic to pass properties and events in and out of the template to accommodate various use cases.

The good news is that content projection provides a way to support all of these use cases and beyond.

Designing the Same Component Using Content Projection

Let's redesign the component's API. Instead of hiding the input element inside the component, we can provide it as part of the component's *content*:

Notice that the form field is no longer a component input property. Instead, it is placed in the content area of the fa-input HTML tag. This style of API is common in standard HTML elements, like select boxes or lists:

Angular Core provides a way to do something similar in our own components!

We can query the content within the component tag and use it as a configuration API in the internal template, using the @ContentChild and @ContentChildren decorators. But we can do more than just query. We can also seamlessly use this content *directly* inside the component's template.

This means we can take the HTML input placed within the fa-input tag and project it directly into the Font Awesome template using the ng-content directive:

This new version is still incomplete; it doesn't yet support the simulated focus. However, it resolves all the shortcomings mentioned earlier because we now have direct access to the HTML input.

Interestingly, this change also introduces a new, albeit minor, issue. Take a look at the input box:

Fonte Awesome double inputs

See the double border? It seems our previous styling to remove the input's border is no longer effective. Additionally, the focus behavior is gone.

Despite solving our initial API problems, we now have two new questions regarding ng-content:

  • how can we style the projected content?
  • how can we interact with the projected content?

How To Apply styles to elements projected via ng-content

Let's first understand why our previous styles stopped working. Our styles looked like this, located in fa-input.component.css:

The problem is that styles in a component's file are automatically scoped. Angular adds a unique attribute to *every* element defined within that component's template at runtime. Let's inspect the runtime HTML to understand:

Here is a simplified view of the HTML to clarify the situation:

  • each element from the fa-input template, like the icon tag, receives a special _ngcontent-c0 attribute unique to this component
  • all component styles target only elements with this attribute
  • consequently, the component's styles do NOT affect the projected input, as it lacks this _ngcontent-c0 attribute
  • this is expected, as the input originates from an external template

Styling projected content

To style the projected input and fix the double border, we need to adjust our CSS as follows:

How do these new styles work? Let's break down the changes:

  • we use the :host selector to scope the styles to this component's host element
  • we then apply the ::ng-deep modifier, which tells Angular to bypass component scoping and apply the styling to any descendant elements

Here's the resulting runtime CSS to illustrate this:

The styles remain anchored to the component, but they are now applied to *all* inputs within it, including the projected one!

This demonstrates the styling approach. Now, let's fix the second issue: how to interact with the projected content to simulate the focus.

Interacting with Projected Content inside ng-content

To simulate the focus functionality, our fa-input component needs to know when the projected input receives or loses focus. We can't directly attach event listeners to the ng-content tag itself.

Instead, the most effective method is to apply a dedicated directive to the projected input. Let's create a directive named inputRef and apply it to the HTML Input:

We can use this directive to track the focus state as well:

Here's how this directive works:

  • it defines a focus property that reflects whether the underlying native Input has focus
  • it uses the @HostListener decorator to listen to the native focus and blur DOM events

We can now use this directive within the Font Awesome Input component. By using the @ContentChild decorator, we can inject the directive instance into the component's class. Then, we use the boolean focus property and @HostBinding to apply the focus CSS class.

With this new implementation, we have a fully working component that is simple to use and inherently supports all HTML input properties, accessibility attributes, third-party data props, and Angular Forms – all thanks to content projection.

So far, we have projected the entire content of fa-input. But what if we only want to project a portion of it?

Multi-Slot Content Projection

Let's consider a scenario where we want to project not just the HTML input but also the icon. We can put multiple types of content within the fa-input tag, for example:

We can then selectively consume these different types of content using the select property of ng-content:

These selectors target a specific element type (input or icon). However, we can also target elements by a CSS class or combine multiple selectors. For instance, this selector would target an input with a class named test-class:

It's also possible to capture content that doesn't match any selector. For example, the following would still project the input element:

An <ng-content></ng-content> without a selector acts as a catch-all. It fetches and projects all content that does not match any of the defined selectors.

In this case, that means all content that is not an icon tag, which is the HTML Input.

Conclusions

As we've seen, understanding how the ng-content core directive works is just as important as recognizing the typical scenarios where it is beneficial. It enables a component design where key internal details are not sealed within the component's template but are instead provided as projection input, which in certain circumstances leads to a much simpler and more flexible architecture.

I hope you found this guide helpful. I invite you to subscribe to our newsletter for the latest Angular news, free courses, and PDFs.

If you're interested in deepening your understanding of advanced Angular Core features, we suggest checking out the Angular Core Deep Dive course, where content projection is covered in much greater detail.

For those who are just getting started with Angular, have a look at the Angular for Beginners Course:

Angular ng-content and Content Projection - The Complete Guide — figure 3

If you enjoyed this post, you might also find these popular articles interesting: