Context Menu Example
This example demonstrates how to implement a context menu that adapts its options based on the context—showing node-specific actions when a node is right-clicked, and diagram-wide actions otherwise.
import '@angular/compiler';import { Component } from '@angular/core';import { NgDiagramModelService, provideNgDiagram } from 'ng-diagram';import { DiagramComponent } from './diagram.component';import { ContextMenuService } from './menu/menu.service';
@Component({ imports: [DiagramComponent], template: `<context-menu-example></context-menu-example> `, styles: [ ` :host { display: flex; position: relative; } `, ], providers: [ContextMenuService, NgDiagramModelService, provideNgDiagram()],})export class DiagramWrapperComponent {}import '@angular/compiler';import { Component, inject } from '@angular/core';import { initializeModel, NgDiagramBackgroundComponent, NgDiagramComponent, NgDiagramNodeTemplateMap, NgDiagramViewportService, type NgDiagramConfig,} from 'ng-diagram';import { MenuComponent } from './menu/menu.component';import { ContextMenuService } from './menu/menu.service';import { NodeComponent } from './node/node.component';
@Component({ selector: 'context-menu-example', imports: [NgDiagramComponent, NgDiagramBackgroundComponent, MenuComponent], template: ` <div (contextmenu)="onDiagramRightClick($event)"> <div class="not-content diagram"> <ng-diagram [model]="model" [config]="config" [nodeTemplateMap]="nodeTemplateMap" > <ng-diagram-background /> </ng-diagram> </div> <menu></menu> </div> `, styleUrl: './diagram.component.scss', providers: [ContextMenuService, NgDiagramViewportService],})export class DiagramComponent { private contextMenuService = inject(ContextMenuService); private readonly viewportService = inject(NgDiagramViewportService);
nodeTemplateMap = new NgDiagramNodeTemplateMap([ ['customNodeType', NodeComponent], ]);
config = { zoom: { max: 3, zoomToFit: { onInit: true, padding: [200, 180], }, }, } satisfies NgDiagramConfig;
model = initializeModel({ nodes: [ { id: '1', position: { x: 0, y: 0 }, type: 'customNodeType', data: { name: 'Custom Node', }, }, ], });
onDiagramRightClick(event: MouseEvent) { event.preventDefault(); event.stopPropagation();
const cursorPosition = this.viewportService.clientToFlowViewportPosition({ x: event.clientX, y: event.clientY, }); this.contextMenuService.showDiagramMenu(cursorPosition); }}import { Component, HostListener, inject } from '@angular/core';import { NgDiagramClipboardService, NgDiagramModelService, NgDiagramSelectionService, NgDiagramViewportService,} from 'ng-diagram';import { ContextMenuService } from './menu.service';
@Component({ selector: 'menu', templateUrl: './menu.component.html', styleUrls: ['./menu.component.scss'],})export class MenuComponent { private contextMenuService = inject(ContextMenuService); private readonly modelService = inject(NgDiagramModelService); private readonly clipboardService = inject(NgDiagramClipboardService); private readonly selectionService = inject(NgDiagramSelectionService); private readonly viewportService = inject(NgDiagramViewportService);
showMenu = this.contextMenuService.visibility; menuPosition = this.contextMenuService.menuPosition; nodeContext = this.contextMenuService.nodeContext;
@HostListener('document:click') closeMenu() { this.contextMenuService.hideMenu(); }
onCopy() { this.clipboardService.copy(); }
onPaste(event: MouseEvent) { const position = this.viewportService.clientToFlowPosition({ x: event.clientX, y: event.clientY, }); this.clipboardService.paste(position); }
onDelete() { this.selectionService.deleteSelection(); }
onSelectAll() { const allNodesIds = this.modelService .getModel() .getNodes() .map((node) => node.id); this.selectionService.select(allNodesIds); }}import { Injectable, signal } from '@angular/core';import type { Point } from 'ng-diagram';
@Injectable()export class ContextMenuService { readonly visibility = signal(false); readonly menuPosition = signal({ x: 0, y: 0 }); readonly nodeContext = signal(false);
showMenu({ x, y }: Point): void { this.nodeContext.set(true); this.menuPosition.set({ x, y }); this.visibility.set(true); }
showDiagramMenu({ x, y }: Point): void { this.nodeContext.set(false); this.menuPosition.set({ x, y }); this.visibility.set(true); }
hideMenu(): void { this.visibility.set(false); }}import { Component, inject, input, model } from '@angular/core';import { NgDiagramNodeSelectedDirective, NgDiagramPortComponent, NgDiagramSelectionService, NgDiagramViewportService, type NgDiagramNodeTemplate, type Node,} from 'ng-diagram';import { ContextMenuService } from '../menu/menu.service';
@Component({ imports: [NgDiagramPortComponent], templateUrl: './node.component.html', styleUrls: ['./node.component.scss'], hostDirectives: [ { directive: NgDiagramNodeSelectedDirective, inputs: ['node'] }, ],})export class NodeComponent implements NgDiagramNodeTemplate<{ name: string }> { private readonly contextMenuService = inject(ContextMenuService); private readonly viewportService = inject(NgDiagramViewportService); private readonly selectionService = inject(NgDiagramSelectionService);
text = model<string>(''); node = input.required<Node<{ name: string }>>();
onRightClick(event: MouseEvent) { event.preventDefault(); event.stopPropagation();
if (this.node()) { // Additionally selects the node on right click const selectedNodes = this.selectionService.selection().nodes; if (!selectedNodes.some((n) => n.id === this.node().id)) { this.selectionService.select([this.node().id]); }
const cursorPosition = this.viewportService.clientToFlowViewportPosition({ x: event.clientX, y: event.clientY, }); this.contextMenuService.showMenu(cursorPosition); } }}<div #menu class="menu" [style.top.px]="menuPosition().y" [style.left.px]="menuPosition().x" [class.show]="showMenu()"> <ul> @if (nodeContext()) { <li (click)="onCopy()">Copy</li> <li (click)="onDelete()">Delete</li> } @else { <li (click)="onSelectAll()">Select All</li> <li (click)="onPaste($event)">Paste</li> } </ul></div><div class="node" (contextmenu)="onRightClick($event)"> <div class="node-header"> <span>{{ node().data.name }}</span> </div> <div class="node-body"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. </div></div>:host { width: 100%; height: 100%;}
.diagram { display: flex; height: var(--ng-diagram-height); border: var(--ng-diagram-border);}.menu { position: absolute; display: none; background-color: var(--ngd-node-bg-primary-default); border: 1px solid var(--ngd-node-stroke-primary-default); z-index: 1000; min-width: 120px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);}.menu.show { display: block;}.menu ul { list-style: none; margin: 0; padding: 0;}.menu li { padding: 8px 16px; cursor: pointer;}.menu li:hover { background-color: var(--ngd-node-stroke-primary-hover);}.menu li.disabled { color: #aaa; pointer-events: none;}.content { padding: 40px; border: 1px solid var(--ngd-node-stroke-secondary-default); margin: 20px; user-select: text;}:host { display: flex; width: 100%; height: 100%; background-color: white; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.node { display: flex; flex-direction: column; padding: 4px 8px; border: 1px solid var(--ngd-node-stroke-primary-default); border-radius: 4px; background-color: var(--ngd-node-bg-primary-default);
&-body { font-size: 12px; } }}Additional Explanation
Section titled “Additional Explanation”- Context Awareness: Menu options change depending on whether a node or the diagram background is right-clicked.
- Positioning: The menu appears at the exact cursor location using viewport coordinates.
- Integration: Uses Angular signals and services for reactive state management.
Key Concepts
Section titled “Key Concepts”-
Node Right-Click Handling:
Nodes handle right-click events to show the context menu and select the node. -
Diagram Right-Click Handling:
Right-clicking the diagram background shows the diagram-wide menu. -
Menu Positioning:
The menu position is calculated using viewport coordinates. -
Context Menu Service:
The service manages the menu’s visibility, position, and context (node or diagram). -
Menu Component:
The menu component displays options based on the context and stores information about menu positioning.
Actions
Section titled “Actions”- Copy: Copies the selected node(s) to the clipboard.
- Paste: Pastes copied nodes at the cursor position.
- Delete: Removes the selected node(s).
- Select All: Selects all nodes in the diagram.