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.
First, you'll need to spin up and style a component for your overlay.
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.component.ts
as is.<div class="mat-menu-content">
to utilize angular material styles and visually match my custom overlay with existing mat-menu
elements in my application.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');
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:
PostitionStrategy
with the Overlay.position
and define our position and overlay class settings. Read more about position strategies on the Material Docs.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. ComponentPortal
, passing in the overlay component and an Injector to provide the component with data.customOverlayPortal
to the overlayRef
.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();
});
}
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);
}
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.