Juri Strumpflohner

RSS

Access Angular Material's MatSelect Options Panel Container

Author profile pic
Juri Strumpflohner
Published

In this article we’re going to explore how you can programmatically access the Angular Material Select panel, without doing strange global DOM queries :smiley:. Let’s dive in!


TL;DR

In your directive, inject the reference to MatSelect, subscribe to the openedChange Observable and if it is open, access the panel property on the MatSelect.


If you’re in search for some high quality component library, Angular Material might be a good point where to start. As a side-note, if you’re not searching for a Material Design, there are lots of other interesting options:

But to come back to our topic. Angular Material puts some major effort around creating accessible components. Sometimes you might want to add some custom logic, like custom attributes. Take for instance the MatSelect (here are the corresponding docs)

The Angular template for this looks as follows:

<mat-select [(ngModel)]="selectedValue" name="food">
  <mat-option *ngFor="let food of foods" [value]="food.value">
    {{food.viewValue}}
  </mat-option>
</mat-select>

A client of mine had the requirement to access the rendered options and add custom attributes to those <options> to enhance support for screenreaders. So the first idea is to place some directive - say myDirective (plz use a proper name 😉) - onto the <mat-select> and then use some DOM selectors to get hold of the options.

The Material Options Panel is not a child of the MatSelect

It might look easy, right? In your directive myDirective you can get the ElementRef injected and simply access the <mat-options>. The ElementRef would be the one of the <mat-select> which would allow to select it’s child option items. Something like

@Directive({})
export class MyDirective implements OnInit {

   constructor(private elementRef: ElementRef) {}

   ngOnInit() {
      this.elementRef.nativeElement.querySelector(...)
   }
}

That won’t work! The <mat-options> - although it might seem from how you write the <mat-select> - are not child objects of the <mat-select> in the DOM.

When you open the select, Material renders them in a dedicated,z-index and absolute positioned panel at the document.body level. Why is that? It’s to make sure it stays over all other elements and to not expand or shift any other element within the body.

You’re doing it wrong

The next immediate step would be to change this.elementRef.nativeElement.querySelector(...) to document.body.querySelector(...), right? Do not! We only keep that as a very last resort. You want to keep your querySelector as focused as possible, for performance reasons but also not to run into other elements rendered on the page.

Reference the Options panel via the panel property

The biggest advantage of using open source libraries is that we can take a look at the source and see how Material creates the hosting overlay and whether it keeps and in particular exposes its reference to the outside. And indeed, if we have a quick look at the API docs, there’s a property panel which is an ElementRef to the container of the <options>.

On that panel property, we can perform our panel.nativeElement.querySelect(...) to have a nicely scoped DOM query that only runs on the container with our option list.

Accessing the Host component with Dependency Injection

We add our directive to the <mat-select> as follows

<mat-select myDirective>
  ...
</mat-select>

We only need a way to get access to the MatSelect instance from within our directive, s.t. we can grab the panel reference and perform our query. The by far easiest way (and surprisingly many devs don’t know about this) is to use Angular’s dependency injection. By requiring the instance in the constructor, Angular will take care to inject the host/parent component.

@Directive({
  selector: '[myDirective]'
})
export class MyDirective implements OnInit {

  /**
   *  MatSelect instance injected into the directive
   */
  constructor(private select:MatSelect) { }

}

Now the only thing left is to actually use the panel property. We need to subscribe to the openedChange Observable since the options are only rendered and visible on the page if the <mat-select> is active.

@Directive({
  selector: '[myDirective]'
})
export class MyDirective implements OnInit {

  constructor(private select:MatSelect) { }

  ngOnInit() {
    this.select.openedChange.subscribe(isOpen => {
      if(isOpen) {
        console.log('open', this.select.panel);
      }
    })
  }
}

Full example

Here’s a Stackblitz example to play around with