I have the following (simplified) scenario:
SelectComponent
that offers a <select>
tagSelectComponent
implements NG_VALUE_ACCESSOR
and NG_VALIDATORS
SelectComponent
This works well so far, but I have issues when I want to automatically update the behavior in the inner SelectComponent
based on the following:
I implemented the ngOnChanges
event in order to achieve the above.
Here is a minimal example on Stackblitz that is nearly working: https://stackblitz.com/edit/angular-ohdb71?file=src%2Fapp%2Fselect%2Fselect.component.ts
But then, I have problems with change detection in the main component. As you can see, the main component does not update the user.value
to Walter automatically, even though the inner SelectComponent
has set this value and also announced it to the outer component. Consequently, the main component thinks that the value is not set and marks the form control with an error (required).
Oddly enough, the code works (without ExpressionChangedAfterItHasBeenCheckedError
) whenever I move the code <app-select [formControlName]="'user'" [options]="userOptions"></app-select>
to the top of the file.
How can I fix this problem in the SelectComponent
in a way such that the outer component does not have to implement any more logic?
TLDR: you can find your working example here: https://stackblitz.com/edit/angular-f8fiux
There are several steps to be done to make it work:
Modify writeValue method. writeValue is the input of your custom component, so it shouldn't call onChange method (which is output of the component), otherwise it can lead to incorrect interaction.
writeValue(value: any): void {
this.selectFormControl.setValue(value, { emitEvent: false });
}
Here emitEvent: false
is passed so as to not trigger change event of selectFormControl
.
To understand logic of Value Accessors I recommend this article.
Modify ngOnChange method. It should set inner value of your component (through writeValue) and then emit it for outer consumer (through onChange). For onChange you have to schedule either microtask (Promise.resolve) or macrotask (setTimeout), otherwise the data will be emitted in the same change detection cycle, causing ExpressionChangedAfterItHasBeenCheckedError
ngOnChanges(simpleChanges: SimpleChanges): void {
if (simpleChanges.options) {
if (this.options.length === 1) {
if (this.selectFormControl.value !== this.options[0]) {
this.writeValue(this.options[0]);
setTimeout(() => {
this.onChange(this.options[0]);
});
}
} else {
this.writeValue(null);
}
}
}
It's better not to mix Reactive Forms and Template-driven forms, so I recommend you to get rid of (ngModelChange)="onChange($event)"
in template as it's asynchronous data flow and needs more complicated handling. Instead of that, you can add a simple subscription in constructor (don't forget to unsubscribe, here I omitted it)
constructor(private cdr: ChangeDetectorRef) {
this.selectFormControl.valueChanges.subscribe((value) => {
this.onChange(value);
});
}
You can find detailed information about async and sync flow in Angular docs