DEV Community

Cover image for How to Build Compound Components in Angular
Dany Paredes for This is Angular

Posted on • Updated on • Originally published at danywalls.com

How to Build Compound Components in Angular

When we need to have different versions and use cases and make it flexible to the changes, however, some stuff becomes a bit complex.

For example, we need to show the list of the country in one case, the company flag or the name of the selected country, in our head comes with some solutions using Angular.

Option A Create a single component with all the logic and use cases. With ng-container and ngIf directive. It creates a component with a vast amount of logic and interaction in a single component.

<hello name="{{ name }}"></hello>
<select>
    <option>A</option>
    <option>B</option>
</select>
<ng-container *ngIf="country">
 <h1>The country</h1>
</ng-container>
<ng-container *ngIf="flag">
 <h1>The flag</h1>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

Option B: Create a version for each case and provide a unique experience for each scenario like:

<country></country>
<country-with-message></country-with-message>
<country-with-flag></country-with-flag>
Enter fullscreen mode Exit fullscreen mode

Alternatively, use the Compound Component Pattern, one component to control the state and interaction between the user and the state, and other components for rendering and reacting to changes.

What is Compound Component?

It is a group of components or child components working together to help us to create a complex component; some frameworks like Kendo UI play with components connected with other components in their context.

For example, The Kendo Charts kendo-chart, kendo-chart-title, and kendo-chart-series work together to share data, state, and context to create a fantastic chart.

<kendo-chart>
   <kendo-chart-title
    text="Amazing title"
   \></kendo-chart-title>
   <kendo-chart-series></kendo-chart-series>
 <kendo-charts>
Enter fullscreen mode Exit fullscreen mode

Also, it is a clear and semantic code for others when working with our components, giving them a straightforward way to work with them.

Creating the component is easy, but creating a powerful, flexible component needs checkpoints before starting.

  • How does the component syntax look like?
  • Will the component emit or interact with other components?
  • Will the component share State?
  • Will it have one or more child components?

To learn about and create compound components, we will leverage some Angular features like NgContent, ContentChild Decorator, and Component Dependency Injection.

The List Of Countries

To have an isolated scope for our components, we provide the list of countries with CountryService to use in the component.

import { Injectable } from '@angular/core';
import {Observable, of} from "rxjs";

interface country {
 name:string;
 code: string | null;
}

@Injectable({
 providedIn: 'root'
})
export class CountryService {
 countries: country[] = [
  {name: 'Australia', code: 'AU'},
  {name: 'Austria', code: 'AT'},
  {name: 'Azerbaijan', code: 'AZ'},
  {name: 'Bahamas', code: 'BS'},
 ]

 getCountries(): Observable<country[]> {
    return of(this.countries);
 }
}
Enter fullscreen mode Exit fullscreen mode

The Country Component

Let us build the country component as outer, with the HTML select to show the list of countries provided by CountryService in the constructor and use the async pipe to subscribe to the countryService.

Here is the full code:

import {Component } from '@angular/core';
import {CountryService} from "../services/country.service";

@Component({
 selector: 'app-country',
 templateUrl: './country.component.html',
 styleUrls: ['./country.component.css']
})

export class CountryComponent  {
 countries$ = this.countryService.getCountries();
 constructor(private countryService: CountryService) {  }
}
Enter fullscreen mode Exit fullscreen mode

Using the async pipe, we subscribe to countries$ observable and the ngFor to iterate over the list.

<select>
  <option *ngFor="let country of countries$ | async" [value]="country.code">
​    {{ country.name }}
  </option>
</select>
Enter fullscreen mode Exit fullscreen mode

Read more about *ngFor and async.

Content Projection

The country component needs to be flexible and have a simple and semantic API for other developers to use, something like:

<country>
  <country-flag></country-flag>
  <country-selected></country-selected>
</country>
Enter fullscreen mode Exit fullscreen mode

We need to use content projection to allow the country component to accept content from other components.

Content projection allows the component to get content by adding the element into the country component HTML to allow of content from other components.

Adding the ng-content element, the country component can render and use the content from those nested components.

<select>
 <option *ngFor="let country of countries$ | async" [value]="country.code">
  {{ country.name }}
 </option>
