Skip to content
TypeScript
/* file: app.component.ts */
import { Component, ChangeDetectorRef, ChangeDetectionStrategy, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
GridSettings,
HotCellEditorAdvancedComponent,
KeyboardShortcutConfig,
HotCellRendererAdvancedComponent,
} from '@handsontable/angular-wrapper';
export 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>';
export const inputData = [
{
id: 640329,
itemName: 'Lunar Core',
},
{
id: 863104,
itemName: 'Zero Thrusters',
},
{
id: 395603,
itemName: 'EVA Suits',
},
{
id: 679083,
itemName: 'Solar Panels',
},
{
id: 912663,
itemName: 'Comm Array',
},
{
id: 315806,
itemName: 'Habitat Dome',
},
{
id: 954632,
itemName: 'Oxygen Unit',
},
];
@Component({
selector: 'example1-guide-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);
}
@Component({
selector: 'example1-guide-star-editor',
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);
}
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);
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();
}
}
@Component({
selector: 'example1-guide-star-angular',
standalone: false,
template: ` <div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>`,
})
export class Example1GuideStarAngularComponent {
readonly data = inputData.map((el) => ({
...el,
stars: Math.floor(Math.random() * 5) + 1,
}));
readonly gridSettings: GridSettings = {
autoRowSize: true,
rowHeaders: true,
autoWrapRow: true,
height: 'auto',
manualColumnResize: true,
manualRowResize: true,
colHeaders: ['ID', 'Item Name', 'Stars Rating'],
columns: [
{ data: 'id', type: 'numeric' },
{
data: 'itemName',
type: 'text',
},
{
data: 'stars',
width: 200,
renderer: StarRendererComponent,
editor: StarEditorComponent,
},
],
};
}
/* end-file */
/* file: app.module.ts */
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';
/* start:skip-in-compilation */
import { Example1GuideStarAngularComponent, StarEditorComponent, StarRendererComponent } from './app.component';
/* end:skip-in-compilation */
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HOT_GLOBAL_CONFIG,
useValue: {
license: NON_COMMERCIAL_LICENSE,
} as HotGlobalConfig,
},
],
};
@NgModule({
imports: [BrowserModule, HotTableModule, CommonModule],
declarations: [Example1GuideStarAngularComponent, StarEditorComponent, StarRendererComponent],
providers: [...appConfig.providers],
bootstrap: [Example1GuideStarAngularComponent],
})
export class AppModule {}
/* end-file */
HTML
<div>
<example1-guide-star-angular></example1-guide-star-angular>
</div>
CSS
.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);
}

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
  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 renderers
    • HotCellEditorAdvancedComponent - Base class for custom editors with advanced features
    • KeyboardShortcutConfig - Type for keyboard shortcuts configuration
    • GridSettings - Type for Handsontable configuration
    • DomSanitizer - 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
  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 value
    • value property - 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 with fill="currentColor" so CSS controls the star color
    • inject(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
  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
  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 true for valid, false for 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
  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); .active for filled (gold), .current for 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
  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):

    1. Get the hovered element; with inline SVG, the target may be the <svg> or <path>, not the span
    2. Use closest('.rating-star') to find the parent span with data-value
    3. If the hovered star’s value differs from the current value, update it with setValue()
    4. Call cdr.detectChanges() so the view updates immediately

    onMouseDown (Click Selection):

    1. User clicks anywhere in the editor
    2. Emit finishEdit to 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 has data-value
    • Ensures hover and click work regardless of which part of the star is under the cursor
  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 shortcuts
    • KeyboardShortcutConfig[] - 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)
    • ArrowLeft: Decrease rating (min 1)
      • Check current value: parseInt(editor.value) > 1
      • Decrement: editor.setValue(parseInt(editor.value) - 1)
    • 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
  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 GridSettings for IntelliSense
    • columns configuration:
      • renderer: StarRendererComponent - Angular component for display
      • editor: StarEditorComponent - Angular component for editing
      • width: 200 - Column width for comfortable star display

    How it works:

    1. Handsontable detects that renderer/editor are Angular components
    2. Creates component instances dynamically
    3. Passes cell data to components via @Input properties
    4. Listens to component @Output events for editor lifecycle
  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,
    },
    ],
    };
    }
  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:

    1. Import HotTableModule - Provides <hot-table> directive
    2. Declare all components - Main component, renderer, and editor
    3. Register Handsontable modules - Call registerAllModules() before module creation
    4. Configure global settings - Use HOT_GLOBAL_CONFIG provider 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 group
  • role="radio": Identifies each star as a radio option
  • aria-label: Describes each star (e.g., “1 star”, “2 stars”)
  • aria-checked: Indicates selected stars
  • tabindex: 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!