When using two-way binding in Angular, it seems that the setter of the child component is called twice.
Here is a playground that demonstrates the issue. If the "Toggle from component" button is clicked, the isShown
setter of toggler.component.ts
is called twice. I reproduced the interesting code below:
@Component({changeDetection: ChangeDetectionStrategy.OnPush})
export class AppComponent implements DoCheck {
public isShown = true;
public onToggle() {
this.isShown = !this.isShown;
}
}
@Component({changeDetection: ChangeDetectionStrategy.OnPush})
export class TogglerComponent {
public get isShown(): boolean {
return this._isShown;
}
@Input()
public set isShown(isShown: boolean) {
console.log('Entering setter component');
this._isShown = isShown;
this.isShownChange.emit(isShown);
}
@Output()
public isShownChange: EventEmitter<boolean> = new EventEmitter();
private _isShown: boolean = true;
public onToggle() {
this.isShown = !this.isShown;
}
}
How can I prevent the setter from being called twice? This behavior is problematic when the parent component initializes the bound variable asynchronously.
See this if you don't mind using rxjs and streams instead of getters/setters
import { Component, ChangeDetectionStrategy, DoCheck } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements DoCheck {
private readonly showSubject = new BehaviorSubject<boolean>(false);
public readonly show$ = this.showSubject.asObservable().pipe(shareReplay());
public toggle() {
this.showSubject.next(!this.showSubject.value);
console.log(
`%c Outside: isShown changed to: ${this.showSubject.value}`,
'color: blue;'
);
}
public ngDoCheck() {
console.log(
`%c Outside: DoCheck: isShown: ${this.showSubject.value}`,
'color: blue;'
);
}
}
<div>Outer isShown value: {{ show$ | async }}</div>
<button (click)="toggle()">Toggle from outside</button>
<br />
<br />
<toggler [show]="show$ | async" (showChange)="toggle()"></toggler>
import {
Component,
ChangeDetectionStrategy,
OnChanges,
SimpleChanges,
Input,
Output,
EventEmitter,
DoCheck,
} from '@angular/core';
@Component({
selector: 'toggler',
template: `
<div>
<div>show: {{ show }}</div>
<button (click)="toggle()">Toggle from component</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host {
display: block;
border: 1px dashed black;
padding: 1em;
color: green;
}
`,
],
})
export class TogglerComponent implements OnChanges, DoCheck {
@Input() show?: boolean;
@Output() showChange: EventEmitter<void> = new EventEmitter();
public ngOnChanges(changes: SimpleChanges) {
if (changes.show) {
console.log(
`%c OnChanges: isShown changed to: ${this.show}`,
'color: green;'
);
}
}
public ngDoCheck() {
console.log(`%c DoCheck: isShown: ${this.show}`, 'color: green;');
}
public toggle() {
this.showChange.emit();
}
}