2023 answer
Inject the parent component's form control into the custom control, then call hasValidator(...)
on the parent control like this:
// your custom input component...
public get isRequired(): boolean {
return (
!!this.parent &&
!!this.parent.control &&
this.parent.control.hasValidator(Validators.required)
);
}
constructor(@Self() @Optional() private parent: NgControl) {
this.parent.valueAccessor = this;
}
// ...
Full example
This example shows how to show an asterisk when the required validator is present.
SignupPage template:
<form [formGroup]="fg">
<app-text-input
[label]="'signup.name' | translate"
formControlName="name"
></app-text-input>
<app-text-input
[label]="'signup.email' | translate"
formControlName="email"
></app-text-input>
</form>
SignupPage component:
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-signup',
templateUrl: './signup.page.html',
styleUrls: ['./signup.page.scss'],
})
export class SignupPage {
public readonly fg = new FormGroup({
name: new FormControl<string>('', [Validators.required]),
email: new FormControl<string>('', [Validators.required, Validators.email]),
});
}
TextInput template:
<input [formControl]="control" (blur)="onBlur()" />
<label *ngIf="label">
{{ label }}
<ng-container *ngIf="isRequired">*</ng-container>
</label>
TextInput component:
import {
Component,
HostBinding,
Input,
OnDestroy,
OnInit,
Optional,
Self,
} from '@angular/core';
import {
ControlValueAccessor,
FormControl,
NgControl,
Validators,
} from '@angular/forms';
import { Subject, takeUntil, tap } from 'rxjs';
@Component({
selector: 'app-text-input',
templateUrl: './text-input.component.html',
styleUrls: ['./text-input.component.scss'],
})
export class TextInputComponent
implements OnInit, OnDestroy, ControlValueAccessor
{
@Input()
public label?: string;
public get isRequired(): boolean {
return (
!!this.parent &&
!!this.parent.control &&
this.parent.control.hasValidator(Validators.required)
);
}
public readonly control = new FormControl<string>('');
private onTouched?: () => void;
private onChanges?: (v: string | null) => void;
private readonly onDestroy$ = new Subject<void>();
constructor(@Self() @Optional() private parent: NgControl) {
this.parent.valueAccessor = this;
}
public ngOnInit(): void {
this.control.valueChanges
.pipe(
tap((e) => {
if (this.onChanges) {
this.onChanges(e);
}
}),
takeUntil(this.onDestroy$)
)
.subscribe();
}
public ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();
}
public registerOnChange(fn: any): void {
this.onChanges = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
public writeValue(value: string | null): void {
this.control.setValue(value, { emitEvent: false });
}
public setDisabledState(value: boolean): void {
if (value) {
this.control.disable();
} else {
this.control.enable();
}
}
public onBlur(): void {
if (this.onTouched) {
this.onTouched();
}
}
}