How to Build Composable & Compound Components in Angular

Build Flexible and Powerful Components with Angular

Featured on Hashnode

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>

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>

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-titletext="Amazing title"
   \></kendo-chart-title>
   <kendo-chart-series></kendo-chart-series>
 <kendo-charts>

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.

  • What 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 utilize 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);
 }
}

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) {  }
}

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>

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>

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>

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;
}

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

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

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>

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;

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

<select #country (change)="selectedCountry(country.value)">

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;

 }

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>

dynamic-change.gif

Learn more about ContentChild

Dependency Injection Component

The country component uses ContentChild for each component. However, what happens if the developer wants to use the flag component two or five times, or add a banner component when selecting the country, something 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>

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 values. 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);
 }
}

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) {

  }

}

Use the country component state in the template:

*ngIf="country.selected$ |async as countryName"

refactor.gif

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, learned how to use Content Projection, and created an excellent API for our components.

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