angularangular-cdk

Why is my Angular EventEmitter not working with TemplatePortal from Angular CDK?


I'm working on an Angular project where I'm using Angular CDK's Overlay and TemplatePortal to display a dynamic menu. The menu should emit events when an option is selected, but the EventEmitter doesn't seem to work as expected.

Here's what I'm doing:

Here's the relevant code:



   import {
     Component,
     ViewChild,
     TemplateRef,
     ViewContainerRef,
     Output,
     EventEmitter,
   } from '@angular/core';
   import { Overlay, OverlayRef } from '@angular/cdk/overlay';
   import { TemplatePortal } from '@angular/cdk/portal';
   
   @Component({
     selector: 'app-menu-example',
     templateUrl: './menu-example.component.html',
     styleUrls: ['./menu-example.component.css'],
   })
   export class MenuExampleComponent {
     @ViewChild('menuTemplate', { read: TemplateRef }) menuTemplate!: TemplateRef<any>;
   
     @Output() triggerOption = new EventEmitter<string>();
   
     private overlayRef!: OverlayRef;
   
     constructor(private overlay: Overlay, private viewContainerRef: ViewContainerRef) {}
   
     openMenu(event: MouseEvent, control: any) {
      
       if (this.overlayRef) {
         this.overlayRef.dispose();
       }
   
      
       const positionStrategy = this.overlay.position()
         .flexibleConnectedTo(event.target as HTMLElement)
         .withPositions([{
           originX: 'start',
           originY: 'bottom',
           overlayX: 'start',
           overlayY: 'top',
           offsetX: 0,
           offsetY: 8,
         }]);
   
       
       this.overlayRef = this.overlay.create({
         positionStrategy,
         hasBackdrop: true,
         backdropClass: 'cdk-overlay-transparent-backdrop',
         panelClass: 'custom-panel-class',
       });
   
       
       const portal = new TemplatePortal(
         this.menuTemplate,
         this.viewContainerRef,
         { $implicit: control, component: this } 
       );
   
       this.overlayRef.attach(portal);
   
       
       this.overlayRef.backdropClick().subscribe(() => this.closeMenu());
     }
   
     closeMenu() {
       if (this.overlayRef) {
         this.overlayRef.dispose();
       }
     }
   
     openOptionsView(control: any) {
       console.log('Settings clicked for:', control);
       this.triggerOption.emit('Settings');
     }
   
     removeControl(control: any) {
       console.log('Remove clicked for:', control);
       this.triggerOption.emit('Remove');
     }
   }


    <ng-template #menuTemplate let-control let-component="component">
      <div class="control-menu-bar">
        <button
          class="control-menu-bar-item"
          matTooltip="Settings"
          (click)="component.openOptionsView(control)"
        >
          <mat-icon>settings_suggest</mat-icon>
        </button>
        <button
          class="control-menu-bar-item"
          matTooltip="Remove"
          (click)="component.removeControl(control)"
        >
          <mat-icon>delete_forever</mat-icon>
        </button>
      </div>
    </ng-template>
    
    <button (click)="openMenu($event, { name: 'Control 1' })">Open Menu</button>


    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-parent',
      templateUrl: './parent.component.html',
      styleUrls: ['./parent.component.css'],
    })
    export class ParentComponent {
      handleMenuOption(option: string) {
        console.log('Menu option selected:', option);
       
      }
    }


    <app-menu-example (triggerOption)="handleMenuOption($event)"></app-menu-example>

Question:

  1. How can I ensure that the triggerOption EventEmitter emits events correctly when using TemplatePortal?
  2. Is there a specific way to manage context or binding issues when using TemplatePortal in Angular CDK?

Any insights or suggestions would be greatly appreciated! Thank you.


Solution

  • The implicit prevented the component template reference variable for being initialized property, instead create two variables.

    const portal = new TemplatePortal(
      this.menuTemplate,
      this.viewContainerRef,
      { control, component: this }
    );
    

    Stackblitz Demo


    If you want to implement using $implicit you can pass them as properties of a single object.

    const portal = new TemplatePortal(
      this.menuTemplate,
      this.viewContainerRef,
      { $implicit: { control, component: this } }
    );
    

    HTML:

    <ng-template #menuTemplate let-item>
        <div class="control-menu-bar">
          <button
            class="control-menu-bar-item"
            matTooltip="Settings"
            (click)="item.component.openOptionsView(item.control)"
          >
            <mat-icon>settings_suggest</mat-icon>
          </button>
          <button
            class="control-menu-bar-item"
            matTooltip="Remove"
            (click)="item.component.removeControl(item.control)"
          >
            <mat-icon>delete_forever</mat-icon> 
          </button>
        </div>
      </ng-template>
      
      <button (click)="openMenu($event, { name: 'Control 1' })">Open Menu</button>
    

    Stackblitz Demo