</select>
<ng-content>
</ng-content>
Enter fullscreen mode Exit fullscreen mode

Read more about Content Projection.

The Childs Components

Next, we create the components flag and message with a @Input selected property to use with ngIf to show his content.

import { Component, Input } from '@angular/core';
@Component({
 selector: 'app-country-flag',
 templateUrl: './country-flag.component.html'
})

export class CountryFlagComponent  {
 @Input() selected!: string;
}
Enter fullscreen mode Exit fullscreen mode

The CountryFlag renders the image using countryflagapi.com when getting the selected value.

<div *ngIf="selected"><img src="https://countryflagsapi.com/png/{{selected}}"/>
</div>
Enter fullscreen mode Exit fullscreen mode

The code for CountrySelectedComponet uses the same logic.

import { Component, Input, OnInit } from '@angular/core';

@Component({
 selector: 'app-country-selected',
 templateUrl: './country-selected.component.html'
})

export class CountrySelectedComponent {
 @Input() selected!: string;
}

<div *ngIf="selected">
 Thanks {{selected}} is a great country!
</div>
Enter fullscreen mode Exit fullscreen mode

Done, Next we start to communicate the Country Component with our Child components.

Using @ContentChild

In the country component context, we want to interact with our components, CountrySelectedComponent and CountryFlagComponent.

Using the @ContentChild decorator to get a reference for these components.

@ContentChild(CountrySelectedComponent) countrySelected!: CountrySelectedComponent;
@ContentChild(CountryFlagComponent) countryFlag!: CountryFlagComponent;
Enter fullscreen mode Exit fullscreen mode

Create selectedCountry method and the change event for selection to get the country selected.

<select #country (change)="selectedCountry(country.value)">
Enter fullscreen mode Exit fullscreen mode

The selectedCountry method updates the selected property for each component, and it reacts to changes.

selectedCountry(select:HTMLSelectElement):void {
  this.countrySelected.selected = select.value;
  this.countryFlag.countrySelected = select.value;

 }
Enter fullscreen mode Exit fullscreen mode

The country component is ready to react when the input changes and adds the components CountrySelectedComponent or, CountryFlagComponent into his body.

<app-country>
 <app-country-selected></app-country-selected>
 <app-country-flag></app-country-flag>
</app-country>
Enter fullscreen mode Exit fullscreen mode

Learn more about ContentChild

Dependency Injection Component

The country component uses ContentChild for each component. However, what happens if the developer wants to use two or five times the component flag or add a banner component when selecting the country, some like:

<app-country>
 <app-country-selected></app-country-selected>
 <app-country-flag></app-country-flag>
 <app-country-flag></app-country-flag>
 <app-banner></app-banner>
</app-country>
Enter fullscreen mode Exit fullscreen mode

The official Angular documentation says:

@ContentChild Use to get the first element or the directive matching the selector from the content DOM. If the content DOM changes and a new child matches the selector, the property will be updated.

The components react to changes, and the new component app-banner needs to add a reference in the CountryComponent. It does not scale for future changes.

Refactor

Remove ContentChild references to static components, create a subject to use as a communication bus, and use the next method to emit subscription value. The final code looks like this:

export class CountryComponent  {
 countries$ = this.countryService.getCountries();
 selected$: Subject<string> = new Subject<string>();
 constructor(private countryService: CountryService) { }
 changed(value: any) {
  this.selected$.next(value);
 }
}
Enter fullscreen mode Exit fullscreen mode

Inject into the constructor for child components to use the selected$ observable and subscribe in the template using the async pipe to store the value in the countryName variable.

The code looks like this:

export class CountryFlagComponent  {

  constructor(public country: CountryComponent) {

  }

}
Enter fullscreen mode Exit fullscreen mode

Use the country component state in the template:

*ngIf="country.selected$ |async as countryName"
Enter fullscreen mode Exit fullscreen mode

Perfect! All components react to changes in the country's context, and other components use the selected value from CountryComponent injecting him into the constructor.

Recap

In this post, we have implemented the Compound Component Pattern in Angular using dependency injection, how to use Content Projection, and created an excellent API for our components.

Read the complete code: https://github.com/danywalls/compound-components-angular.

Top comments (0)