简体   繁体   中英

Listen to Observable.timer and emit to parent component

I have a countdown component using Observable.timer which is used in a parent component. When it's used, it takes @Input() seconds: string from the parent component that is used as which number to begin for countdown ex: 60 . Now, I want to emit the countdown number @Output() checkTime to the parent component to disable a button once it reaches 0 .

So I subscribed the countdown number to checkTime event emitter. But as a result, countdown doesn't stop it at 0 and it keeps decrementing. Can anyone see the problem below?

register.component.html

<tr *ngFor="let course of courses">            
    <td>
        <button class="btn btn-info" [disabled]="counter === 0"> Register</button>              
    </td>  
    <td>
        <countdown [seconds]="60" (checkTime)="checkTimeExpired($event);"></countdown>
    </td>         
</tr>   

countdown.component.ts

import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core'
import { Observable, Subscription } from 'rxjs/Rx';

@Component({
  selector: 'countdown',
  template: '{{ countDown | async | formatTime }}'
})
export class CountdownComponent implements OnInit {
  @Input() seconds: string;
  @Output() checkTime: EventEmitter<number> = new EventEmitter();
  countDown: any;
  counter: number;
  private subscription: Subscription;

  constructor() {}

  ngOnInit() {
    this.counter = parseInt(this.seconds, 10);
    this.countDown = Observable.timer(0, 1000)
                    .take(this.counter)
                    .map(() => --this.counter)

    this.subscription = this.countDown.subscribe(t => {
        this.checkTime.emit(this.counter)
    });                   
  }
}

register.component.ts

counter: number;

checkTimeExpired(evt) {
   this.counter = evt;   
}

Countdown stops at 0 if I don't use subscription.

this.subscription = this.countDown.subscribe(t => {
    this.checkTime.emit(this.counter)
}); 

Sorry if there is already a relevant question, but help this newly entered angular user.

The main issue is that you're decoupling the Observable's value with an arbitrary variable. Usually you want an Observable stream to transform a value for you -- that's half of what RxJS is about.

Problem is with .map(() => --this.counter) . The map operator transforms the stream of values and returns the new value. Here, you're transforming the Observable value into --counter ; there's the problem. The --counter will return counter - 1 and decrement counter , but the value is completely decoupled from the Observable stream. What does that mean?

Controlling that value from outside of that Observable makes that Observable hot . It's values can stream to different subscribers, but the values can change due to outside factors. (Remember that an Observable doesn't execute until it's subscribed to)

But...you have only one subscriber, so that's not a problem, right? Actually, the Angular async pipe also subscribes to Observables under the hood. That means your Observable has been subscribed to twice , which means there's two timers counting down, which means there are two streams executing that map function -- which means every second, this.counter is being decremented twice .

I'd recommend combining the Observable's timer value and the actual time value, and then emit your event only as a side effect:

  ngOnInit() {
    const start = parseInt(this.seconds, 10);
    this.countDown = Observable.timer(0, 1000)
                    .take(start + 1) // add one, so zero is included in emissions.
                    .map(i => start - i) // decrement the stream's value and return
                    .do(s => this.checkTime.emit(s)) // do a side effect without affecting value

  }

The async pipe will subscribe to it and start the emissions. Best part is, the async pipe automatically cleans up the subscription when it's destroyed.

Another idea is using takeWhile , which is a more declarative way to say how many emissions should be taken, and completes when the provided expression is false:

  ngOnInit() {
    const start = parseInt(this.seconds, 10);
    this.countdown = Observable.timer(0, 1000)
                .map(i => start - i) // decrement the stream's value and return
                .takeWhile(i => i >= 0)
                .do(s => this.checkTime.emit(s)) // do a side effect without affecting value

  }

Hope that helps clear up some confusion!

You can try to stop the countdown on the subscription, the timer will run on the background.

Stackblitz Rebuild

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM