Hone logo
Hone
Problems

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 ControlValueAccessor interface, 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:

  1. ControlValueAccessor Implementation: Implement writeValue, registerOnChange, and registerOnTouched methods.
  2. Visual Star Representation: Use a visual cue (e.g., font awesome stars, SVGs, or simple characters) to represent filled and empty stars.
  3. Click Handling: When a user clicks on a star, the component should update its internal rating value and notify the Angular form.
  4. Configurable Total Stars: The component should accept an input (@Input()) to define the maximum number of stars available for rating.
  5. Configurable Initial Rating: The component should accept an input (@Input()) to set the initial value of the rating.
  6. 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.
  7. 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"
      >
        &#9733; <!-- 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.ts sets up a Reactive Form with a rating control initialized to 3.
  • The app-star-rating component is used with formControlName="rating".
  • The StarRatingComponent implements ControlValueAccessor to interact with the form.
  • The template displays stars. The filled class is applied based on currentValue and hoverValue.
  • Clicking a star updates currentValue and calls onChange.

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 maxStars input to create a 10-star rating system.
  • The [ngModel] binding (or directly setting the form control value) sets the initial rating. Note that ngModel is typically used with FormsModule, while Reactive Forms use formControlName or formControl. For Reactive Forms, setting the initial value in fb.group is 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 the app-star-rating component visually appear disabled.
  • User clicks on the stars will be ignored, and the disabledRating form control's value will remain unchanged by user interaction.

Constraints

  • The maxStars input should be a positive integer. If invalid input is provided, the component should gracefully default to 5 stars.
  • The initial currentValue should be a non-negative integer. If it's greater than maxStars, it should be capped at maxStars. 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 (&#9733;) is sufficient.
  • Remember to import FormsModule and ReactiveFormsModule in your app.module.ts if you are using them in your application.
  • Think about how to manage the state for both the currentValue (the actual selected rating) and the hoverValue (for visual feedback during mouseover).
  • The ControlValueAccessor interface is key here. Ensure you understand its purpose and how each method contributes to bridging your custom component with Angular forms.
Loading editor...
typescript