Beyond basic required/email validation, min/max length rules and building a genuinely custom form control (like a star rating widget) that integrates fully with Angular's reactive forms are both common, closely related needs.
Min and max length validation
form = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(20)]],
bio: ['', Validators.maxLength(500)],
});
Username must be at least 3 characters.
Note the error key's exact casing: minlength/maxlength, all lowercase, not minLength — a subtle but genuine gotcha, since checking hasError('minLength') with the wrong casing silently never matches, and the error message never displays despite the validation actually failing correctly.
Showing a live character counter
{{ form.get('bio')?.value?.length || 0 }} / 500
Building a custom form control: the ControlValueAccessor interface
@Component({
selector: 'app-star-rating',
template: `...`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true,
}],
})
export class StarRatingComponent implements ControlValueAccessor {
value = 0;
onChange: (value: number) => void = () => {};
onTouched: () => void = () => {};
writeValue(value: number): void {
this.value = value;
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
selectRating(rating: number): void {
this.value = rating;
this.onChange(rating);
this.onTouched();
}
}
ControlValueAccessor is the interface that lets a completely custom component plug into formControlName exactly the way a native <input> does — without implementing it, Angular's reactive forms system has no way to read the component's current value or be notified when it changes.
Using the custom control exactly like a built-in one
form = this.fb.group({
rating: [0, [Validators.required, Validators.min(1)]],
});
Once ControlValueAccessor is properly implemented, the custom <app-star-rating> component works with formControlName and standard validators identically to a native input — the parent form has no idea the underlying implementation is a custom component rather than a plain input element.
Why registerOnChange/registerOnTouched exist as separate methods
Angular's reactive forms system calls registerOnChange() and registerOnTouched() once, early on, to give the custom component two callback functions — the component then calls these callbacks itself whenever its internal value changes or it loses focus, which is precisely the mechanism that keeps the parent FormControl's value and touched/dirty state properly synchronized with the custom component's own internal state.
Handling the disabled state
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
Implementing the optional setDisabledState() method lets the custom control correctly respond to form.get('rating')?.disable() — without it, disabling the underlying form control has no visible effect on the custom component at all, since nothing tells the component it should stop accepting interaction.