💲 Black Friday Special

Get an extra 35% OFF everything with coupon code BLACK_FRIDAY

0 days
00 hours
00 mins
00 secs

Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Angular Component Composition and Communication with <ng-content>

The fundamentals of component architecture promote a strict one-way data flow pattern, which helps to make things like testing, state management and component interaction simpler and more effective.

Despite the benefits of one-way dataflow, this approach of binding to @Input and @Output properties can quickly lead to “@Input soup”…

When there are lots of nested components it’s common to keep passing down the same piece of data to each component.

When something change, we would then emit an event from a child component all the way up the tree, to inform the parent component that something has changed.

While being a great practice and design, it leads to a lot of wiring up and duplicated @Input and @Output.

So I’d like to show you how to use content projection with Angular’s <ng-content> directive - it can remove a lot of your code, wiring up, and simplify your component design.

Not only that, but it will enhance your component’s testing and also make them more composable. Sounds too good to be true? It isn’t!

I’ve built a simple audio app to demonstrate these concepts, we’ll look at both @Input and <ng-content> approaches as we go, but first check it out:

Here’s what the components look like:

<app>
  |
  └─ <player>
        |
        ├─ <player-info>
        |
        └─ <player-actions>
                  |
                  ├─ <player-button>
                  |
                  └─ <player-button>

Looks fairly simple, but let’s assume we fetch the data inside <app> and want to pass it down, we then have a lot of @Input bindings to get the data down into each component.

Similarly, we would need to bind an EventEmitter to an @Output as well:

<app>
  |
  └─ <player> // @Input() audio + @Output() toggle + @Output() stop
        |
        ├─ <player-info> // @Input() audio
        |
        └─ <player-actions> // @Input() audio + @Output() toggle + @Output() stop
                  |
                  ├─ <player-button> // @Output() toggle
                  |
                  └─ <player-button> // @Output() stop

With the conceptual flow in mind, let’s look at the actual code of how these nested components are setup:

@Component({
  selector: 'app',
  template: `
    <player
      [audio]="audio"
      (toggle)="toggle()"
      (stop)="stop()">
    </player>
  `,
})
export class AppComponent implements OnInit {
  audio: HTMLAudioElement;

  ngOnInit() {
    this.audio = new Audio('sound.mp3');
  }

  toggle() {
    if (this.audio.paused) {
      this.audio.play();
    } else {
      this.audio.pause();
    }
  }

  stop() {
    this.audio.pause();
    this.audio.currentTime = 0;
  }
}

Our <app> then renders the <player> component:

@Component({
  selector: 'player',
  template: `
    <div
      class="player">
      <player-info
        [audio]="audio">
      </player-info>
      <player-actions
        [audio]="audio"
        (toggle)="toggle.emit()"
        (stop)="stop.emit()">
      </player-actions>
    </div>
  `,
})
export class PlayerComponent {
  @Input() audio: HTMLAudioElement;
  @Output() toggle: EventEmitter<void> = new EventEmitter<void>();
  @Output() stop: EventEmitter<void> = new EventEmitter<void>();
}

The <player> component accepts the audio as an @Input and passes it down to <player-info> and <player-actions>.

Our <player-info> simply renders the data from another @Input:

@Component({
  selector: 'player-info',
  template: `
    <div
      class="player-info">
      {{ audio.currentTime | duration }} / {{ audio.duration | duration }}
    </div>
  `,
})
export class PlayerInfoComponent {
  @Input() audio: HTMLAudioElement;
}

That’s the end of the road for <player-info>, but until this point it’s taken 2x @Input bindings.

Let’s move to the <player-actions> and see what’s happening there too:

@Component({
  selector: 'player-actions',
  template: `
    <div
      class="player-actions">
        <player-button
          [label]="audio.paused ? 'Play' : 'Pause'"
          (click)="toggle.emit()">
        </player-button>
        <player-button
          [label]="'Stop'"
          (click)="stop.emit()">
        </player-button>
    </div>
  `,
})
export class PlayerActionsComponent {
  @Input() audio: HTMLAudioElement;
  @Output() toggle: EventEmitter<void> = new EventEmitter<void>();
  @Output() stop: EventEmitter<void> = new EventEmitter<void>();
}

More @Input bindings, but also two @Output bindings to emit a click event back to the parent.

You can see I’m also using [label] to pass some custom text down into the button - we’ll learn how <ng-content> can help with this too in a moment.

We can skip a custom @Output on the <player-button> as the click event will bubble up from the component. When it does, we can locally call toggle.emit() or stop.emit().

The button then has 1x @Input for the label:

@Component({
  selector: 'player-button',
  template: `
    <button
      type="button"
      class="player-button">
      {{ label }}
    </button>
  `,
})
export class PlayerButtonComponent {
  @Input() label: string;
}

With this in mind, we have inputs and outputs as effective ways to communicate across components.

