DEV Community

Cover image for Wrapping Angular Material button in custom Angular component (part 2)
Dzhavat Ushev for This is Angular

Posted on • Originally published at dzhavat.github.io

Wrapping Angular Material button in custom Angular component (part 2)

In the first part of this post we ended up with a simple but repetitive solution to wrapping Angular Material button in a custom component. In this post we’ll explore another way of solving the problem.

To remind you of what we ended up with in part 1, here’s the final solution again:

<!-- my-button.component.html -->
<ng-container *ngIf="type === 'primary'">
  <button mat-flat-button color="primary">
    <ng-container [ngTemplateOutlet]="buttonContent"></ng-container>
  </button>
</ng-container>

<ng-container *ngIf="type === 'secondary'">
  <button mat-stroked-button color="primary">
    <ng-container [ngTemplateOutlet]="buttonContent"></ng-container>
  </button>
</ng-container>

<ng-container *ngIf="type === 'text'">
  <button mat-button color="primary">
    <ng-container [ngTemplateOutlet]="buttonContent"></ng-container>
  </button>
</ng-container>

<ng-template #buttonContent>
  <ng-content></ng-content>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

The repetition here is quite obvious. Can we do something about it?

Solution 1

First, we can start by creating a component for each button type and move some of the code there. Imagine the following:

<!-- my-button.component.html -->
<primary-button *ngIf="type === 'primary'">
  <ng-container [ngTemplateOutlet]="buttonContent"></ng-container>
</primary-button>

<secondary-button *ngIf="type === 'secondary'">
  <ng-container [ngTemplateOutlet]="buttonContent"></ng-container>
</secondary-button>

<text-button *ngIf="type === 'text'">
  <ng-container [ngTemplateOutlet]="buttonContent"></ng-container>
</text-button>

<ng-template #buttonContent>
  <ng-content></ng-content>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

<primary-button>* component might look like this:

import { Component } from "@angular/core";

@Component({
  selector: "primary-button",
  template: `<button mat-flat-button color="primary">
    <ng-content></ng-content>
  </button>`,
})
export class PrimaryButtonComponent {}
Enter fullscreen mode Exit fullscreen mode

* Note: <secondary-button> and <text-button> components are identical the only difference being that secondary button uses mat-stroked-button and text button uses mat-button directives.

StackBlitz demo.

The code was improved a tiny bit. Can we do better?

Solution 2

What if instead of using ngIfs to decide which button to show, we move that logic in the component’s class and use another Angular API to instantiate the relevant component dynamically?

Meet ngComponentOutlet. As written in the documentation, this directive provides a declarative approach for dynamic component creation. Declarative, meaning we tell Angular which component to instantiate and where to place it. The rest, like rendering it on the page, updating it on changes and destroying it, is handled by the framework.

Let’s see it in practice.

// my-button.component.ts
import { Component, Input } from "@angular/core";

import { PrimaryButtonComponent } from "../primary-button/primary-button.component";
import { SecondaryButtonComponent } from "../secondary-button/secondary-button.component";
import { TextButtonComponent } from "../text-button/text-button.component";

@Component({
  selector: "my-button",
  templateUrl: "./my-button.component.html",
})
export class MyButtonComponent {
  @Input() type: "primary" | "secondary" | "text" = "text";

  get buttonComponentType() {
    switch (this.type) {
      case "primary":
        return PrimaryButtonComponent;
      case "secondary":
        return SecondaryButtonComponent;
      case "text":
      default:
        return TextButtonComponent;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
<!-- my-button.component.html -->
<ng-container
  *ngComponentOutlet="buttonComponentType; content: [[buttonContent]]"
></ng-container>

<div #buttonContent>
  <ng-content></ng-content>
</div>
Enter fullscreen mode Exit fullscreen mode

The template is much cleaner now. There’s no repetitive logic. We send two properties to ngComponentOutlet - buttonComponentType, which holds the component that we want to instantiate, and content which takes a list of nodes to project into <ng-content> element(s) inside the component. buttonContent is a local variable that references the div element.

* Note: The type of content is any[][] hence the double square brackets around buttonContent. This also means that we can project more than one node elements into the component. The node can be any element that implements the Node interface like a text node, div, span, etc.

Demo time 🎉 (StackBlitz)

Demo to solution 2: All buttons are still displayed correctly

Well, visually the buttons still look the same but the code behind them is more organized. MyButton component is more flexible. We can easily add another button type by creating a new component and adding it to the switch statement.

In this post we looked at ngComponentOutlet and how it helps us dynamically create components to support a few basic requirements (outlined in part 1).

But things don’t have to stop here! MyButton component will rarely stay as it is right now. New use cases emerge all the time and some adjustments will be necessary. In part 3 we’ll introduce more requirements like icon support and disabled state.

Thanks to Lars Gyrup Brink Nielsen for reviewing this post.


Photo by Chris Lawton on Unsplash

Discussion (3)

Collapse
beazer profile image
Dave

Learned some good stuff in these two posts - thanks!

Collapse
dzhavat profile image
Dzhavat Ushev Author

Thanks Dave. I'm glad to hear that 😊
Other parts will be coming as well :)

Collapse
beazer profile image
Dave

Cool, I look forward to it. ✌🏻