Reader Stacks

Min/Max Length Validation and Custom Form Controls in Angular

ControlValueAccessor is the interface that lets a completely custom component plug into formControlName exactly like a native input — without it, Angular has no way to read or write the component's value.

Min/Max Length Validation and Custom Form Controls in Angular

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.