Angular Data GridFeedback Editor Cell - Step-by-Step Guide (Angular)

Overview

This guide shows how to create a simple feedback editor cell using emoji buttons with Angular's HotCellEditorAdvancedComponent. Perfect for quick feedback selection, status indicators, or any scenario where users need to choose from a small set of visual options.

Difficulty: Beginner Time: ~15 minutes Libraries: None

What You'll Build

A cell that:

  • Displays emoji feedback buttons when editing
  • Shows the selected emoji when viewing
  • Uses Handsontable CSS tokens for theme-aware styling (same look as the Feedback recipe)
  • Supports keyboard navigation (arrow keys and Tab)
  • Provides click-to-select functionality
  • Works with Angular's component-based architecture
  • Supports per-column configuration

Complete Example

    Prerequisites

    npm install @handsontable/angular-wrapper
    

    What you need:

    • Angular 16+ (decorators support)
    • @handsontable/angular-wrapper package
    • Basic Angular knowledge (components, decorators, change detection)

    Step 1: Import Dependencies

    import { Component, ChangeDetectorRef } from "@angular/core";
    import { HotTableModule, HotCellEditorAdvancedComponent, KeyboardShortcutConfig } from "@handsontable/angular-wrapper";
    import { registerAllModules } from "handsontable/registry";
    
    registerAllModules();
    

    What we're importing:

    • HotCellEditorAdvancedComponent - Base class for creating custom editors
    • HotTableModule - Angular wrapper module
    • ChangeDetectorRef - For manual change detection
    • KeyboardShortcutConfig - Type definitions for keyboard shortcuts
    • Handsontable styles

    Step 2: Create the Editor Component

    Create an Angular component that extends HotCellEditorAdvancedComponent.

    @Component({
      standalone: false,
      template: `
        <div class="feedback-editor">
          @for (option of config; track option) {
          <button [class.active]="option === getValue()" (click)="onClick(option)">
            {{ option }}
          </button>
          }
        </div>
      `,
      styleUrls: ['./example1.css'],
    })
    export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {
      override config = ["πŸ‘", "πŸ‘Ž", "🀷"];
      override value = "πŸ‘";
    
      private readonly cdr = inject(ChangeDetectorRef);
    
      onClick(option: string): void {
        this.setValue(option);
        this.finishEdit.emit();
      }
    }
    

    What's happening:

    1. Class extends HotCellEditorAdvancedComponent<string> - base class for custom editors
    2. @Component decorator with inline template and styleUrls for theme-aware CSS (Handsontable tokens)
    3. override config - array of options to display (πŸ‘, πŸ‘Ž, 🀷)
    4. override value - default value for the editor
    5. getValue() / setValue() - inherited methods for value management
    6. finishEdit.emit() - emits event to save and close the editor
    7. ChangeDetectorRef - injected for manual change detection
    8. @for - loops through config options
    9. feedback-editor and active CSS classes - styled via external CSS using Handsontable tokens (same as the Feedback recipe)

    Key concepts:

    • Class-based component: Extends from HotCellEditorAdvancedComponent<T>
    • State management: value is a class property, managed by base class
    • Angular patterns: Template syntax, property binding, event binding

    Step 3: Add Styling

    Use a separate CSS file with Handsontable CSS custom properties (tokens) so the editor matches native editors and adapts to themes and dark modeβ€”same approach as the Feedback recipe.

    example1.css:

    .feedback-editor {
      display: flex;
      gap: var(--ht-gap, 4px);
      width: 100%;
      height: 100%;
      box-sizing: border-box !important;
      padding: var(--ht-cell-vertical-padding, 4px) var(--ht-cell-horizontal-padding, 8px);
      background-color: var(--ht-cell-editor-background-color, #ffffff);
      box-shadow: inset 0 0 0 var(--ht-cell-editor-border-width, 2px)
        var(--ht-cell-editor-border-color, #1a42e8),
        0 0 var(--ht-cell-editor-shadow-blur-radius, 0) 0
        var(--ht-cell-editor-shadow-color, transparent);
      border: none;
      border-radius: 0;
    }
    
    .feedback-editor button {
      background: var(--ht-background-color, #ffffff);
      color: var(--ht-foreground-color, #000000);
      border: 1px solid var(--ht-border-color, #e0e0e0);
      border-radius: var(--ht-border-radius, 4px);
      padding: 0;
      margin: 0;
      height: 100%;
      width: 33%;
      font-size: var(--ht-font-size, 14px);
      text-align: center;
      cursor: pointer;
    }
    
    .feedback-editor button:hover {
      background: var(--ht-border-color, #e0e0e0);
    }
    
    .feedback-editor button.active,
    .feedback-editor button.active:hover {
      background: var(--ht-accent-color, #1a42e8);
      color: #ffffff;
      border-color: var(--ht-accent-color, #1a42e8);
    }
    

    Reference it in the component with styleUrls: ['./example1.css'] and use classes feedback-editor and active in the template.

    Key tokens: --ht-cell-editor-border-color, --ht-cell-editor-background-color, --ht-accent-color, --ht-background-color, --ht-foreground-color, --ht-border-color, --ht-font-size, --ht-gap.

    Step 4: Read Config from Cell Properties

    The Angular wrapper automatically handles per-column configuration through the base class.

    import { Component } from "@angular/core";
    import { GridSettings } from "@handsontable/angular-wrapper";
    
    @Component({
      selector: "app-example",
      template: ` <hot-table [data]="data" [settings]="gridSettings"></hot-table> `,
    })
    export class ExampleComponent {
      readonly data = [
        { feature: "Dark Mode", category: "UI", priority: "High", feedback: "πŸ‘", votes: 124, status: "Planned" },
        { feature: "Bulk Edit", category: "Core", priority: "High", feedback: "πŸ‘", votes: 98, status: "In Progress" },
        { feature: "AI Suggestions", category: "Beta", priority: "Medium", feedback: "🀷", votes: 45, status: "Research" },
        { feature: "Offline Mode", category: "Infra", priority: "Low", feedback: "πŸ‘Ž", votes: 12, status: "Backlog" },
      ];
    
      readonly gridSettings: GridSettings = {
        ...,
        columns: [
          { data: "feature", type: "text", width: 200 },
          { data: "category", type: "text", width: 90 },
          { data: "priority", type: "text", width: 100 },
          {
            data: "feedback",
            width: 100,
            editor: FeedbackEditorComponent,
            config: ["πŸ‘", "πŸ‘Ž", "🀷"],
          },
          { data: "votes", type: "numeric", width: 60 },
          { data: "status", type: "text", width: 120 },
        ],
      };
    }
    
    @Component({
      standalone: false,
      template: `...`,
    })
    export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {
      // Config will be automatically populated from cellProperties.config
      override config?: string[];
      override value = "πŸ‘";
    
      private readonly cdr = inject(ChangeDetectorRef);
    
      onClick(option: string): void {
        this.setValue(option);
        this.finishEdit.emit();
      }
    }
    

    What's happening:

    • override config - allows the base class to set config from column definition
    • The Angular wrapper automatically reads config from cellProperties
    • No manual onPrepare implementation needed for basic config reading
    • Each column can pass different config values

    Why this matters:

    • Different columns can have different options
    • One editor component, multiple configurations
    • Automatic configuration injection by the wrapper

    Step 5: Add Keyboard Shortcuts

    Add keyboard navigation using the shortcuts property.

    @Component({
      standalone: false,
      template: `...`,
    })
    export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {
      override config = ["πŸ‘", "πŸ‘Ž", "🀷"];
      override value = "πŸ‘";
    
      override shortcuts: KeyboardShortcutConfig[] = [
        {
          keys: [["ArrowRight"], ["Tab"]],
          callback: (editor, _event) => {
            let index = this.config.indexOf(this.getValue());
            index = index === this.config.length - 1 ? 0 : index + 1;
            this.setValue(this.config[index]);
            this.cdr.detectChanges(); // Manual change detection
            return false; // Prevent default Tab behavior
          },
        },
        {
          keys: [["ArrowLeft"]],
          callback: (editor, _event) => {
            let index = this.config.indexOf(this.getValue());
            index = index === 0 ? this.config.length - 1 : index - 1;
            this.setValue(this.config[index]);
            this.cdr.detectChanges();
          },
        },
      ];
    
      private readonly cdr = inject(ChangeDetectorRef);
    
      onClick(option: string): void {
        this.setValue(option);
        this.finishEdit.emit();
      }
    }
    

    What's happening:

    • ArrowRight/Tab: Move to next option (wraps to first if at end)
    • ArrowLeft: Move to previous option (wraps to last if at start)
    • callback: (editor, _event) => {} - receives editor instance and event
    • this.getValue() and this.setValue() - access current value and update it
    • this.cdr.detectChanges() - triggers UI update after value change
    • Return false to prevent default behavior (e.g., Tab moving to next cell)

    Keyboard navigation benefits:

    • Fast selection without mouse
    • Accessible for keyboard-only users
    • Intuitive left/right navigation
    • Tab cycles through options instead of moving cells

    Step 6: Complete Editor Component

    Put it all together:

    @Component({
      standalone: false,
      template: `
        <div class="feedback-editor">
          @for (option of config; track option) {
          <button [class.active]="option === getValue()" (click)="onClick(option)">
            {{ option }}
          </button>
          }
        </div>
      `,
      styleUrls: ['./example1.css'],
    })
    export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {
      override config = ["πŸ‘", "πŸ‘Ž", "🀷"];
      override value = "πŸ‘";
    
      override shortcuts: KeyboardShortcutConfig[] = [
        {
          keys: [["ArrowRight"], ["Tab"]],
          callback: (editor, _event) => {
            let index = this.config.indexOf(this.getValue());
            index = index === this.config.length - 1 ? 0 : index + 1;
            this.setValue(this.config[index]);
            this.cdr.detectChanges();
            return false;
          },
        },
        {
          keys: [["ArrowLeft"]],
          callback: (editor, _event) => {
            let index = this.config.indexOf(this.getValue());
            index = index === 0 ? this.config.length - 1 : index - 1;
            this.setValue(this.config[index]);
            this.cdr.detectChanges();
          },
        },
      ];
    
      private readonly cdr = inject(ChangeDetectorRef);
    
      onClick(option: string): void {
        this.setValue(option);
        this.finishEdit.emit();
      }
    }
    

    What's happening:

    • Class properties: config, value, and shortcuts defined as class properties
    • Template: Uses feedback-editor and active classes; styling from CSS file with Handsontable tokens
    • Keyboard shortcuts: Defined directly as class property
    • Change detection: Manual trigger with ChangeDetectorRef
    • Styling: External CSS with theme-aware tokens (same look as the Feedback recipe)

    Step 7: Use in Handsontable

    Use the editor component in your Angular component (same table structure as the Feedback recipe):

    import { Component } from "@angular/core";
    import { GridSettings } from "@handsontable/angular-wrapper";
    
    @Component({
      selector: "app-example",
      template: ` <hot-table [data]="data" [settings]="gridSettings"></hot-table> `,
    })
    export class ExampleComponent {
      readonly data = [
        { feature: "Dark Mode", category: "UI", priority: "High", feedback: "πŸ‘", votes: 124, status: "Planned" },
        { feature: "Bulk Edit", category: "Core", priority: "High", feedback: "πŸ‘", votes: 98, status: "In Progress" },
        { feature: "AI Suggestions", category: "Beta", priority: "Medium", feedback: "🀷", votes: 45, status: "Research" },
        { feature: "Offline Mode", category: "Infra", priority: "Low", feedback: "πŸ‘Ž", votes: 12, status: "Backlog" },
      ];
    
      readonly gridSettings: GridSettings = {
        autoRowSize: true,
        rowHeaders: true,
        autoWrapRow: true,
        height: "auto",
        width: "100%",
        headerClassName: "htLeft",
        colHeaders: ["Feature", "Category", "Priority", "Feedback", "Votes", "Status"],
        columns: [
          { data: "feature", type: "text", width: 200 },
          { data: "category", type: "text", width: 90 },
          { data: "priority", type: "text", width: 100 },
          {
            data: "feedback",
            width: 100,
            editor: FeedbackEditorComponent,
            config: ["πŸ‘", "πŸ‘Ž", "🀷"],
          },
          { data: "votes", type: "numeric", width: 60 },
          { data: "status", type: "text", width: 120 },
        ],
      };
    }
    

    HTML (.html):

    <hot-table [data]="data" [settings]="gridSettings"></hot-table>
    

    What's happening:

    • editor: FeedbackEditorComponent - Assigns the editor class to the column
    • config: ['πŸ‘', 'πŸ‘Ž', '🀷'] - Column-specific options
    • Same editor component can be reused with different config per column
    • Configuration through GridSettings interface

    Key features:

    • Reusable editor component with theme-aware styling
    • Per-column configuration
    • Type-safe with TypeScript
    • Declarative settings object

    Enhancements

    1. More Feedback Options

    Add more emoji options (same as the Feedback recipe):

    readonly gridSettings: GridSettings = {
      columns: [
        {
          data: 'feedback',
          editor: FeedbackEditorComponent,
          config: ['πŸ‘', 'πŸ‘Ž', '🀷', '❀️', 'πŸ”₯', '⭐'],
        },
      ],
    };
    

    The editor automatically adjusts button widths based on config length.

    2. Custom Button Styling

    You can add extra styles (e.g. transitions, hover effects) in your CSS file while keeping Handsontable tokens for theme consistency. For example, in example1.css:

    .feedback-editor button {
      transition: transform 0.2s, box-shadow 0.2s;
    }
    
    .feedback-editor button:hover {
      transform: scale(1.05);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
    }
    

    Keep using the existing .feedback-editor and .active rules with var(--ht-*) tokens so the editor stays theme-aware.

    3. Dynamic Config from Cell Properties

    To read custom config from column definition, implement the beforeOpen lifecycle hook:

    @Component({
      standalone: false,
      template: `...`,
    })
    export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {
      override config?: string[];
      override value = "πŸ‘";
    
      override beforeOpen(editor: ExtendedEditor<any>, { cellProperties }: any): void {
        this.config = cellProperties.config as string[];
      }
    
      // ... rest of the component
    }
    

    Then pass different configs per column:

    columns: [
      {
        data: "feedback",
        editor: FeedbackEditorComponent,
        config: ["πŸ‘", "πŸ‘Ž", "❀️", "πŸ”₯"],
      },
    ];
    

    What's happening:

    • beforeOpen is called before the editor opens
    • Read config from cellProperties.config
    • Each column can have different options
    • One editor component, multiple configurations

    4. Tooltip on Hover

    Add tooltips to buttons using the title attribute:

    const tooltips: Record<string, string> = {
      "πŸ‘": "Positive",
      "πŸ‘Ž": "Negative",
      "🀷": "Neutral",
    };
    

    In the template:

    <div class="feedback-editor">
      @for (option of config; track option) {
      <button
        [class.active]="option === getValue()"
        [attr.title]="tooltips[option] ?? ''"
        (click)="onClick(option)"
      >
        {{ option }}
      </button>
      }
    </div>
    

    Expose tooltips as a class property or use a getter.

    5. Text Labels Instead of Emojis

    Use text buttons for clarity:

    columns: [
      {
        data: "feedback",
        editor: FeedbackEditorComponent,
        config: ["Positive", "Negative", "Neutral"],
      },
    ];
    

    The editor works with any string values, not just emojis.

    6. Using External CSS File

    The main example already uses an external CSS file (example1.css) with Handsontable tokensβ€”see Step 3 and the Feedback recipe CSS. Use styleUrls: ['./example1.css'] (or your own path) in the component. For a different file name or folder, point styleUrls to that file and keep the same .feedback-editor and .active rules with var(--ht-*) tokens for theme-aware styling.

    Accessibility

    HTML buttons are inherently accessible, but you can enhance them with ARIA attributes:

    @Component({
      standalone: false,
      template: `
        <div class="feedback-editor">
          @for (option of config; track $index) {
          <button
            [attr.aria-label]="option + ' feedback option'"
            [attr.aria-pressed]="option === getValue()"
            [tabIndex]="option === getValue() ? 0 : -1"
            [class.active]="option === getValue()"
            (click)="onClick(option)"
          >
            {{ option }}
          </button>
          }
        </div>
      `,
      styleUrls: ['./example1.css'],
    })
    export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {
      // ... component code
    }
    

    Keyboard navigation:

    • Tab: Navigate to editor (focuses active button)
    • Arrow Left/Right: Cycle through options (via shortcuts)
    • Enter: Select current option and finish editing
    • Escape: Cancel editing
    • Click: Direct selection

    ARIA attributes:

    • [attr.aria-label]: Describes each button
    • [attr.aria-pressed]: Indicates selected state
    • [tabIndex]: Controls keyboard focus order

    Congratulations! You've created a theme-aware feedback editor with emoji buttons using Angular's HotCellEditorAdvancedComponent, matching the look of the Feedback recipe and perfect for quick feedback selection in your data grid!