Much debate has existed on whether to inject things such as Services and Stores down into the component tree, to stop having to emit an event all the way back up.

I’m personally not a fan of injecting a Service or Store down into a component that shouldn’t really be in charge of direct data communication and updates to state.

Take a look at the code for what we have so far:

So, this is where the <ng-content> pattern comes in to help clean up @Input and @Output code, simplify how we compose and arrange components inside templates, but also make it extremely easy for child components to communicate with a service layer without having to pass data all the way up a component tree.

First, instead of this:

@Component({
  selector: 'app',
  template: `
    <player
      [audio]="audio"
      (toggle)="toggle()"
      (stop)="stop()">
    </player>
  `,
})
export class AppComponent implements OnInit {
  audio: HTMLAudioElement;

  ngOnInit() {...}

  toggle() {...}

  stop() {...}
}

We’re going to do this:

@Component({
  selector: 'app',
  template: `
    <player>
      <player-info>
        {{ audio.currentTime | duration }} / {{ audio.duration | duration }}
      </player-info>
      <player-actions>
        <player-button (click)="toggle()">
          {{ audio.paused ? 'Play': 'Pause' }}
        </player-button>
        <player-button (click)="stop()">
          Stop
        </player-button>
      </player-actions>
    </player>
  `,
})
export class AppComponent implements OnInit {
  audio: HTMLAudioElement;

  ngOnInit() {...}

  toggle() {...}

  stop() {...}
}

To achieve this, we now need to add <ng-content> inside any components we want to project the content into.

Check the before and after for the <player> component template:

//////////////////// 😨 BEFORE
@Component({
  selector: 'player',
  template: `
    <div
      class="player">
      <player-info
        [audio]="audio">
      </player-info>
      <player-actions
        [audio]="audio"
        (toggle)="toggle.emit()"
        (stop)="stop.emit()">
      </player-actions>
    </div>
  `,
})
export class PlayerComponent {
  @Input() audio: HTMLAudioElement;
  @Output() toggle: EventEmitter<void> = new EventEmitter<void>();
  @Output() stop: EventEmitter<void> = new EventEmitter<void>();
}

//////////////////// ✅ AFTER
@Component({
  selector: 'player',
  template: `
    <div
      class="player">
      <ng-content></ng-content>
    </div>
  `,
})
export class PlayerComponent {}

The <player> component projects <player-info> and <player-actions>, and <player-actions> projects 2x <player-button>.

We then apply <ng-content> to each other component:

//////////////////// 😨 BEFORE
@Component({
  selector: 'player-actions',
  template: `
    <div
      class="player-actions">
        <player-button
          [label]="audio.paused ? 'Play' : 'Pause'"
          (click)="toggle.emit()">
        </player-button>
        <player-button
          [label]="'Stop'"
          (click)="stop.emit()">
        </player-button>
    </div>
  `,
})
export class PlayerActionsComponent {
  @Input() audio: HTMLAudioElement;
  @Output() toggle: EventEmitter<void> = new EventEmitter<void>();
  @Output() stop: EventEmitter<void> = new EventEmitter<void>();
}

//////////////////// ✅ AFTER
@Component({
  selector: 'player-actions',
  template: `
    <div
      class="player-actions">
      <ng-content></ng-content>
    </div>
  `,
})
export class PlayerActionsComponent {}

And finally the <player-button>:

//////////////////// 😨 BEFORE
@Component({
  selector: 'player-button',
  template: `
    <button
      type="button"
      class="player-button">
      {{ label }}
    </button>
  `,
})
export class PlayerButtonComponent {
  @Input() label: string;
}

//////////////////// ✅ AFTER
@Component({
  selector: 'player-button',
  template: `
    <button
      type="button"
      class="player-button">
      <ng-content></ng-content>
    </button>
  `,
})
export class PlayerButtonComponent {}

We’ve removed all of the @Input and @Output bindings, making for a very nice and clean component that is simply presentational and manages its own styles.

Speaking of styling, this also makes no difference to the CSS of a component, we can “move” the components around inside of one another to “compose” them how we see fit - without adjusting any CSS. Super flexible.

Check out the fully working finished example of using the <ng-content> architecture here, I think you’ll agree the components are far cleaner, easier to test as there is no need to test the simple @Input and @Output anymore:

We’ve enhanced our component architecture pattern by using <ng-content> to bind directly to components, instead of passing data all the way down and all the way back up. In turn, it makes them more flexible - for example if we swapped the buttons around it can be done easily, and also if we are using the same components in multiple places we can compose them differently.

We’ve reduced the boilerplate needed to pass data, and cleaned up how our components talk to each other.

🚀 Learn more techniques, best practices and real-world expert knowledge, I’d highly recommend checking out my Angular courses - they will guide you through your journey to mastering Angular to the fullest!

Not only to each other, but most importantly where our smart component fetches the data and talks back to the service (well, in our example the service would be the Audio API but the pattern remains the same).

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover