Skip to content

Latest commit

 

History

History
440 lines (381 loc) · 12.7 KB

File metadata and controls

440 lines (381 loc) · 12.7 KB

Custom Menu Slots - Rendering

All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support cross-framework compatible slot rendering for custom content injection in menu items. This is achieved through the slotRenderer callback at the item level combined with an optional defaultMenuItemRenderer at the menu level.

Note: This documentation covers how menu items are rendered (visual presentation). If you need to dynamically modify which commands appear in the menu (filtering, sorting, adding/removing items), see the commandListBuilder callback documented in Grid Menu, Context Menu, or Header Menu.

TypeScript Tip: Type Inference with commandListBuilder

When using commandListBuilder to add custom menu items with slotRenderer callbacks, cast the return value to the appropriate type to enable proper type parameters in callbacks:

contextMenu: {
  commandListBuilder: (builtInItems) => {
    return [
      ...builtInItems,
      {
        command: 'custom-action',
        title: 'My Action',
        slotRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
      }
    ] as Array<MenuCommandItem | GridMenuItem | 'divider'>;
  }
}

Alternatively, if you only have built-in items and dividers, you can use the simpler cast:

return [...] as Array<MenuCommandItem | 'divider'>;

Core Concept

Each menu item can define a slotRenderer callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.

Slot Renderer Callback

slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement
  • cmdItem - The menu cmdItem object containing command, title, iconCssClass, etc.
  • args - The callback args providing access to grid, column, dataContext, and other context
  • event - Optional DOM event passed during click handling (allows stopPropagation())

Basic Example - HTML String Rendering

const menuItem = {
  command: 'custom-command',
  title: 'Custom Action',
  iconCssClass: 'mdi mdi-star',
  // Return custom HTML string for the entire menu item
  slotRenderer: () => `
    <div class="custom-menu-item">
      <i class="mdi mdi-star"></i>
      <span>Custom Action</span>
      <span class="badge">NEW</span>
    </div>
  `
};

Advanced Example - HTMLElement Objects

This approach is safer since it's CSP compliant with its use native HTML Elements. This could also be used to add event listeners or simply use the action callback.

// Create custom element with full DOM control
const menuItem = {
  command: 'notifications',
  title: 'Notifications',
  // Return HTMLElement for more control and event listeners
  slotRenderer: (cmdItem, args) => {
    const container = document.createElement('div');
    container.style.display = 'flex';
    container.style.alignItems = 'center';

    const icon = document.createElement('i');
    icon.className = 'mdi mdi-bell';
    icon.style.marginRight = '8px';

    const text = document.createElement('span');
    text.textContent = cmdItem.title;

    const badge = document.createElement('span');
    badge.className = 'badge';
    badge.textContent = '5';
    badge.style.marginLeft = 'auto';

    container.appendChild(icon);
    container.appendChild(text);
    container.appendChild(badge);

    return container;
  }
};

Default Menu-Level Renderer

Set a defaultMenuItemRenderer at the menu option level to apply to all items (unless overridden by individual slotRenderer):

const menuOption = {
  // Apply this renderer to all menu items (can be overridden per item)
  defaultMenuItemRenderer: (cmdItem, args) => {
    return `
      <div style="display: flex; align-items: center; gap: 8px;">
        ${cmdItem.iconCssClass ? `<i class="${cmdItem.iconCssClass}" style="font-size: 18px;"></i>` : ''}
        <span style="flex: 1;">${cmdItem.title}</span>
      </div>
    `;
  },
  commandItems: [
    {
      command: 'action-1',
      title: 'Action One',
      iconCssClass: 'mdi mdi-check',
      // This item uses defaultMenuItemRenderer
    },
    {
      command: 'custom',
      title: 'Custom Item',
      // This item overrides defaultMenuItemRenderer with its own slotRenderer
      slotRenderer: () => `
        <div style="color: #ff6b6b; font-weight: bold;">
          Custom rendering overrides default
        </div>
      `
    }
  ]
};

Menu Types & Configuration

The slotRenderer and defaultMenuItemRenderer work identically across all menu plugins:

Header Menu

const columnDef = {
  id: 'name',
  header: {
    menu: {
      defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
      commandItems: [
        {
          command: 'sort',
          title: 'Sort',
          slotRenderer: () => '<div>Custom sort</div>'
        }
      ]
    }
  }
};

Cell Menu

const columnDef = {
  id: 'action',
  cellMenu: {
    defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
    commandItems: [
      {
        command: 'edit',
        title: 'Edit',
        slotRenderer: (cmdItem, args) => `<div>Edit row ${args.dataContext.id}</div>`
      }
    ]
  }
};

Context Menu

const gridOptions = {
  enableContextMenu: true,
  contextMenu: {
    defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
    commandItems: [
      {
        command: 'export',
        title: 'Export',
        slotRenderer: () => '<div>📊 Export data</div>'
      }
    ]
  }
};

Grid Menu

const gridOptions = {
  enableGridMenu: true,
  gridMenu: {
    defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
    commandItems: [
      {
        command: 'refresh',
        title: 'Refresh',
        slotRenderer: () => '<div>🔄 Refresh data</div>'
      }
    ]
  }
};

Real-World Use Cases

1. Add Keyboard Shortcuts

{
  command: 'copy',
  title: 'Copy',
  iconCssClass: 'mdi mdi-content-copy',
  slotRenderer: () => `
    <div style="display: flex; align-items: center; gap: 8px;">
      <i class="mdi mdi-content-copy" style="font-size: 18px;"></i>
      <span style="flex: 1;">Copy</span>
      <kbd style="background: #eee; border: 1px solid #ccc; border-radius: 2px; padding: 2px 4px; font-size: 10px;">Ctrl+C</kbd>
    </div>
  `
}

2. Add Status Indicators

{
  command: 'filter',
  title: 'Filter',
  iconCssClass: 'mdi mdi-filter',
  slotRenderer: () => `
    <div style="display: flex; align-items: center; gap: 8px;">
      <i class="mdi mdi-filter" style="font-size: 18px;"></i>
      <span style="flex: 1;">Filter</span>
      <span style="width: 6px; height: 6px; border-radius: 50%; background: #44ff44; box-shadow: 0 0 4px #44ff44;"></span>
    </div>
  `
}

3. Add Dynamic Content Based on Context

{
  command: 'edit-row',
  title: 'Edit Row',
  slotRenderer: (cmdItem, args) => `
    <div style="display: flex; align-items: center; gap: 8px;">
      <i class="mdi mdi-pencil" style="font-size: 18px;"></i>
      <span>Edit Row #${args.dataContext?.id || 'N/A'}</span>
    </div>
  `
}

4. Add Interactive Elements

{
  command: 'toggle-setting',
  title: 'Auto Refresh',
  slotRenderer: (cmdItem, args, event) => {
    const container = document.createElement('label');
    container.style.display = 'flex';
    container.style.alignItems = 'center';
    container.style.gap = '8px';
    container.style.marginRight = 'auto';

    const checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.addEventListener('change', (e) => {
      // Prevent menu item click from firing when toggling checkbox
      event?.stopPropagation?.();
      console.log('Auto refresh:', checkbox.checked);
    });

    const label = document.createElement('span');
    label.textContent = cmdItem.title;

    container.appendChild(label);
    container.appendChild(checkbox);
    return container;
  }
}

5. Add Badges and Status Labels

{
  command: 'export-excel',
  title: 'Export as Excel',
  slotRenderer: (cmdItem, args) => `
    <div style="display: flex; align-items: center; gap: 8px;">
      <i class="mdi mdi-file-excel-outline"></i>
      <span style="flex: 1;">${cmdItem.title}</span>
      <span style="background: #44ff44; color: #000; padding: 2px 4px; border-radius: 3px; font-size: 9px; font-weight: bold;">RECOMMENDED</span>
    </div>
  `
}

6. Gradient and Styled Icons

{
  command: 'advanced-export',
  title: 'Advanced Export',
  slotRenderer: (cmdItem, args) => {
    const container = document.createElement('div');
    container.style.display = 'flex';
    container.style.alignItems = 'center';
    container.style.gap = '8px';

    const iconDiv = document.createElement('div');
    iconDiv.style.width = '20px';
    iconDiv.style.height = '20px';
    iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
    iconDiv.style.borderRadius = '4px';
    iconDiv.style.display = 'flex';
    iconDiv.style.alignItems = 'center';
    iconDiv.style.justifyContent = 'center';
    iconDiv.style.color = 'white';
    iconDiv.style.fontSize = '12px';
    iconDiv.innerHTML = '📊';

    const textSpan = document.createElement('span');
    textSpan.textContent = cmdItem.title;

    container.appendChild(iconDiv);
    container.appendChild(textSpan);
    return container;
  }
}

Notes and Best Practices

  • HTML strings are inserted via innerHTML - ensure content is sanitized if user-provided
  • HTMLElement objects are appended directly - safer for dynamic content and allows event listeners
  • Cross-framework compatible - works in vanilla JS and any other frameworks using the same API
  • Priority order - Item-level slotRenderer overrides menu-level defaultMenuItemRenderer
  • Built-in command preservation - When overriding a built-in command (e.g., sort-asc, sort-desc, hide, etc.) with custom properties like slotRenderer or iconCssClass, if you don't provide an action callback, the library will automatically preserve and use the built-in action for that command. This means you can safely customize the appearance of built-in commands without losing their functionality.
  • Accessibility - Include proper ARIA attributes when creating custom elements
  • Event handling - Call event.stopPropagation() in interactive elements to prevent menu commands from firing and closing the menu
  • Default fallback - If neither slotRenderer nor defaultMenuItemRenderer is provided, the default icon + text rendering is used
  • Performance - Avoid heavy DOM manipulation inside renderer callbacks (they may be called multiple times)
  • Event parameter - The optional event parameter is passed during click handling and allows you to control menu behavior
  • All menus supported - This API works uniformly across Header Menu, Cell Menu, Context Menu, and Grid Menu

Styling Custom Menu Items

/* Example CSS for styled menu items */
.slick-menu-item {
  padding: 4px 8px;
}

.slick-menu-item div {
  display: flex;
  align-items: center;
  gap: 8px;
}

.slick-menu-item kbd {
  background: #f0f0f0;
  border: 1px solid #ddd;
  border-radius: 3px;
  padding: 2px 6px;
  font-size: 11px;
  font-family: monospace;
  color: #666;
}

.slick-menu-item .badge {
  background: #ff6b6b;
  color: white;
  padding: 2px 6px;
  border-radius: 3px;
  font-size: 9px;
  font-weight: bold;
  white-space: nowrap;
}

.slick-menu-item:hover {
  background: #f5f5f5;
}

.slick-menu-item.slick-menu-item-disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Migration from Static Rendering

Before (Static HTML Title):

{
  command: 'action',
  title: 'Action ⭐',  // Emoji embedded in title
  iconCssClass: 'mdi mdi-star'
}

After (Custom Rendering):

{
  command: 'action',
  title: 'Action',
  slotRenderer: () => `
    <div style="display: flex; align-items: center; gap: 8px;">
      <i class="mdi mdi-star" style="font-size: 18px;"></i>
      <span style="flex: 1;">Action</span>
      <span style="font-size: 18px;">⭐</span>
    </div>
  `
}

Error Handling

When creating custom renderers, handle potential errors gracefully:

{
  command: 'safe-render',
  title: 'Safe Render',
  slotRenderer: (cmdItem, args) => {
    try {
      if (args?.dataContext?.status === 'error') {
        return `<div style="color: red;">❌ Error loading</div>`;
      }
      return `<div>✓ Data loaded</div>`;
    } catch (error) {
      console.error('Render error:', error);
      return '<div>Render error</div>';
    }
  }
}