Reputation: 7682
I have an InputComponent
which works as a wrapper around several native inputs and adds some logic.
This component has an input control
which accepts FormControl
and binds it to the input field via [formControl]
.
To improve the performance I've set changeDetectionStrategy
to OnPush.
Now I've noticed that when I use formGroup.get(...).setValidators(...)
the InputComponent doesn't update its state. How can I perform this? I don't see any hook into validators change cycle.
As a workaround I've added a public API to manually invoke detectChanges()
.
Is there an Observable that I can listen to and call detectChanges()
when there is changes to validation rules and not to cause call stack size exceed.
This can be paraphrased as How can I invoke a change detection of the whole tree when OnPush detection strategy is being used
Small example here you can see one checkbox. Click the button and see that there is no difference. However, If you click on checkbox it will show an asterisk.
What's also interesting is that when requiredTrue
is assigned to control and the control doesn't have true
as a value the form is still valid. I can't understand why.
Upvotes: 2
Views: 3476
Reputation: 1659
The actual issue here is that setting the validators outside breaks the concept of immutability. The input you have is an object where only a property inside that object is changed. Angular doesn't register this as a change.
So interestingly this is not just a problem with ChangeDetction.OnPush. Using the default change detection, angular does update the view because all components get checked, but if you would register to the onChanges lifecycle hook, you would notice that the actual change is not registered inside the lifecycle hook in either change detection configuration. So be careful it only seems to work with the default change detection, but isn't working completely. Therefore be careful to keep object mutability in mind independent on the change detection you are using.
Having said that: In your case, following SoC, I would actually move setting the validator inside the input component, dependent on a boolean input property instead of setting the validator from outside. Next to SoC this would also have the advantage that in case there are other validators your input component might need, you actually have all those in one spot and could reset those once you only want to remove the required validator since it as far as I know there is no possibility of removing a specific validator (yet).
Your input.component.ts
would look something like this (be aware this is only pseudo code):
input.component.ts
export class InputComponent implements OnChanges {
@Input() required: boolean;
@Input() control: FormControl;
ngOnChanges(simpleChanges: SimpleChanges){
if(simpleChanges.required) {
if(simpleChanges.required.nextValue) {
this.control.addValidator(...)
} else {
this.control.clearValidators()
}
}
}
}
In case setting the validator inside your input is not at all an option for whatever reason, you would actually need to trigger the changeDetection manually. For this I would do the following:
app.component.ts
constructor(private fb: FormBuilder, private ref: ChangeDetectorRef) {}
toggleValidator() {
if (this.requiredTrue) {
this.requiredTrue = false;
this.formGroup.get('agreed').clearValidators();
// this is important
this.formGroup.get('agreed').updateValueAndValidity();
} else {
this.requiredTrue = true;
this.formGroup.get('agreed').setValidators(Validators.requiredTrue)
// this is important
this.formGroup.get('agreed').updateValueAndValidity();
}
console.log(this.formGroup);
}
input.component.ts
constructor(private ref: ChangeDetectorRef) {
}
ngOnInit() {
this.control.valueChanges.subscribe(() => {
this.ref.markForCheck();
});
}
Have a look at this stackblitz
Upvotes: 2