Reputation: 1559
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.
Upvotes: 1
Views: 847
Reputation: 241
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();
}
}
Upvotes: 1