Building a Custom Angular Rating Input Component
This challenge focuses on creating a reusable custom form control in Angular. You will build a star rating component that can be integrated seamlessly into Angular Reactive Forms, allowing users to select a rating out of a specified number of stars. This is a common requirement for user feedback and data collection in web applications.
Problem Description
Your task is to create a custom Angular component that functions as a star rating input. This component should:
- Visually represent a rating: Display a series of stars, where some are filled (selected) and others are empty (unselected).
- Allow user interaction: Users should be able to click on stars to set the rating.
- Integrate with Angular Forms: The component must implement the
ControlValueAccessorinterface, enabling it to be used with Angular's Reactive Forms (e.g.,FormControl,FormGroup). - Be configurable: Allow the total number of stars and the initial rating to be set.
- Handle disabled state: The component should visually indicate when it's disabled and prevent user interaction.
Key Requirements:
ControlValueAccessorImplementation: ImplementwriteValue,registerOnChange, andregisterOnTouchedmethods.- Visual Star Representation: Use a visual cue (e.g., font awesome stars, SVGs, or simple characters) to represent filled and empty stars.
- Click Handling: When a user clicks on a star, the component should update its internal rating value and notify the Angular form.
- Configurable Total Stars: The component should accept an input (
@Input()) to define the maximum number of stars available for rating. - Configurable Initial Rating: The component should accept an input (
@Input()) to set the initial value of the rating. - Disabled State: The component should accept an input (
@Input()) to control its disabled state and visually reflect this. When disabled, clicks should have no effect. - Styling: Basic styling should be applied to make the stars visually distinct and to indicate the selected state.
Expected Behavior:
- When the component is rendered, it should display the correct number of empty and filled stars based on its initial value.
- Clicking on a star should fill all stars up to and including that star, and update the form control's value.
- Hovering over stars (when enabled) should visually indicate which stars would be filled if clicked, without permanently changing the selection.
- When disabled, the stars should appear visually different (e.g., grayed out) and clicking them should do nothing.
Edge Cases to Consider:
- What happens if the initial rating is greater than the total number of stars?
- What happens if the total number of stars is zero or negative?
- How does the component handle rapid clicks?
Examples
Example 1: Basic Usage with Reactive Forms
Consider a form with a rating form control.
Component Template (app.component.html):
<form [formGroup]="myForm">
<app-star-rating formControlName="rating"></app-star-rating>
</form>
Component Logic (app.component.ts):
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
myForm: FormGroup;
constructor(private fb: FormBuilder) {
this.myForm = this.fb.group({
rating: [3] // Initial rating of 3 stars
});
}
}
Custom star-rating.component.ts (Conceptual):
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-star-rating',
template: `
<div class="star-rating-container">
<span
*ngFor="let star of stars; let i = index"
class="star"
[class.filled]="i < currentValue"
(click)="onClick(i)"
(mouseenter)="onMouseEnter(i)"
(mouseleave)="onMouseLeave()"
[class.disabled]="disabled"
>
★ <!-- Star character -->
</span>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true
}
]
})
export class StarRatingComponent implements ControlValueAccessor {
@Input() maxStars: number = 5;
@Input() disabled: boolean = false;
currentValue: number = 0; // Current selected rating
hoverValue: number = 0; // Value on hover
stars: number[] = [];
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
ngOnInit() {
this.stars = Array(this.maxStars).fill(0).map((_, i) => i);
}
writeValue(value: number): void {
this.currentValue = value;
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
onClick(index: number): void {
if (!this.disabled) {
this.currentValue = index + 1;
this.onChange(this.currentValue);
}
}
onMouseEnter(index: number): void {
if (!this.disabled) {
this.hoverValue = index + 1;
}
}
onMouseLeave(): void {
if (!this.disabled) {
this.hoverValue = 0; // Reset hover when mouse leaves
}
}
}
Explanation:
- The
app.component.tssets up a Reactive Form with aratingcontrol initialized to 3. - The
app-star-ratingcomponent is used withformControlName="rating". - The
StarRatingComponentimplementsControlValueAccessorto interact with the form. - The template displays stars. The
filledclass is applied based oncurrentValueandhoverValue. - Clicking a star updates
currentValueand callsonChange.
Example 2: Component with Custom Number of Stars and Initial Value
Component Template:
<form [formGroup]="myForm">
<app-star-rating
formControlName="productRating"
[maxStars]="10"
[ngModel]="4"
></app-star-rating>
</form>
Component Logic:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
myForm: FormGroup;
constructor(private fb: FormBuilder) {
this.myForm = this.fb.group({
productRating: [4] // Initial rating of 4 stars
});
}
}
(Assume app-star-rating is configured to handle [maxStars] and [ngModel] which will be mapped to currentValue via writeValue)
Explanation:
- This example demonstrates using the
maxStarsinput to create a 10-star rating system. - The
[ngModel]binding (or directly setting the form control value) sets the initial rating. Note thatngModelis typically used withFormsModule, while Reactive Forms useformControlNameorformControl. For Reactive Forms, setting the initial value infb.groupis the standard approach.
Example 3: Disabled State
Component Template:
<form [formGroup]="myForm">
<app-star-rating formControlName="disabledRating" [disabled]="true"></app-star-rating>
</form>
Component Logic:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
myForm: FormGroup;
constructor(private fb: FormBuilder) {
this.myForm = this.fb.group({
disabledRating: [2] // Initial rating of 2 stars
});
}
}
Explanation:
- The
[disabled]="true"input makes theapp-star-ratingcomponent visually appear disabled. - User clicks on the stars will be ignored, and the
disabledRatingform control's value will remain unchanged by user interaction.
Constraints
- The
maxStarsinput should be a positive integer. If invalid input is provided, the component should gracefully default to 5 stars. - The initial
currentValueshould be a non-negative integer. If it's greater thanmaxStars, it should be capped atmaxStars. If it's negative, it should be treated as 0. - The component should be performant enough for a typical web application, even with many stars (e.g., 20+).
- The solution must be implemented entirely in TypeScript.
- Angular CLI must be used for project setup and component generation.
Notes
- Consider using CSS for styling the stars, including hover effects and the filled/empty states.
- You might want to explore using SVGs or icon fonts (like Font Awesome) for more professional-looking stars. For this challenge, a simple character (
★) is sufficient. - Remember to import
FormsModuleandReactiveFormsModulein yourapp.module.tsif you are using them in your application. - Think about how to manage the state for both the
currentValue(the actual selected rating) and thehoverValue(for visual feedback during mouseover). - The
ControlValueAccessorinterface is key here. Ensure you understand its purpose and how each method contributes to bridging your custom component with Angular forms.