Vladimir Naumov
Vladimir Naumov

Reputation: 23

Confused behavior with async pipe (Angular)

I will try use async pipe for show/hide some waiting message.

app.component.html

<ng-template #isWait>
    <h1>Please wait</h1>
</ng-template>

<div *ngIf="wait | async; else isWait">
    <hello name="{{ name }}"></hello>

    <p>
        Start editing to see some magic happen :)
    </p>

</div>

<button (click)="toggle()">Toggle</button>

<!-- Doesn't change -->
<div style="margin:1rem;">
    Doesn't change here
    <span style="color:red;">Value is {{wait | async}}</span>
  (AppComponent)
</div>

app.component.ts

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  name: string = 'Angular 5';
  wait: Observable<boolean>;

  constructor(public waitService: WaitService) {
    this.wait = this.waitService.wait;
  }

  toggle() {
    this.waitService.toggle();
  }
}

wait.service.ts

export class WaitService {
  wait: Observable<boolean>;
  private _wait: boolean = false;
  private _onChange: EventEmitter<boolean> = new EventEmitter();

  constructor() {
    this.wait = Observable.create((obs: any) => {
      obs.next(this._wait);
      this._onChange.subscribe((w: boolean) => {
        if (this._wait !== w) {
          this._wait = w;
          obs.next(this._wait);
        }
      });
    });
  }

  toggle() {
    this._onChange.emit(!this._wait);
  }
}

I have WaitService with wait property and method toggle for switching wait. When I try toggle wait, it works in one case, but doesn't work for each others (as I expected).

So, It confused me. I will try figure out, why it happens.

Example: https://stackblitz.com/edit/angular-fwghfj

If you click on Toggle button nothing happen with wait message, but subscription in Wait2Component will be emitted and and will be write to console output each time when you clicked on toggle.

But after commented line with subscription, show and hide wait message will be work ok, but in other places wait still have no change.

Sure I can change wait to boolean and don't care about this situation with async, but this unexpected behavior for me.

Upvotes: 2

Views: 6601

Answers (3)

mickaelw
mickaelw

Reputation: 1513

app.component.ts

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  name: string = 'Angular 5';
  wait: Observable<boolean>;

  constructor(public waitService: WaitService) {
    this.wait = this.waitService.waitingStatus();
  }

  toggle() {
    this.waitService.toggle();
  }
}

Your service

@Injectable()
export class WaitService {
  private currentWaitingStatus: boolean = false;
  private waitingStatus$: Subject<boolean> = new BehaviorSubject<boolean>(this.currentWaitingStatus)

  waitingStatus(): Observable<boolean> {
     return this.waitingStatus$
  }

  toggle() {
     this.currentWaitingStatus= !this.currentWaitingStatus
     this.waitingStatus$.next(this.currentWaitingStatus)
  }
}

I prefer to have a method which return my property than to put it public.

Because, your component must not know how is the inside of your service. If your service is use by n clients, tomorrow you want to change the name of your property you must change it everywhere

Otherwise, if you abstract this behind a method you change juste at one place in your service

Upvotes: 0

martin
martin

Reputation: 96891

The problem is that for each subscriber the callback for Observable.create is called and that's the place where you modify the state of this._wait. In other words, when you call toggle() the this._onChange.subscribe()'s next handle is called eg. five times or maybe even more. But only the first call notifies its observer because of this._wait !== w.

One simple way to avoid this is to share the same source Observable with the share() operator.

this.wait = Observable.create((obs: any) => {
  // ...
}).share();

But even better solution would be not calling subscribe() inside Observable.create and just using an operator chain with do() to perform any side effects:

this.wait = this._onChange
  .filter((w: boolean) => w !== this._wait)
  .do((w: boolean) => this._wait = w)
  .share();

This should produce the same results.

See you updated demo: https://stackblitz.com/edit/angular-nbgmz7?file=app%2Fservices%2Fwait.service.ts

Upvotes: 1

Pavel Agarkov
Pavel Agarkov

Reputation: 3783

You can write WaitService like this instead:

import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class WaitService
{
    private readonly isWaiting = new BehaviorSubject(false);
    public readonly wait$ = this.isWaiting.asObservable();
    public toggle()
    {
        this.isWaiting.next(!this.isWaiting.value);
    }
}

Upvotes: 3

Related Questions