Feedback Editor
This tutorial shows you how to build an emoji feedback cell in Angular using HotCellEditorAdvancedComponent, with Handsontable CSS tokens for theme-aware styling and keyboard navigation.
Overview
This guide shows how to create a feedback editor cell using emoji buttons with Angularβs HotCellEditorAdvancedComponent. Use it for status indicators or any scenario where users 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
Prerequisites
npm install @handsontable/angular-wrapperWhat you need:
- Angular 16+ (decorators support)
@handsontable/angular-wrapperpackage- Basic Angular knowledge (components, decorators, change detection)
Import Dependencies
import { Component, ChangeDetectorRef } from "@angular/core";import { HotTableModule, HotCellEditorAdvancedComponent, KeyboardShortcutConfig } from "@handsontable/angular-wrapper";What weβre importing:
HotCellEditorAdvancedComponent- Base class for creating custom editorsHotTableModule- Angular module providing the<hot-table>component (imported inAppComponent)ChangeDetectorRef- For manual change detectionKeyboardShortcutConfig- Type definitions for keyboard shortcuts
registerAllModules()is called once inapp.config.ts, not in component files.Create the Editor Component
Create an Angular component that extends
HotCellEditorAdvancedComponent.@Component({standalone: true,imports: [],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
- Class extends
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) 0var(--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 classesfeedback-editorandactivein 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.Read Config from Cell Properties
The Angular wrapper automatically handles per-column configuration through the base class.
import { Component } from "@angular/core";import { GridSettings, HotTableModule } from "@handsontable/angular-wrapper";@Component({selector: "app-root",standalone: true,imports: [HotTableModule],template: ` <hot-table [data]="data" [settings]="gridSettings"></hot-table> `,})export class AppComponent {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: true,imports: [],template: `...`,})export class FeedbackEditorComponent extends HotCellEditorAdvancedComponent<string> {// Config will be automatically populated from cellProperties.configoverride 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
Add Keyboard Shortcuts
Add keyboard navigation using the
shortcutsproperty.@Component({standalone: true,imports: [],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 detectionreturn 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
Complete Editor Component
Put it all together:
@Component({standalone: true,imports: [],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)
- Class properties:
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, HotTableModule } from "@handsontable/angular-wrapper";@Component({selector: "app-root",standalone: true,imports: [HotTableModule],template: ` <hot-table [data]="data" [settings]="gridSettings"></hot-table> `,})export class AppComponent {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
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.
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-editorand.activerules withvar(--ht-*)tokens so the editor stays theme-aware.Dynamic Config from Cell Properties
To read custom config from column definition, implement the
beforeOpenlifecycle hook:@Component({standalone: true,imports: [],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
Tooltip on Hover
Add tooltips to buttons using the
titleattribute: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
tooltipsas a class property or use a getter.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.
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. UsestyleUrls: ['./example1.css'](or your own path) in the component. For a different file name or folder, pointstyleUrlsto that file and keep the same.feedback-editorand.activerules withvar(--ht-*)tokens for theme-aware styling.
Accessibility
HTML buttons are inherently accessible, but you can enhance them with ARIA attributes:
@Component({ standalone: true, imports: [], 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!
What you learned
You built an emoji feedback cell editor in Angular using HotCellEditorAdvancedComponent. You used Handsontable CSS tokens for theme-aware button styling, override shortcuts for keyboard navigation, and ChangeDetectorRef to trigger view updates after keyboard-driven value changes.
Next steps
- Feedback (JavaScript) - The same concept using
editorFactorywith Handsontable CSS tokens. - Feedback (React) - The React version using
EditorComponent. - Star Rating Editor (Angular) - Another Angular editor using
HotCellEditorAdvancedComponentwith SVG stars.