Chapter 23 of 34

Events and a context menu of your own

Override only the events you need – and build a context menu that looks exactly like your app.

After this chapter you can

  • extend EventAdapter
  • tell single from double clicks
  • map a displayed row back to your object

EventAdapter: override what you need

The fifth argument of the TableScope is an EventListenerIf. EventAdapter implements every method empty – extend it and override only your events:

src/order-events.ts
import { EventAdapter, type GeMouseEvent } from "@guiexpert/table";
export class OrderEvents extends EventAdapter {
constructor(
private readonly onRowDoubleClick: (rowIndex: number) => void,
private readonly onMenu: (evt: GeMouseEvent) => void) {
super();
}
override onMouseClicked(evt: GeMouseEvent): void { // rowIndex, columnIndex, areaIdent, clickCount
if (evt.areaIdent === "body" && evt.clickCount === 2) this.onRowDoubleClick(evt.rowIndex);
}
override onContextmenu(evt: GeMouseEvent): void {
if (evt.areaIdent === "body" && evt.rowIndex > -1) this.onMenu(evt);
}
}

A GeMouseEvent tells you where it happened:

FieldMeaning
areaIdent, sideIdentheader / body / footer – west / center / east
rowIndex, columnIndexthe cell (−1 outside of cells)
clickCount1 = click, 2 = double-click
originalEventthe browser's MouseEvent (position, modifier keys)

The other events: onModelChanged (edits, chapter 31 (Inline editing)), onSelectionChanged (chapter 22 (Selection & copy)), onCheckboxChanged (chapter 30 (Tree table)), onScroll, onMouseMoved, onMouseDragging, onFocusChanged.

From the displayed row to your object

rowIndex is the row as displayed – after filter and sorting. Never index your original array with it. Ask the body model: AreaModelIf.getRowByIndex().

There is no built-in context menu – so yours looks and works exactly like the rest of your app. About 30 lines, no library:

src/context-menu.ts
export interface MenuItem {
label: string;
action: () => void;
}
export function showContextMenu(event: MouseEvent, items: MenuItem[]): void {
event.preventDefault(); // no browser menu
closeContextMenu();
const menu = document.body.appendChild(document.createElement("div"));
menu.className = "context-menu";
menu.style.left = `${event.clientX}px`;
menu.style.top = `${event.clientY}px`;
for (const item of items) {
const button = menu.appendChild(document.createElement("button"));
button.textContent = item.label;
button.addEventListener("click", () => { closeContextMenu(); item.action(); });
}
}
export function closeContextMenu(): void {
document.querySelector(".context-menu")?.remove();
}
document.addEventListener("mousedown", e => { // close on a click outside …
if (!(e.target as HTMLElement).closest(".context-menu")) closeContextMenu();
});
document.addEventListener("keydown", e => e.key === "Escape" && closeContextMenu()); // … and on Escape

Wire events, menu and API together:

src/main.ts
const orderAt = (rowIndex: number) => tableModel.getBodyModel().getRowByIndex(rowIndex) as Order; // displayed row → object
const showOrder = (o: Order) => info.textContent = `#${o.id} · ${o.customer} · ${o.quantity} × ${o.product}`;
const events = new OrderEvents(row => showOrder(orderAt(row)), evt => {
const order = orderAt(evt.rowIndex);
showContextMenu(evt.originalEvent!, [
{ label: `Show order #${order.id}`, action: () => showOrder(order) },
{ label: "Copy selection", action: () => tableApi.copyToClipboard() },
{ label: `Only ${order.customer}`, action: () => { query = order.customer; tableApi.externalFilterChanged(); } },
{ label: "Show all", action: () => { query = ""; tableApi.externalFilterChanged(); } },
{ label: "Export view as Excel", action: () => tableApi.downloadExcel("orders.xlsx", "GuiExpert") }
]);
});
const tableScope = new TableScope(host, tableModel, new SimpleDomService(), options, events);
tableScope.firstInit();
const tableApi = tableScope.getApi();
Live Double-click and context menu
Screenshot: Double-click and context menu Screenshot: Double-click and context menu
Double-click a row: the toolbar shows the order. Right-click: own menu – filter to one customer, copy, export.

In the framework wrappers

The same events arrive as props or outputs: mouseClicked, contextmenu, selectionChanged … (chapter 10 (Svelte, Solid, Preact)).

Next

The menu's last entry exports to Excel – how that works: chapter 24 (Excel export).

API used in this chapter

Showcases