Building Custom Form Controls in Angular
Learn how to create reusable and accessible custom form controls using Control Value Accessors.
Custom form controls are essential in Angular applications. From fancy date pickers and dynamic sliders to star rating components, Control Value Accessors (CVAs) enable seamless integration with Angular's forms API.
In this guide, we'll examine Control Value Accessors, their use cases, and demonstrate how to build a reusable Star Rating Component using Angular's standalone component architecture.
What Are Control Value Accessors?
Control Value Accessors (CVAs) are a fundamental part of Angular's forms ecosystem. They enable custom form components to integrate with Angular's form APIs (FormControl, FormGroup, ngModel) — working just like native form elements such as <input>
and <select>
.
Control Value Accessors enable you to:
- Bind values from a form to a custom component
- React to changes in the component's value
- Support Angular's validation and state tracking
When to Use Control Value Accessors
You should implement a Control Value Accessor when you need:
- Custom Form Components: Such as sliders, star ratings, dropdown selectors, etc.
- Form Integration: Your component needs to work with
formControlName
orngModel
- Complex UI Logic: Your component handles asynchronous data or manages multiple values
When Not to Use Control Value Accessors
If your component doesn't need form integration or won't use Angular's forms API, stick with a simple @Input()
and @Output()
approach. Implementing a Control Value Accessor when it's not needed only adds unnecessary complexity to your component.
Benefits of Using formControlName
with CVAs
Using formControlName
with CVAs offers:
- Seamless Integration: Automatic value synchronization, validation, and state tracking
- Clean API: Simply use
formControlName
for direct binding instead of passingFormControl
through@Input()
- Validation Support: Access Angular's built-in and custom validators effortlessly
- Reusable Components: Create standardized interfaces that work consistently across your application
Building a Star Rating Component with CVA (Standalone)
Let's build a star rating component that works seamlessly with Angular's reactive forms using Control Value Accessors. For simplicity, the template will be inline.
Step 1: Create the Star Rating Component
We'll start by creating a standalone component for the star rating. It will:
- Render 5 clickable stars.
- Allow the user to select a rating.
- Work seamlessly with Angular forms using
formControlName
.
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-star-rating',
standalone: true,
imports: [CommonModule],
template: `
<div class="stars" role="radiogroup" aria-labelledby="star-rating-label">
<ng-container *ngFor="let star of stars; let i = index">
<i
class="star"
[attr.aria-checked]="i < value ? 'true' : 'false'"
[class.filled]="i < value"
(click)="onStarClick(i)"
>
★
</i>
</ng-container>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true,
},
],
styleUrls: ['./star-rating.component.scss'],
})
export class StarRatingComponent implements ControlValueAccessor {
value = 0;
stars = Array(5).fill(false);
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
writeValue(value: number): void {
this.value = value || 0;
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
onStarClick(index: number): void {
this.value = index + 1;
this.onChange(this.value);
this.onTouched();
}
}
Step 2: Create the Rating Form
Next, we’ll create a standalone form component to demonstrate how the star rating component works with Angular’s reactive forms.
import { Component } from '@angular/core';
import {
FormBuilder,
FormGroup,
Validators,
ReactiveFormsModule,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
import { StarRatingComponent } from '../star-rating/star-rating.component';
@Component({
selector: 'app-rating-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, StarRatingComponent],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label for="rating" id="star-rating-label">Rate your experience</label>
<app-star-rating formControlName="rating"></app-star-rating>
<!-- Error message handling -->
<div
*ngIf="ratingControl?.touched && ratingControl?.invalid"
class="error-message"
role="alert"
>
{{ errorMessage }}
</div>
<div class="form-buttons">
<button type="submit" [disabled]="form.invalid">Submit</button>
<button type="button" (click)="reset()">Reset</button>
</div>
</form>
`,
styleUrls: ['./rating-form.component.scss'],
})
export class RatingFormComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
rating: [0, [Validators.required, Validators.min(1)]], // Default initial value
});
}
// Get the rating control once to avoid repetition
get ratingControl() {
return this.form.get('rating');
}
// Get the error message based on the validation
get errorMessage(): string {
const control = this.ratingControl;
if (control?.hasError('required')) return 'Please select a rating.';
if (control?.hasError('min')) return 'Rating must be at least 1.';
return '';
}
// Handle form submission
onSubmit(): void {
if (this.form.valid) {
console.log('Form Submitted with Rating:', this.form.value);
}
}
// Handle form reset and trigger error message
reset(): void {
this.form.reset();
this.ratingControl?.markAsTouched();
}
}
Finally, after adding some styling, app looks like this:
Key Takeaways
- Seamless Forms Integration: CVA lets you build custom controls that feel native to Angular's forms ecosystem.
- Simplified Reusability: Using
formControlName
makes custom controls straightforward to implement and maintain. - Encapsulation of Logic: CVAs encapsulate the internal behavior of your custom controls, keeping the API clean for parent components.
Final Thoughts
Control Value Accessors are a powerful feature in Angular, especially when paired with standalone components. They make custom form controls reusable, maintainable, and fully compatible with Angular’s forms API. If your app relies heavily on forms, mastering CVAs is a must for creating a seamless user experience.
Happy coding! 🎉
You can find the whole source code in the Github repo.
Thank you for your the time to read this blog! Feel free to share your thoughts about this topic and drop us an email at hello@prototyp.digital.