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-wrapperpackage- 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 editorsHotTableModule- Angular wrapper moduleChangeDetectorRef- For manual change detectionKeyboardShortcutConfig- 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:
- Class extends
HotCellEditorAdvancedComponent<string>- base class for custom editors @Componentdecorator with inline template andstyleUrlsfor theme-aware CSS (Handsontable tokens)override config- array of options to display (π,π,π€·)override value- default value for the editorgetValue()/setValue()- inherited methods for value managementfinishEdit.emit()- emits event to save and close the editorChangeDetectorRef- injected for manual change detection@for- loops through config optionsfeedback-editorandactiveCSS classes - styled via external CSS using Handsontable tokens (same as the Feedback recipe)
Key concepts:
- Class-based component: Extends from
HotCellEditorAdvancedComponent<T> - State management:
valueis 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
configfromcellProperties - No manual
onPrepareimplementation needed for basic config reading - Each column can pass different
configvalues
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 eventthis.getValue()andthis.setValue()- access current value and update itthis.cdr.detectChanges()- triggers UI update after value change- Return
falseto 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, andshortcutsdefined as class properties - Template: Uses
feedback-editorandactiveclasses; 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 columnconfig: ['π', 'π', 'π€·']- Column-specific options- Same editor component can be reused with different
configper column - Configuration through
GridSettingsinterface
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:
beforeOpenis called before the editor opens- Read
configfromcellProperties.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!