DurandA
DurandA

Reputation: 1559

Setter is called twice with two-way binding in Angular

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:

Parent component

@Component({changeDetection: ChangeDetectionStrategy.OnPush})
export class AppComponent implements DoCheck {
  public isShown = true;

  public onToggle() {
    this.isShown = !this.isShown;
  }
}

Child component

@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

Answers (1)

rfprod
rfprod

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

Related Questions