Big thanks to @Eliseo but that solution was not working for me on my existing code (different way of binding, Angular 8?) and I was getting even more frustrated - ngControl.control
was always undefined..
The solution does not require a custom ErrorStateMatcher
apparently, but the answer is to ensure the mat-select
is bound to the FormControl
in the FormGroup
which is fiddly due to life-cycle events, but effectively:
export class DatasetSelectComponent extends AbstractFormFieldComponent {
@Input() label!: string;
@Input() items!: [{id: number, label: string}];
}
export abstract class AbstractFormFieldComponent implements ControlValueAccessor {
// tslint:disable-next-line:variable-name
_formControl = new FormControl();
onChange = (value: any) => {};
constructor(@Self() @Optional() public ngControl: NgControl) {
if(this.ngControl) {
this.ngControl.valueAccessor = this;
}
}
ngAfterViewInit(): void {
if (this.ngControl) {
/**
* get a handle on the FormControl that was created in the last Reactive FormGroup in the component injection hierarchy
* so that it can be bound to the input in our Custom Component
* this ensures input value binding to model + explicit validation is bound
* e.g. new FormGroup({ titleId: new FormControl(personalDetails.titleId, Validators.required) } =>
* <input [formControl]="this.formControl"
* otherwise you will have to do that manually for evey single control on every single form
* which is obviously a lot of repeating yourself
*/
of(this.ngControl.control)
.pipe(
skipWhile(fc => !fc),
take(1)
)
.subscribe(fc => {
this.formControl = fc as FormControl;
console.log(
'Custom FormControl (AbstractFormFieldComponent): Binding to Reactive Form',
this.ngControl,
this.ngControl.control
);
});
}
get formControl() :FormControl|RequiredFormControl {
return this._formControl;
}
set formControl(forControl:FormControl|RequiredFormControl) {
this._formControl = forControl;
}
registerOnChange(fn: (value: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: (value: any) => void): void {}
writeValue(value: any): void {
if(this.formControl) this.formControl.setValue(value, { emitEvent: false });
}
}
Note the removal of the component injection of NG_VALUE_ACCESSOR
(replaced by the workings in the constructor), which prevents a cyclical dependency compile-time error:
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomSelectComponent),
}
]
And a snippet from the template:
<mat-select [formControl]="formControl" [required]="formControl.required">
<mat-option *ngFor="let item of items" [value]="item.id">
{{ item.label }}
</mat-option>
</mat-select>
Updated blitz
