Search code examples
angularangular-materialangular-components

Angular: disable MatButton if MatMenu hasn't MatMenuItem


I have a complex MatMenu with more MatMenuItem. Each MatMenuItem's visibility is based on a condition. The trigger button is disabled if all possible condition are false, eg.:

<button mat-button [matMenuTriggerFor]="menu" [disable]="!condition1 && !condition2 && !condition3 && !conditionN">Menu</button>
<mat-menu #menu="matMenu">
  <button mat-menu-item *ngIf="condition1">Item 1</button>
  <button mat-menu-item *ngIf="condition2">Item 2</button>
  <button mat-menu-item *ngIf="condition3">Item 3</button>
  ...
  <button mat-menu-item *ngIf="conditionN">Item N</button>
</mat-menu>

There are a simple way for check if a MatMenu has at least one MatMenuItem and disable the trigger button if no one MatMenuItem are available?


Solution

  • Issue with items

    As mentioned in previous answer, one potential solution could be to use items of exported matMenu.

    But we will have 2 issues :

    • items property is deprecated, and could be removed soon.
    /**
     * List of the items inside of a menu.
     * @deprecated
     * @breaking-change 8.0.0
     */
    items: QueryList<MatMenuItem>;
    
    • set disabled property with matMenu.items.length will throw a NG0100 error if we are in default change detection strategy (not in OnPush), because change detection process is not finished.

    Potential solution

    To solve this, we can create a reusable directive, which will query MatMenuItem of a MatMenu, and then fire an event with the current status of menu : true if at least one option, false if not.

    menu-toggled.directive.ts:

    @Directive({
      selector: 'mat-menu',
    })
    export class MenuToggledDirective implements AfterContentInit, OnDestroy {
      _destroyed$ = new Subject<void>();
    
      @ContentChildren(MatMenuItem, { descendants: true })
      _items!: QueryList<MatMenuItem>;
    
      @Output()
      menuToggled = new EventEmitter<boolean>();
    
      ngAfterContentInit(): void {
        this._items.changes
          .pipe(
            takeUntil(this._destroyed$),
            startWith(0),
            map((_) => this._items.length > 0)
          )
          .subscribe(this.menuToggled);
      }
    
      ngOnDestroy(): void {
        this._destroyed$.next();
        this._destroyed$.complete();
      }
    }
    

    Usage in app.component.html :

    <button #button mat-stroked-button [mat-menu-trigger-for]="menu">
      Menu
    </button>
    <mat-menu #menu="matMenu" (menuToggled)="button.disabled = !$event">
      <button mat-menu-item *ngIf="flag">Item 1</button>
      <button mat-menu-item *ngIf="false">Item 2</button>
    </mat-menu>