Rating
A star rating as a form editor — Kendo's Rating, PrimeNG's Rating: a value between 0 (not rated, committed as null) and max in steps of precision, so half stars and averages such as 4.3 render exactly. It is one APG role="slider" by default — arrows follow the reading direction, so RTL mirrors them — or a radio group of whole items with semantics="radiogroup".
Works standalone via [(value)], with Signal Forms via [formField], and with reactive/template forms via formControl/ngModel.
Getting started
Arrows step by one item (RTL-mirrored), PageUp/PageDown too, Home/End to the ends, the digit keys jump and Delete, Backspace or 0 clear. A press on the current value clears it while allowClear is on; the pointer previews the value it would pick.
Value: 3
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeRating } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeRating],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- An APG slider: arrows step (RTL-mirrored), PageUp/PageDown by one
item, Home/End, digits jump, Delete/Backspace/0 clear. A press on the
current value clears it (allowClear); the pointer previews the pick. -->
<oge-rating label="Your rating" [(value)]="stars" />
<p>Value: <code>{{ stars() ?? 'null' }}</code></p>
`,
})
export class Demo {
protected readonly stars = signal<number | null>(3);
}Half and fractional values
precision sets the step: 0.5 for half stars — a press in an item's first half picks the half — or 0.1 to show an average exactly. readonly keeps the rating focusable and announced (aria-readonly) but unchangeable.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeRating } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeRating],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- precision 0.5 = half stars: a press in an item's first half
picks the half (mirrored in RTL). Fractional display values (an
average) render exactly with readonly — still focusable, aria-readonly. -->
<oge-rating label="Half stars" [precision]="0.5" [(value)]="half" />
<oge-rating
label="Average rating"
[value]="4.3"
[precision]="0.1"
[readonly]="true"
/>
<oge-rating label="Out of ten" [max]="10" size="sm" [(value)]="ten" />
`,
})
export class Demo {
protected readonly half = signal<number | null>(2.5);
protected readonly ten = signal<number | null>(7);
}Icons and templates
Pick a built-in glyph — 'star' | 'heart' | 'circle' — or render any markup with [ogeRatingItemTemplate]. The template runs once for the empty and once for the filled layer, so fractional fills still work. selection="single" paints only the item holding the value.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeRating, OgeRatingItemTemplate } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeRating, OgeRatingItemTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Built-in glyphs: 'star' | 'heart' | 'circle'. The item template
renders any markup — once for the empty layer, once for the filled one,
so fractional fills still work. selection="single" paints only the
item that holds the value (a pick-one scale). -->
<oge-rating label="Love it" icon="heart" size="lg" [(value)]="love" />
<oge-rating label="Effort" selection="single" [allowClear]="false" [(value)]="effort">
<ng-template ogeRatingItemTemplate let-item>
<span class="text-sm font-bold">{{ item.itemValue }}</span>
</ng-template>
</oge-rating>
`,
})
export class Demo {
protected readonly love = signal<number | null>(4);
protected readonly effort = signal<number | null>(3);
}Radio group semantics
semantics="radiogroup" renders one radio per item with a single roving tab stop: arrows move and select (wrapping at the ends), Space selects. It needs whole-item precision — fractional precisions fall back to the slider.
Value: 4
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeRating } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeRating],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- semantics="radiogroup": one APG radio per item with a roving
tab stop — arrows move and select (wrapping), Space selects. Needs
whole-item precision; fractional precisions fall back to the slider. -->
<oge-rating label="Service" semantics="radiogroup" [(value)]="service" />
`,
})
export class Demo {
protected readonly service = signal<number | null>(4);
}Inside a form
A FormValueControl and a ControlValueAccessor: formControl, ngModel and [formField] all bind it. Not rated is null, so Validators.required means "rate it".
Valid: false
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeRating } from '@oge-ui/inputs';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'demo-root',
imports: [OgeRating, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- FormValueControl + ControlValueAccessor: formControl, ngModel and
[formField] all bind it; null (not rated) fails Validators.required. -->
<oge-rating label="Overall" [formControl]="overall" />
<p>Valid: <code>{{ overall.valid }}</code></p>
`,
})
export class Demo {
protected readonly overall = new FormControl<number | null>(null, [
Validators.required,
]);
}