Angular Data GridStar Rating Cell Type - Step-by-Step Guide (Angular)
- Overview
- What You'll Build
- Complete Example
- Step 1: Import Dependencies
- Step 2: Create the Renderer Component
- Step 3: Add CSS Styling
- Step 4: Column Configuration (Optional Validator)
- Step 5: Create the Editor Component
- Step 6: Editor - Mouse Event Handlers
- Step 7: Editor - Keyboard Shortcuts
- Step 8: Complete Column Configuration
- Step 9: Create the Angular Component
- Step 10: Register in Angular Module
- Enhancements
- Accessibility
Overview
This guide shows how to create an interactive star rating cell using inline SVG stars with Angular's custom cell components. Perfect for product ratings, review scores, or any scenario where users need to provide a 1-5 star rating.
Difficulty: Beginner Time: ~15 minutes Libraries: None (pure HTML, SVG and JavaScript)
What You'll Build
A cell that:
- Displays 5 SVG stars both when editing and viewing
- Shows filled stars (gold) and unfilled stars (gray)
- Uses Handsontable CSS tokens for theme-aware editor styling
- Supports mouse hover for preview
- Allows keyboard input (1-5 keys, arrow keys)
- Provides immediate visual feedback
- Highlights the current star (accent color) while editing
- Works without any external libraries
Complete Example
Step 1: Import Dependencies
import { Component, ChangeDetectorRef, ChangeDetectionStrategy, inject } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import {
GridSettings,
HotCellEditorAdvancedComponent,
KeyboardShortcutConfig,
HotCellRendererAdvancedComponent,
} from "@handsontable/angular-wrapper";
import { registerAllModules } from "handsontable/registry";
registerAllModules();
What we're importing:
HotCellRendererAdvancedComponent- Base class for custom renderersHotCellEditorAdvancedComponent- Base class for custom editors with advanced featuresKeyboardShortcutConfig- Type for keyboard shortcuts configurationGridSettings- Type for Handsontable configurationDomSanitizer- Required so we can render SVG via[innerHTML](Angular strips SVG by default)- Angular core modules for component creation
What we're NOT importing:
- No date libraries
- No UI component libraries
- No external emoji libraries
Step 2: Create the Renderer Component
The renderer displays 5 SVG stars wrapped in a flex container using CSS classes for color control (same approach as the Star Rating recipe).
const starSvg =
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
@Component({
selector: "star-renderer",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="rating-cell">
@for (star of stars; track $index) {
<span class="rating-star" [class.active]="$index < value" [innerHTML]="starSvgMarkup"></span>
}
</div>`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarRendererComponent extends HotCellRendererAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
}
What's happening:
extends HotCellRendererAdvancedComponent<number>- Inherits base renderer functionality with typed valuevalueproperty - Automatically provided by the base class (1-5 rating).rating-cell- Flex container wrapping the stars (matches the editor layout).rating-star- Base class for each star (gray via CSS token--ht-background-secondary-color).active- Filled stars (gold#facc15)[innerHTML]="starSvgMarkup"- Inline SVG withfill="currentColor"so CSS controls the star colorinject(DomSanitizer).bypassSecurityTrustHtml(starSvg)- Angular sanitizes[innerHTML]and strips SVG by default; marking the SVG as trusted allows it to render
Why SVG instead of emoji?
- Consistent rendering across all browsers and operating systems
- Full control over color, size, and styling via CSS
- Theme-aware when using Handsontable CSS tokens
Step 3: Add CSS Styling
Create a separate CSS file for the rating styles. This uses Handsontable CSS custom properties (tokens) so the editor automatically adapts to custom themes and dark mode.
.rating-cell {
display: flex;
align-items: center;
margin: 3px 0 0 -1px;
}
.rating-star {
color: var(--ht-background-secondary-color, #e0e0e0);
cursor: default;
display: inline-flex;
align-items: center;
}
.rating-star.active {
color: #facc15;
}
.rating-editor {
display: flex;
align-items: center;
width: 100%;
height: 100%;
box-sizing: border-box !important;
border: none;
border-radius: 0;
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);
background-color: var(--ht-cell-editor-background-color, #ffffff);
padding: var(--ht-cell-vertical-padding, 4px)
var(--ht-cell-horizontal-padding, 8px);
font-family: inherit;
font-size: inherit;
line-height: inherit;
cursor: pointer;
}
.rating-editor .rating-star {
cursor: pointer;
}
.rating-editor .rating-star.current {
color: var(--ht-accent-color, #1a42e8);
}
Handsontable tokens used:
--ht-background-secondary-color- Inactive star color (adapts to theme)--ht-accent-color- Current star highlight in editor--ht-cell-editor-border-color/--ht-cell-editor-border-width- Editor border--ht-cell-editor-background-color- Editor background--ht-cell-vertical-padding/--ht-cell-horizontal-padding- Cell padding
Step 4: Column Configuration (Optional Validator)
In Angular, validators are typically configured at the column level in GridSettings. Here's how to ensure values are within the 1-5 star range:
columns: [
{
data: "stars",
width: 200,
renderer: StarRendererComponent,
editor: StarEditorComponent,
// Optional: Add validator to ensure valid range
validator: (value: number) => {
const rating = parseInt(value?.toString() || "0");
return rating >= 1 && rating <= 5;
},
},
];
What's happening:
- Angular validator uses
CustomValidatorFn<T>- returns boolean directly - Convert value to integer (keyboard input may be strings)
- Check if between 1 and 5 (star rating range)
- Returns
truefor valid,falsefor invalid - Validator runs before saving to data model
When to use:
- Validating user input from keyboard shortcuts
- Ensuring data integrity
- Providing visual feedback for invalid values
Step 5: Create the Editor Component
The editor component extends HotCellEditorAdvancedComponent and provides interactive star selection using the same SVG and CSS classes as the renderer.
@Component({
standalone: false,
template: `
<div
class="rating-editor"
(mouseover)="onMouseOver($event)"
(mousedown)="onMouseDown()"
>
@for (star of stars; track $index) {
<span
[attr.data-value]="$index + 1"
class="rating-star"
[class.active]="$index < getValue()"
[class.current]="isCurrentStar($index)"
[innerHTML]="starSvgMarkup"
></span>
}
</div>
`,
styleUrls: ["./example1.css"],
})
export class StarEditorComponent extends HotCellEditorAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
isCurrentStar(index: number): boolean {
return (index + 1) === parseInt(this.getValue()?.toString() ?? "0", 10);
}
// ... event handlers and shortcuts
}
What's happening:
- Container -
class="rating-editor"uses the same theme-aware styling as the Star Rating recipe (blue border, padding, background via CSS tokens) - Stars - Same SVG as the renderer (via sanitized
starSvgMarkup);.activefor filled (gold),.currentfor the selected star (accent color) - isCurrentStar(index) - Template expressions can't call global
parseInt, so we use a component method to compare the current value with the star index - getValue() - Method from base class returns current editor value
- Event bindings -
(mouseover)for hover preview,(mousedown)for selection
Step 6: Editor - Mouse Event Handlers
Add mouse interaction for hover preview and click selection. Use closest('.rating-star') so that when the user hovers over the SVG (or its <path>), we still find the parent span with data-value.
export class StarEditorComponent extends HotCellEditorAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = starSvg;
private readonly cdr = inject(ChangeDetectorRef);
onMouseOver(event: MouseEvent): void {
const star = (event.target as HTMLElement).closest(".rating-star") as HTMLElement | null;
if (
star?.dataset["value"] &&
parseInt(this.getValue()?.toString() ?? "0", 10) !== parseInt(star.dataset["value"], 10)
) {
this.setValue(parseInt(star.dataset["value"], 10));
}
this.cdr.detectChanges();
}
onMouseDown(): void {
this.finishEdit.emit();
}
}
What's happening:
onMouseOver (Hover Preview):
- Get the hovered element; with inline SVG, the target may be the
<svg>or<path>, not the span - Use
closest('.rating-star')to find the parent span withdata-value - If the hovered star's value differs from the current value, update it with
setValue() - Call
cdr.detectChanges()so the view updates immediately
onMouseDown (Click Selection):
- User clicks anywhere in the editor
- Emit
finishEditto close the editor and save the value
Why closest()?
- With inline SVGs, the event target is often the inner
<path>or<svg>element closest('.rating-star')walks up the DOM to the span that hasdata-value- Ensures hover and click work regardless of which part of the star is under the cursor
Step 7: Editor - Keyboard Shortcuts
Add keyboard support for rating selection using the shortcuts property from the base class.
export class StarEditorComponent extends HotCellEditorAdvancedComponent<number> {
readonly stars = Array(5);
override shortcuts?: KeyboardShortcutConfig[] = [
{
keys: [["1"], ["2"], ["3"], ["4"], ["5"]],
callback: (editor, _event) => {
editor.setValue(_event.key);
},
},
{
keys: [["ArrowRight"]],
callback: (editor, _event) => {
if (parseInt(editor.value) < 5) {
editor.setValue(parseInt(editor.value) + 1);
}
},
},
{
keys: [["ArrowLeft"]],
callback: (editor, _event) => {
if (parseInt(editor.value) > 1) {
editor.setValue(parseInt(editor.value) - 1);
}
},
},
];
private readonly cdr = inject(ChangeDetectorRef);
// ... rest of the component
}
What's happening:
Shortcuts Property:
override shortcuts- Overrides the base class property to define custom keyboard shortcutsKeyboardShortcutConfig[]- Type-safe configuration for keyboard shortcuts- Handsontable automatically registers and handles these shortcuts
Number Keys (1-5):
- Press 1-5 to set rating directly
- Fastest way to select a specific rating
- Gets key value from keyboard event:
_event.key editor.setValue(_event.key)- Updates the value immediately
Arrow Keys:
- ArrowRight: Increase rating (max 5)
- Check current value:
parseInt(editor.value) < 5 - Increment:
editor.setValue(parseInt(editor.value) + 1)
- Check current value:
- ArrowLeft: Decrease rating (min 1)
- Check current value:
parseInt(editor.value) > 1 - Decrement:
editor.setValue(parseInt(editor.value) - 1)
- Check current value:
- Bounded within valid range
- Smooth incremental adjustment
Keyboard navigation benefits:
- Fast selection without mouse
- Accessible for keyboard-only users
- Number keys for direct selection, arrows for adjustment
- All shortcuts handled by Handsontable's shortcut manager
Step 8: Complete Column Configuration
Now combine the renderer and editor components in your column configuration:
export class AppComponent {
readonly data = [
{ id: 640329, itemName: "Lunar Core", stars: 4 },
{ id: 863104, itemName: "Zero Thrusters", stars: 5 },
{ id: 395603, itemName: "EVA Suits", stars: 3 },
];
readonly gridSettings: GridSettings = {
autoRowSize: true,
rowHeaders: true,
height: "auto",
colHeaders: ["ID", "Item Name", "Stars Rating"],
columns: [
{ data: "id", type: "numeric" },
{ data: "itemName", type: "text" },
{
data: "stars",
width: 200,
renderer: StarRendererComponent,
editor: StarEditorComponent,
},
],
};
}
What's happening:
- data: Array of objects with star ratings (1-5)
- gridSettings: Typed with
GridSettingsfor IntelliSense - columns configuration:
renderer: StarRendererComponent- Angular component for displayeditor: StarEditorComponent- Angular component for editingwidth: 200- Column width for comfortable star display
How it works:
- Handsontable detects that renderer/editor are Angular components
- Creates component instances dynamically
- Passes cell data to components via
@Inputproperties - Listens to component
@Outputevents for editor lifecycle
Step 9: Create the Angular Component
Put it all together in your Angular component:
@Component({
selector: "app-star-rating",
standalone: false,
template: `
<div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>
`,
})
export class AppComponent {
readonly data = [
{ id: 640329, itemName: "Lunar Core", stars: 4 },
{ id: 863104, itemName: "Zero Thrusters", stars: 5 },
{ id: 395603, itemName: "EVA Suits", stars: 3 },
];
readonly gridSettings: GridSettings = {
autoRowSize: true,
rowHeaders: true,
height: "auto",
colHeaders: ["ID", "Item Name", "Stars Rating"],
columns: [
{ data: "id", type: "numeric" },
{ data: "itemName", type: "text" },
{
data: "stars",
width: 200,
renderer: StarRendererComponent,
editor: StarEditorComponent,
},
],
};
}
Step 10: Register in Angular Module
Declare all components in your Angular module:
import { NgModule, ApplicationConfig } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { registerAllModules } from "handsontable/registry";
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from "@handsontable/angular-wrapper";
import { CommonModule } from "@angular/common";
import { NON_COMMERCIAL_LICENSE } from "@handsontable/angular-wrapper";
import { AppComponent, StarEditorComponent, StarRendererComponent } from "./app.component";
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HOT_GLOBAL_CONFIG,
useValue: {
license: NON_COMMERCIAL_LICENSE,
} as HotGlobalConfig,
},
],
};
@NgModule({
imports: [BrowserModule, HotTableModule, CommonModule],
declarations: [AppComponent, StarEditorComponent, StarRendererComponent],
providers: [...appConfig.providers],
bootstrap: [AppComponent],
})
export class AppModule {}
Important steps:
- Import HotTableModule - Provides
<hot-table>directive - Declare all components - Main component, renderer, and editor
- Register Handsontable modules - Call
registerAllModules()before module creation - Configure global settings - Use
HOT_GLOBAL_CONFIGprovider for theme and license
Enhancements
1. Show Numeric Value
Display the numeric rating alongside stars:
@Component({
selector: "star-renderer-with-number",
template: `
<div style="display: flex; align-items: center; gap: 8px;">
<div class="rating-cell">
@for (star of stars; track $index) {
<span class="rating-star" [class.active]="$index < value" [innerHTML]="starSvgMarkup"></span>
}
</div>
<span style="color: #666; font-size: 14px;">({{ value }}/5)</span>
</div>
`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarRendererWithNumberComponent extends HotCellRendererAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
}
2. Color-Coded Stars
Change star color based on rating value:
@Component({
selector: "star-renderer-colored",
template: `
<div class="rating-cell">
@for (star of stars; track $index) {
<span
class="rating-star"
[class.active]="$index < value"
[style.color]="$index < value ? getColor() : undefined"
[innerHTML]="starSvgMarkup"
></span>
}
</div>
`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarRendererColoredComponent extends HotCellRendererAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
getColor(): string {
if (this.value >= 4) return "#ffd700"; // Gold
if (this.value === 3) return "#ffa500"; // Orange
return "#ff6347"; // Red
}
}
3. Half-Star Ratings
Support half-star ratings (0.5 increments):
@Component({
selector: "star-renderer-half",
template: `
<div class="rating-cell">
@for (star of stars; track $index) {
<span>
@if ($index < fullStars) {
<span class="rating-star active" [innerHTML]="starSvgMarkup"></span>
} @if ($index === fullStars && hasHalf) {
<span class="rating-star" style="opacity: 0.7" [innerHTML]="starSvgMarkup"></span>
} @if ($index >= fullStars && !($index === fullStars && hasHalf)) {
<span class="rating-star" [innerHTML]="starSvgMarkup"></span>
}
</span>
}
</div>
`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarRendererHalfComponent extends HotCellRendererAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
get fullStars(): number {
return Math.floor(this.value);
}
get hasHalf(): boolean {
return this.value % 1 !== 0;
}
}
// Update validator in column configuration
columns: [
{
data: "stars",
renderer: StarRendererHalfComponent,
validator: (value: number) => {
const rating = parseFloat(value?.toString() || "0");
return rating >= 0.5 && rating <= 5 && rating % 0.5 === 0;
},
},
];
4. Custom Star Count
Configurable number of stars per column using rendererProps:
@Component({
selector: "star-renderer-custom",
template: `
<div class="rating-cell">
@for (star of getStarsArray(); track $index) {
<span class="rating-star" [class.active]="$index < value" [innerHTML]="starSvgMarkup"></span>
}
</div>
`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarRendererCustomComponent extends HotCellRendererAdvancedComponent<number, { maxStars?: number }> {
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
getStarsArray(): unknown[] {
const maxStars = this.getProps().maxStars ?? 5;
return Array(maxStars);
}
}
// Usage in column configuration
columns: [
{
data: "stars",
renderer: StarRendererCustomComponent,
// Pass custom properties via cellProperties
rendererProps: { maxStars: 10 },
},
];
5. Text Labels
Add text labels like "Excellent", "Good", etc.:
@Component({
selector: "star-renderer-labels",
template: `
<div style="display: flex; flex-direction: column; gap: 4px;">
<div class="rating-cell">
@for (star of stars; track $index) {
<span class="rating-star" [class.active]="$index < value" [innerHTML]="starSvgMarkup"></span>
}
</div>
<div style="font-size: 12px; color: #666;">
{{ getLabel() }}
</div>
</div>
`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarRendererLabelsComponent extends HotCellRendererAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
readonly labels = ["", "Poor", "Fair", "Good", "Very Good", "Excellent"];
getLabel(): string {
return this.labels[this.value] ?? "";
}
}
Accessibility
Add ARIA attributes for screen readers:
@Component({
selector: "star-editor-accessible",
template: `
<div
class="rating-editor"
role="radiogroup"
aria-label="Star rating from 1 to 5"
(mouseover)="onMouseOver($event)"
(mousedown)="onMouseDown()"
>
@for (star of stars; track $index) {
<span
[attr.data-value]="$index + 1"
class="rating-star"
[class.active]="$index < getValue()"
[class.current]="isCurrentStar($index)"
[innerHTML]="starSvgMarkup"
role="radio"
[attr.aria-checked]="$index < getValue()"
[attr.aria-label]="$index + 1 + ' star' + ($index > 0 ? 's' : '')"
tabindex="0"
></span>
}
</div>
`,
styleUrls: ["./example1.css"],
standalone: false,
})
export class StarEditorAccessibleComponent extends HotCellEditorAdvancedComponent<number> {
readonly stars = Array(5);
readonly starSvgMarkup = inject(DomSanitizer).bypassSecurityTrustHtml(starSvg);
isCurrentStar(index: number): boolean {
return (index + 1) === parseInt(this.getValue()?.toString() ?? "0", 10);
}
private readonly cdr = inject(ChangeDetectorRef);
// ... rest of implementation (onMouseOver with closest('.rating-star'), onMouseDown)
}
Keyboard navigation:
- Number keys (1-5): Direct rating selection
- Arrow Right: Increase rating (max 5)
- Arrow Left: Decrease rating (min 1)
- Enter: Confirm selection and finish editing
- Escape: Cancel editing
- Tab: Navigate to editor
ARIA attributes:
role="radiogroup": Identifies the star grouprole="radio": Identifies each star as a radio optionaria-label: Describes each star (e.g., "1 star", "2 stars")aria-checked: Indicates selected starstabindex: Controls keyboard focus order
Congratulations! You've created a theme-aware SVG star rating editor with hover preview and keyboard support using Angular components, perfect for intuitive 1-5 star ratings in your data grid!