fbpx Skip to content

Aquent | DEV6

Creating Reusable Components with Angular Content Projection (ng-content)

Written by: Traian Rusu

In this blog post we will go over creating reusable components in Angular using ng-content.

Reusable components can be a powerful tool and one way to accomplish this easily in Angular is using ng-content aka content projection.

Often in web development we need to create components that need to be used in multiple places in our application. Usually these have some common features such as layout or functionality or both. These components also may have sections that show custom content that changes depending on where they are displayed. For example, we may consider a ‘card’ component that collapses and expands and has a common layout in its header but shows some custom content in the body of the card. In this case the header layout and the collapse/expand functionality would be the common features, but the actual content of the card varies for each instance of the card.

Options

There are many ways to make such components. One option is to write the component and then just copy all the code where it’s needed. This is not ideal because any time we change the ‘common’ component we need to copy the changes wherever this component is shown.

Another option is to write a component that accepts some properties that change the behaviour or layout. This is fine for simple cases where we just need to pass a single string or value but if  we want the custom part to be more complex, and possibly mix html and JavaScript or TypeScript, this is difficult to achieve using this method and we end up with many parameters and conditional statements.

The other option we have is to make a reusable component that contains all the common functionality and then pass in another component or html for the section that varies. Using this pattern, we only have to write the code for the reusable component once, then just insert the component tag anywhere we need it.

Ng-Content

One way to implement this in Angular is using ng-content or “content projection”. Using ng-content we can create a re-usable component which contains all the common logic and layout but also allows the you to pass in any custom components or html you wish. The way this works is pretty simple to implement.  Below, we create a component with all of our common layout and logic, then inside that component’s html file we have a <ng-content> tag which renders or “projects” any content that is placed between the tags of the reusable component. When any changes are needed to our common component all we need to do is change the implementation of our one common component and then anywhere that our common component is used will automatically reflect the changes.

For example, here’s a very simple expandable/collapsible card component:

<div class="card-container">
  <div class="header" (click)="onClick()">
    This is the common header!
  </div>
  <div class="main-content" *ngIf="isExpanded">
    This is the common main content!

    <!--Here the custom content will be rendered-->
    <ng-content></ng-content>
  </div>
</div>

The header section contains some of our common layout and logic. In our very simple case, it is just a header with some text with a click handler that sets the variable isExpanded to true or false which will expand/collapse the main content section. Then we have the main-content section with an ngIf which is shown depending on that isExpanded variable.

In our typescript code we just have an onClick method which toggles the isExpanded variable.

export class CardComponent implements OnInit {
  isExpanded = false;

  constructor() { }

  ngOnInit() {
  }

  onClick(){
    this.isExpanded = !this.isExpanded;
  }

The above html and typescript code is where our common logic and layout lives. This is a very simple example for demonstration purposes, a real world example could have much more complex logic and layout but the overall pattern would be the same.

The last part of our implementation is where we add the tags for our reusable component.

<app-card>
  <p> This is custom content that is projected! </p>
</app-card>

In this example we add some custom html in between our component’s tags. This content is then rendered where the ng-content tags are in our component.

We can also pass in full components in between the tags and it works in the same way, those components in between the tags are rendered where the ng-content tags are.

<app-card>
  <app-users></app-users>
</app-card>

Multiple Ng-Content’s

We can also add multiple ng-contents to project content to multiple places. Ng-content has a select attribute which allows us to use a css selector to choose between the different projected content. For example we can use <ng-content select=”.main”></ng-content> and then in our projected component we add this class onto an element and then ng-content injects that element into the ng-content with the matching select tag.

<div class="main-content" *ngIf="isExpanded">
    This is the common main content!
    <ng-content select=”.main”></ng-content> 
    <p>In between main and end</p>
    <ng-content select=”.end”></ng-content> 
    <p>In between end and default</p>
    <ng-content></ng-content>
    <p>after default</p>
  </div>
<app-card>
  <p class="main"> This is the first content that is projected! </p>
  <p class="end"> This is the second content that is projected! </p>
  <p> This is the default content that is projected! </p>
</app-card>

In this example we use ng-content 3 different times. The first 2 have a select property that matches any element by class. The final one has no select, and this is treated as the default where any content without a matching class is rendered.

Alternatives and Drawbacks

As you can see, this is quite simple to implement but can be pretty powerful if used correctly. Now, this is not the only way to achieve this and there may be other ways that allow you more control, for example, using ComponentFactoryResolver, but ng-content is a clean and simple way of doing this directly from the templates.

This solution does come with a few drawbacks though. The biggest one is that unfortunately our inner component that we pass in between our reusable component tags will not automatically have access to any properties in the wrapping parent component. To achieve this, we either need to directly pass in the values to the inner component or use a different approach like having a service or a directive for that.

Creating reusable components is a good practice in the right situations and can save a lot of time and effort in development. Just like anything in Javascript, there are many ways to achieve this, luckily for us Angular developers, Angular gives us a very clean and simple way to implement such components.