Angular Data GridFeedback Cell Type - Step-by-Step Guide
- Overview
- What You'll Build
- Complete Example
- Prerequisites
- Step 1: Import Dependencies
- Step 2: Add CSS Styling
- Step 3: Editor - Initialize (init)
- Step 4: Editor - Render Function
- Step 5: Editor - Keyboard Shortcuts
- Step 6: Editor β Custom Tab Key Behavior
- Step 7: Editor - Before Open Hook
- Step 8: Complete Cell Definition
- Step 9: Register and Use in Handsontable
- How It Works - Complete Flow
- Enhancements
- Accessibility
Overview
This guide shows how to create a simple feedback editor cell using emoji buttons. 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 (pure HTML)
What You'll Build
A cell that:
- Displays emoji feedback buttons (rounded) when editing
- Shows the selected emoji when viewing
- Uses Handsontable CSS tokens for theme-aware styling
- Supports keyboard navigation (arrow keys, Tab)
- Provides click-to-select functionality
- Works without any external libraries
Complete Example
Prerequisites
None! This uses only native HTML and JavaScript features.
Step 1: Import Dependencies
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { editorFactory } from 'handsontable/editors';
import { registerCellType } from 'handsontable/cellTypes';
registerAllModules();
What we're NOT importing:
- No date libraries
- No UI component libraries
- No external emoji libraries
- Just Handsontable.
Step 2: Add CSS Styling
Create a separate CSS file for the editor styles. This uses Handsontable CSS custom properties (tokens) so the editor automatically adapts to custom themes and dark mode.
.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);
}
Handsontable tokens used:
--ht-cell-editor-border-color/--ht-cell-editor-border-width- blue border matching native editors--ht-cell-editor-background-color- editor background--ht-cell-vertical-padding/--ht-cell-horizontal-padding- consistent cell padding--ht-background-color/--ht-foreground-color- base button colors--ht-border-color- button borders and hover state--ht-accent-color- active/selected button highlight--ht-border-radius- button corner rounding--ht-font-size/--ht-gap- consistent sizing
Step 3: Editor - Initialize (init)
Create the DOM structure with emoji buttons, this function will be called only once.
init(editor) {
editor.input = document.createElement('DIV') as HTMLDivElement;
editor.input.classList.add('feedback-editor');
editor.input.addEventListener('click', (event) => {
if (event.target instanceof HTMLButtonElement) {
editor.setValue(event.target.innerText);
editor.finishEditing();
}
});
editor.render(editor);
}
What's happening:
- Create a
divcontainer for the buttons - Add the
feedback-editorCSS class (all styling is in the CSS file) - Add click handler to detect button clicks
- When a button is clicked, set the value and finish editing
- Call
renderto create the initial button layout
Step 4: Editor - Render Function
Create buttons dynamically based on the config, using CSS classes instead of inline styles.
render(editor) {
editor.input.innerHTML = editor.config
.map((option) =>
`<button class="${editor.value === option ? 'active' : ''}">${option}</button>`
)
.join('');
}
What's happening:
- Generate HTML for each button from
configarray - Add
activeclass to the currently selected button - The
.activeCSS class applies--ht-accent-coloras background - Each button takes 33% width with rounded corners (
--ht-border-radius)
Step 5: Editor - Keyboard Shortcuts
Add arrow key navigation to cycle through options.
shortcuts: [
{
keys: [['ArrowRight']],
callback: (editor, _event) => {
let index = editor.config.indexOf(editor.value);
index = index === editor.config.length - 1 ? 0 : index + 1;
editor.setValue(editor.config[index]);
}
},
{
keys: [['ArrowLeft']],
callback: (editor, _event) => {
let index = editor.config.indexOf(editor.value);
index = index === 0 ? editor.config.length - 1 : index - 1;
editor.setValue(editor.config[index]);
}
}
]
What's happening:
- ArrowRight: Move to next option (wraps to first if at end)
- ArrowLeft: Move to previous option (wraps to last if at start)
- Finds current index in config array
- Updates value and triggers render automatically
Keyboard navigation benefits:
- Fast selection without mouse
- Accessible for keyboard-only users
- Intuitive left/right navigation
Step 6: Editor β Custom Tab Key Behavior
By default, pressing Tab in Handsontable saves the cell and moves the selection horizontally, following your layout direction.
In this example, we want Tab to cycle through feedback optionsβjust like the arrow keysβwithout moving to another cell.
To achieve this, we use the editor's shortcuts and return false in callback to prevent the default action (saving and moving to the next cell).
shortcuts: [
{
keys: [['ArrowRight'], ['Tab']],
callback: (editor, _event) => {
let index = editor.config.indexOf(editor.value);
index = index === editor.config.length - 1 ? 0 : index + 1;
editor.setValue(editor.config[index]);
return false; // Prevent default tabbing behavior
},
},
]
How it works:
- Listens for Tab when the editor is active
- Moves to the next option in
config(wraps around at the end) - Updates the editor's value and button highlight
- Returning
falseblocks Handsontable's built-in tab handler, so editing stays in place
Step 7: Editor - Before Open Hook
Initialize the editor with the current cell value when editing starts.
beforeOpen(editor, { originalValue, cellProperties }) {
editor.setValue(originalValue);
}
What's happening:
- Called when editor is about to open
- Receives the current cell value as
originalValue - Sets the editor's value to match the cell
- This ensures the correct button is highlighted when editing starts
Step 8: Complete Cell Definition
const cellDefinition = {
editor: editorFactory<{ input: HTMLDivElement; value: string; config: string[] }>({
config: ['π', 'π', 'π€·'],
value: 'π',
shortcuts: [
{
keys: [['ArrowRight'], ['Tab']],
callback: (editor, _event) => {
let index = editor.config.indexOf(editor.value);
index = index === editor.config.length - 1 ? 0 : index + 1;
editor.setValue(editor.config[index]);
return false;
},
},
{
keys: [['ArrowLeft']],
callback: (editor, _event) => {
let index = editor.config.indexOf(editor.value);
index = index === 0 ? editor.config.length - 1 : index - 1;
editor.setValue(editor.config[index]);
},
},
],
render: (editor) => {
editor.input.innerHTML = editor.config
.map(
(option) =>
`<button class="${editor.value === option ? 'active' : ''}">${option}</button>`,
)
.join('');
},
init: (editor) => {
editor.input = document.createElement('DIV') as HTMLDivElement;
editor.input.classList.add('feedback-editor');
editor.input.addEventListener('click', (event) => {
if (event.target instanceof HTMLButtonElement) {
editor.setValue(event.target.innerText);
editor.finishEditing();
}
});
editor.render(editor);
},
beforeOpen: (editor, { originalValue, cellProperties }) => {
editor.setValue(originalValue);
},
}),
};
What's happening:
- config: Array of emoji options (
π,π,π€·) - value: Default/initial value
- shortcuts: Keyboard navigation (ArrowLeft/Right cycle options, Tab cycles and prevents default)
- render: Creates button HTML with
activeCSS class for the selected option - init: Sets up the container with
feedback-editorclass and click handler - beforeOpen: Initializes editor with the current cell value
Note: No custom renderer needed! Handsontable's default renderer will display the emoji value in the cell. All visual styling is handled by the CSS file using Handsontable tokens.
Step 9: Register and Use in Handsontable
Register the cell definition as a reusable cell type, then use it in the column configuration.
registerCellType('feedback', cellDefinition);
const container = document.querySelector('#example1')!;
const hotOptions: Handsontable.GridSettings = {
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' },
],
colHeaders: ['Feature', 'Category', 'Priority', 'Feedback', 'Votes', 'Status'],
autoRowSize: true,
rowHeaders: true,
autoWrapRow: true,
height: 'auto',
width: '100%',
headerClassName: 'htLeft',
columns: [
{ data: 'feature', type: 'text', width: 200 },
{ data: 'category', type: 'text', width: 90 },
{ data: 'priority', type: 'text', width: 100 },
{ data: 'feedback', width: 100, type: 'feedback' },
{ data: 'votes', type: 'numeric', width: 60 },
{ data: 'status', type: 'text', width: 120 },
],
licenseKey: 'non-commercial-and-evaluation',
};
const hot = new Handsontable(container, hotOptions);
Key configuration:
registerCellType('feedback', cellDefinition)- Registers the editor as a reusable cell typetype: 'feedback'- Applies the cell type to the Feedback columnheaderClassName: 'htLeft'- Left-aligns all column headers
How It Works - Complete Flow
- Initial Render: Cell displays the emoji value (π, π, or π€·)
- User Double-Clicks or Enter: Editor opens over cell showing three rounded buttons with the Handsontable blue border
- Button Display: All options visible, current value highlighted using
--ht-accent-color - User Interaction:
- Click a button: Selects value and closes editor
- Press ArrowLeft/Right: Cycles through options
- Press Tab: Cycles through options (stays in editor)
- Enter key saves value and closes editor
- Visual Feedback: Selected button highlighted with accent color
- Save: Value saved to cell
- Editor Closes: Cell shows selected emoji
Enhancements
1. More Feedback Options
Add more emoji options by extending the config array and adjusting the button width in CSS:
config: ['π', 'π', 'π€·', 'β€οΈ', 'π₯', 'β'],
2. Dynamic Config from Cell Properties
Make options configurable per column:
beforeOpen: (editor, { cellProperties }) => {
if (cellProperties.feedbackOptions) {
editor.config = cellProperties.feedbackOptions;
}
editor.setValue(editor.originalValue || editor.value);
},
// Usage
columns: [{
data: 'feedback',
type: 'feedback',
feedbackOptions: ['π', 'π', 'β€οΈ', 'π₯']
}]
3. Tooltip on Hover
Add tooltips to buttons:
render: (editor) => {
const tooltips = { 'π': 'Positive', 'π': 'Negative', 'π€·': 'Neutral' };
editor.input.innerHTML = editor.config
.map((option) =>
`<button class="${editor.value === option ? 'active' : ''}" title="${tooltips[option] || ''}">${option}</button>`
)
.join('');
}
4. Text Labels Instead of Emojis
Use text buttons for clarity:
config: ['Positive', 'Negative', 'Neutral'],
Accessibility
Keyboard navigation:
- Tab: Cycles through feedback options (stays in editor)
- Arrow Left/Right: Cycles through options
- Enter: Saves value and closes editor
- Escape: Cancels editing
- Click: Direct selection
Congratulations! You've created a theme-aware feedback editor with emoji buttons using Handsontable CSS tokens, perfect for quick feedback selection in your data grid!