Search code examples
angulartypescriptcontextmenu

Customizing context menu in angular


I am using this tutorial to have a custom context menu. I am showing items of a list, and I want that when the user clicks on these items, their index be shown. Currently, right clicking on any item shows the last index, e.g., if my list has 3 items, clicking on any item shows 2! How can I show the correct index of each item?

Here is my code:

<div *ngFor="let item of items; let i = index" (contextmenu)="displayContextMenu($event); false">
    <span> {{item}}</span>
    <app-context-menu
        *ngIf="rightClickMenuItems.length > 0 && isDisplayContextMenu"
        [ngStyle]="getRightClickMenuStyle()"        
        [contextMenuItems]="rightClickMenuItems"
        (onContextMenuItemClick)="handleMenuItemClick($event, i)"
    ></app-context-menu>
</div>

ts file:

items = ["item0","item1","item2"];

isDisplayContextMenu!: boolean;
rightClickMenuItems: Array<ContextMenu> = [];

rightClickMenuPositionX!: number;
rightClickMenuPositionY!: number;

displayContextMenu(event: any) {
  this.isDisplayContextMenu = true;
  this.rightClickMenuItems = [
    {
      menuText: 'Print index',
      menuEvent: 'Handle print index',
    },
  ];

  this.rightClickMenuPositionX = event.clientX;
  this.rightClickMenuPositionY = event.clientY;
}

handleMenuItemClick(event: any, index: number) {
  switch (event.data) {
    case this.rightClickMenuItems[0].menuEvent:
      this.printIndex(index);
      break;
  }
}

printIndex(index: number){
  alert(index);
}

@HostListener('document:click')
  documentClick(): void {
  this.isDisplayContextMenu = false;
}

getRightClickMenuStyle() {
  return {
    position: 'fixed',
    left: `${this.rightClickMenuPositionX}px`,
    top: `${this.rightClickMenuPositionY}px`
  }
}

Context menu component:

.html:

<ng-container>
    <div>
        <div *ngFor="let menuItem of contextMenuItems; index as i"
            (click)="onContextMenuClick($event, menuItem.menuEvent)">
            {{ menuItem.menuText }}
        </div>
    </div>
</ng-container>

.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ContextMenu } from '../../_models/contextMenu.model';
import { NgIf, NgFor } from '@angular/common';

@Component({
    selector: 'app-context-menu',
    templateUrl: './context-menu.component.html',
    styleUrls: ['./context-menu.component.css'],
    standalone: true,
    imports: [NgIf, NgFor]
})

export class ContextMenuComponent {
    @Input() contextMenuItems!: Array<ContextMenu>;
    @Output() onContextMenuItemClick: EventEmitter<any> = new EventEmitter<any>();
    onContextMenuClick(event: any, data: any): any {
        this.onContextMenuItemClick.emit({
            event,
            data,
        });
    }
}

model:

export interface ContextMenu {
    menuText: any;
    menuEvent: any;
}

Solution

    1. We can move the context-menu outside the *ngFor because you only need a single context menu for a table.
    2. To identify the selected index I am using data-id data attributes to identify which item was selected.

    code

    <div *ngFor="let item of items; let i = index" (contextmenu)="displayContextMenu($event); false"
        [attr.data-id]="item">
        <span> {{item}}</span>
    
    1. We can access the attribute to identify which item was selected, we can just store the selected index, here you have two options store it in the main component, or you can just store it in the context menu.

      const id = event.target.parentElement.dataset.id ? event.target.parentElement.dataset.id : event.target.dataset.id; const foundIndex = this.items.findIndex((x) => x === id); if (foundIndex > -1) { this.index = foundIndex;

    Finally, we can just print the index using alert!

    this.printIndex(this.index);
    

    main.ts

    import { CommonModule } from '@angular/common';
    import { Component, HostListener } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import {
      ContextMenu,
      ContextMenuComponent,
    } from './context-menu/context-menu.component';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CommonModule, ContextMenuComponent],
      template: `
        <div *ngFor="let item of items; let i = index" (contextmenu)="displayContextMenu($event); false"
        [attr.data-id]="item">
        <span> {{item}}</span>
    </div>
        <app-context-menu
            *ngIf="rightClickMenuItems.length > 0 && isDisplayContextMenu"
            [ngStyle]="getRightClickMenuStyle()"
            [contextMenuItems]="rightClickMenuItems"
            (onContextMenuItemClick)="handleMenuItemClick($event)"
        ></app-context-menu>
      `,
    })
    export class App {
      items = ['item0', 'item1', 'item2'];
      index = -1;
    
      isDisplayContextMenu!: boolean;
      rightClickMenuItems: Array<ContextMenu> = [];
    
      rightClickMenuPositionX!: number;
      rightClickMenuPositionY!: number;
    
      displayContextMenu(event: any) {
        const id = event.target.parentElement.dataset.id
          ? event.target.parentElement.dataset.id
          : event.target.dataset.id;
        const foundIndex = this.items.findIndex((x) => x === id);
        if (foundIndex > -1) {
          this.index = foundIndex;
          this.isDisplayContextMenu = true;
          this.rightClickMenuItems = [
            {
              menuText: 'Print index',
              menuEvent: 'Handle print index',
            },
          ];
    
          this.rightClickMenuPositionX = event.clientX;
          this.rightClickMenuPositionY = event.clientY;
        }
      }
    
      handleMenuItemClick(event: any) {
        switch (event.data) {
          case this.rightClickMenuItems[0].menuEvent:
            this.printIndex(this.index);
            break;
        }
      }
    
      printIndex(index: number) {
        alert(index);
      }
    
      @HostListener('document:click')
      documentClick(): void {
        this.isDisplayContextMenu = false;
      }
    
      getRightClickMenuStyle() {
        return {
          position: 'fixed',
          left: `${this.rightClickMenuPositionX}px`,
          top: `${this.rightClickMenuPositionY}px`,
        };
      }
    }
    
    bootstrapApplication(App);
    

    context menu

    import { Component, Input, Output, EventEmitter } from '@angular/core';
    import { NgIf, NgFor } from '@angular/common';
    
    export interface ContextMenu {
      menuText: any;
      menuEvent: any;
    }
    
    @Component({
      selector: 'app-context-menu',
      template: `
      <ng-container>
      <div>
        <div
          *ngFor="let menuItem of contextMenuItems; index as i"
          (click)="onContextMenuClick($event, menuItem.menuEvent)"
        >
          {{ menuItem.menuText }}
        </div>
      </div>
    </ng-container>
    
      `,
      standalone: true,
      imports: [NgIf, NgFor],
    })
    export class ContextMenuComponent {
      private id!: string;
      @Input() contextMenuItems!: Array<ContextMenu>;
      @Output() onContextMenuItemClick: EventEmitter<any> = new EventEmitter<any>();
      onContextMenuClick(event: any, data: any): any {
        this.onContextMenuItemClick.emit({
          event,
          data,
        });
      }
    }
    

    stackblitz