Custom Overlays with Angular Material CDK

Posted on March 5, 2022 with tags:
In this post:

As modern programmers and web developers, component libraries are our bread and butter. However, every developer runs a scenario where the configuration and customizability of our chosen component library doesn't afford the flexibility we need.

Such was the case when I needed to reposition a MatMenu from Angular Material. I couldn't accomplish what the designer had declared, and thus I was left to DIY my own overlay using the CDK.

Building the Overlay Component

First, you'll need to spin up and style a component for your overlay.

  1. In terminal, cd to the module where my component should live and run ng generate component. The Angular CLI creates the necessary files and adds your new component to it's parent module's declarations. Read more about ng generate in the angular cli docs.
  2. For now, we won't pipe in any data from outside the component, so you can leave the component.ts as is.
  3. I wrapped my template in a <div class="mat-menu-content"> to utilize angular material styles and visually match my custom overlay with existing mat-menu elements in my application.

Create Injection Token

In Angular, the primary way we pass data into and out of components is by referencing Input() and Output() attributes in the html. However, since we'll be instantiating this overlay programatically rather through the DOM, we need to "inject" in data using an InjectionToken.

I defined my injection token in my overlay component:

export const CUSTOM_OVERLAY_DATA = new InjectionToken<{
  mesage: String;
}>('CUSTOM_OVERLAY_DATA');

Create Overlay Service

Next, we need to create a service to act as the overlay "remote control". The service will handle creating the component instance, injecting data, and attaching the overlay to the DOM. In my case, I handled all of this functionality in a single openCustomOverlay() function. Here's a breakdown:

  1. Create a PostitionStrategy with the Overlay.position and define our position and overlay class settings. Read more about position strategies on the Material Docs.
  2. Use Overlay.create to instantiate a new overlay, passing in the positionStrategy and backdrop options. A backdrop is necessary to close the overlay when anywhere else on the page is clicked.
  3. Create a new ComponentPortal, passing in the overlay component and an Injector to provide the component with data.
  4. To add the overlay to the DOM, attach the customOverlayPortal to the overlayRef.
  5. Listen for overlayRef.backdropClick to close the overlay.
constructor(
    private _overlay: Overlay,
    private _injector: Injector,
  ) {}

openCustomOverlay(attachTo: MatButton): void {
    const positionStrategy = this._overlay
      .position()
      .flexibleConnectedTo(attachTo._getHostElement())
      .withPositions([
        {
          offsetX: 64,
          originX: 'start',
          originY: 'bottom',
          overlayX: 'start',
          overlayY: 'bottom',
          weight: 1,
          panelClass: ['mat-menu-panel'],
        },
      ]);
    const overlayRef = this._overlay.create({
      disposeOnNavigation: true,
      positionStrategy,
      hasBackdrop: true,
      backdropClass: 'cdk-overlay-transparent-backdrop',
    });

    const customOverlayPortal = new ComponentPortal(
      CustomOverlayComponent,
      null,
      Injector.create({
        parent: this._injector,
        providers: [
          {
            provide: CUSTOM_OVERLAY_DATA,
            useValue: {
              message: 'Hello world!',
            },
          },
        ],
      }),
    );

    overlayRef.attach(customOverlayPortal);

    overlayRef.backdropClick().subscribe(() => {
      overlayRef.dispose();
    });
  }

Add the Overlay Panel Trigger

In an existing parent component in our app we need to provide the overlay service a trigger element to open the panel. This can be any clickable element. We'll declare a variable #overlayButton and pass it to our openOverlay function. This can be the same element that calls the open function, or a different one if you wish to attach the overlay somewhere else.

parent.component.html

<button mat-button #overlayButton (click)="openCustomOverlay(overlayButton)">

parent.component.ts

constructor(private  _customOverlayService:  CustomOverlayService) {}

openCustomOverlay(attachTo:  MatButton):  void  {
    this._customOverlayService.openCustomOverlay(attachTo);
}

Wrapping Up

At this point you should have a working custom overlay! For further customization you could alter the data structure of the InjectionToken, or add more custome styles using the panelClass property on the positionStrategy.

Comments

Sign in to leave a comment.