2

I have attempted to figure out a simple timer observable for a couple of weeks now with no luck. I originally posted this last week: ngrx and angular 5 on stackoverflow and didn't get anywhere. I tried implementing what was suggested and got a little further with my original solution. At this point I have a timer that is emitting and outputting the countdown but only when a play or pause button is clicked. I am trying to get the countdown to continue emitting values to the display component while the play button is pushed. I have console logged the timer and it emits the values while play is pushed fine but the display component does not. I can't figure this out. I am new to Angular 5 and to ngrx/rxjs.

I have the project code available in a working form on Stackblitz here. I have the project code available in a working form on Stackblitz here.

You can login with user: test password: test

The timer code is in core/services/pomo-timer.ts

The container component is books/containers/selected-book-page.ts

The display component is books/components/book-detail.ts

At the moment it should display 6 seconds and once the play button is pushed it should emit and display each second countdown until the pause button is pushed at which time it should pause until play is clicked again. As I mentioned, when I console.log the values the work just fine. It is only when displayed in the component that they don't.

From UI: log in with test/test. Search for a book. Add To Collection. Click Through to Detail Page. There is a play and pause button. Displayed on the page are three variations of the timer I have tried from solutions found on StackOverflow. The timer starts with 6 seconds and counts down to zero. play is clicked the timer begins. pause is clicked the timer stops until play clicks again. on the display page the emitted values are not counting down. with console open, it does countdown emitted values.

The timer is handled by core/services/pomo-timer.ts

   startTimer() {

    const resumeButton = document.getElementById('resume');
    const pauseButton = document.getElementById('pause');
    const resetButton = document.getElementById('reset');
    const interval$: any = interval(1000).pipe(mapTo(-1));
    const pause$ = fromEvent(pauseButton, 'click').pipe(mapTo(false));
    const resume$ = fromEvent(resumeButton, 'click').pipe(mapTo(true));

   const timer$ = merge(pause$, resume$).pipe(
    startWith(interval$),
     switchMap(val => (val ? interval$ : empty())),
     scan((acc, curr) => (curr ? curr + acc : acc), 
     this.countdownSeconds$),
      takeWhile(v => v >= 0),
      )
     .subscribe(
      val => { this.timeRemaining = val; 
               console.log(this.timeRemaining); 
       },
        val => { this.checkTime.emit(val); },
        () => {
         this.resetTimer();
        });
       }

The display is handled by app/books/components/book-detail.ts

export class BookDetailComponent {

  @Input() simpleObservable: number;
  @Input() seconds: string;
  @Input() timeRemaining: number;
  @Input() timerSubscription: Subscription;
  @Input() book: Book;
  @Input() inCollection: boolean;
  @Output() add = new EventEmitter<Book>();
  @Output() remove = new EventEmitter<Book>();
  @Output() resumeClicked = new EventEmitter();
  @Output() checkTime: EventEmitter<number> = new EventEmitter();

get id() {
 return this.book.id;
}

get title() {
 return this.book.volumeInfo.title;
 }

get subtitle() {
  return this.book.volumeInfo.subtitle;
}

get description() {
 return this.book.volumeInfo.description;
}

get thumbnail() {
 return (
   this.book.volumeInfo.imageLinks &&
   this.book.volumeInfo.imageLinks.smallThumbnail
  );
}

get time() {
  return this.timeRemaining;
 }
resumeCommand(action: any) {
  this.resumeClicked.emit(action);
 }
}

The communication with the timer service is handled by: app/books/containers/selected-book-page.ts

@Component({
  selector: 'bc-selected-book-page',
  changeDetection: ChangeDetectionStrategy.OnPush,
 template: `
   <bc-book-detail
    [book]="book$ | async"
    [inCollection]="isSelectedBookInCollection$ | async"
    [timeRemaining]="this.pomoTimerService.timeRemaining"
    [simpleObservable]="this.simpleObservable | async"
    [seconds]="this.pomoTimerService.timeRemaining"
    (checkTime)="checkCurrentTime($event)"
    (add)="addToCollection($event)"
    (remove)="removeFromCollection($event)"
    (resumeClicked)="resumeClicked($event)"
    (resumeClicked)="resumeClicked($event)"
    (reset)="resumeClicked($event)">
   </bc-book-detail>
  `,
  })
  export class SelectedBookPageComponent implements OnInit {
   book$: Observable<Book>;
   isSelectedBookInCollection$: Observable<boolean>;
   timeRemaining: any;
  private timerSubscription: Subscription;
  timerSource = new Subject<any>();
  simpleObservable;
  countDown: any;
  counter: number;
  seconds: string;
  private subscription: Subscription;
  checkTime;

 constructor(public pomoTimerService: PomoTimerService, private store: 
   Store<fromBooks.State>) {
   this.book$ = store.pipe(select(fromBooks.getSelectedBook));
   this.isSelectedBookInCollection$ = store.pipe(
   select(fromBooks.isSelectedBookInCollection)
  );
 }

ngOnInit(): void {
  this.pomoTimerService.pomoCount$ = 0;
  this.pomoTimerService.pomosCompleted$ = 0;
   this.pomoTimerService.pomoTitle$ = 'Time to Work';
   this.pomoTimerService.initTimer();
 }

addToCollection(book: Book) {
 this.store.dispatch(new collection.AddBook(book));
 }

 removeFromCollection(book: Book) {
  this.store.dispatch(new collection.RemoveBook(book));
  }

resumeClicked(event) {
  console.log(event);
  console.log(event.target);
  console.log(event.srcElement);
  console.log(event.type);
  console.log(event.currentTarget.attributes.name.nodeValue);
  console.log(event.currentTarget.attributes.id.nodeValue);
   if (event.currentTarget.attributes.id.nodeValue === 'resume' && 
    !this.pomoTimerService.timerStarted) {
    this.pomoTimerService.timerStarted = true;
    this.pomoTimerService.startTimer();
    }
   }

checkCurrentTime(event) {
  this.counter = event;
 }
}

The pomo-timer.ts is outputting the timer via this.remainingTime Any assistance you might be able to provide would be greatly appreciated. I have tried all examples that are even remotely related that I have found here on Stackoverflow as well. Thank you very much.

5
  • you need to provide the minimum minimal reproducible example here please.
    – Suraj Rao
    Commented Apr 21, 2018 at 6:42
  • @SurajRao From UI: log in with test/test. Search for a book. Add To Collection. Click Through to Detail Page. There is a play and pause button. Displayed on the page are three variations of the timer I have tried from solutions found on StackOverflow. The timer starts with 6 seconds and counts down to zero. play is clicked the timer begins. pause is clicked the timer stops until play clicks again. on the display page the emitted values are not counting down. with console open, it does countdown emitted values. I will add more details to the original post now. Commented Apr 21, 2018 at 14:21
  • @suraj-rao I have added steps above in previous comment and added more code snippets to original post. Commented Apr 21, 2018 at 14:37
  • I have @SurajRao added steps to recreate above. I have added more details to the original post with code snippets. Commented Apr 21, 2018 at 14:40
  • Seems like it doesnt detect change from a provider. You will perhaps need something like a behaviorSubject on your remainingSeconds and subscribe in container
    – Suraj Rao
    Commented Apr 21, 2018 at 14:54

1 Answer 1

1

I managed to get a working timer service.

There's a lot I would refactor in the code, but here I've presented the minimum triage needed to get it working with your existing app structure.

The basic principles I applied are:

  1. Subscribe with async
    The service produces values over time, so should be subscribed in the component as an observable, preferably with async pipe so that the subscription is automatically cleaned up by Angular.

  2. Buffer the inner observable
    Use the Subject as a buffer between the timer$ and it's consuming component. This is so that the component always sees a valid observable, even before timer$ is initialized.

  3. Access buttons with ViewChild
    Do not access the buttons with document.getElementById() as the document may not be ready when this line is run. Use Angular's @ViewChild instead, and pass the elements in to the service on init.

These are the mods I made. I have cut out unchanged blocks for brevity, hopefully there's enough details for you to make the changes.

PomoTimerService mods

// imports as before, plus
import { tap } from 'rxjs/operators';

@Injectable()
export class PomoTimerService {

  timerSource$ = new Subject<any>(); // added '$' to this property, for clarity
  // other properties same as before

  private buttons; // to receive button references passed in

  // Create a new version of init, which is called once and receives the buttons
  initTimer (buttons) {  
    this.buttons = buttons;
    this.initTimerParameters();
  }

  // Renamed the original initTimer() method to initTimerParamters, 
  // as it is called on true init and also in reset    
  initTimerParameters() {
    // same statements as original initTimer() method
  }

  startTimer() {
    this.timerStarted = true;  // moved from component
    const interval$: any = interval(1000).pipe(mapTo(-1));
    const pause$ = fromEvent(this.buttons.pauseButton.nativeElement, 'click').pipe(mapTo(false));
    const resume$ = fromEvent(this.buttons.resumeButton.nativeElement, 'click').pipe(mapTo(true));

    const timer$ = merge(pause$, resume$).pipe(
      startWith(true),  // previously startWith(interval$), but that looks suspect
      switchMap(val => (val ? interval$ : empty())),
      scan((acc, curr) => (curr ? curr + acc : acc), this.countdownSeconds$),
      takeWhile(v => v >= 0),
      tap(val => console.log('timeRemaining', val)),  // use tap (not subscribe) to monitor on console 
      tap(val => {  // resetting this.timerStarted is a 'side-effect', best done with tap operator rather than finally callback of subscribe
        if (val === 0) {
          this.timerStarted = false;
        }
      }),
    );

    timer$.subscribe(val => this.timerSource$.next(val))  // send values to Subject
  }

  resetTimer() {
    this.initTimerParameters();  // was calling this.initTimer()
  }
}

book-detail.ts - template mods

Consume the timer values via the service's Subject and async pipe.
Add template variables to the buttons for use in @ViewChild attributes.

@Component({
  selector: 'bc-book-detail',
  template: `
    <mat-card *ngIf="book">
      ...
      <mat-card-subtitle>Original {{ timerService.timerSource$ | async }} 
      </mat-card-subtitle>
      ...
      <button #resume id="resume" ...</button>
      <button #pause id="pause" ...</button>
      <button #reset id="reset" ...</button>
      </mat-card-actions>
    </mat-card>

  `,

book-detail.ts - javacript mods

Bring the service in via constructor injector, to call initTimer(). Use @ViewChild to send buttons to the service.

export class BookDetailComponent implements AfterViewInit {

  // @Inputs and @Outputs as previously defined

  constructor(public timerService: PomoTimerService) {}

  @ViewChild('resume', {read: ElementRef}) resumeButton;
  @ViewChild('pause', {read: ElementRef}) pauseButton;
  @ViewChild('reset', {read: ElementRef}) resetButton;

  ngAfterViewInit() {
    const buttons = {
      resumeButton: this.resumeButton,
      pauseButton: this.pauseButton,
      resetButton: this.resetButton
    };
    this.timerService.initTimer(buttons);
  }
9
  • Thank you Richard for this solution. At least I was on the right track as I had attempted something like this but couldn't get it to work. and unfortunately, I still can not get it to work. What are you passing in as @Input to the component? At this point, I am not even seeing the console.log of the timer. Commented Apr 24, 2018 at 15:51
  • I got it working now! but the pause is not working. The timer continues to emit values even when pause is pushed. Commented Apr 24, 2018 at 15:57
  • Good to see you are making progress with it. The pause button works the same as the start/resume button, so sounds like there's a typo somewhere in your code. It certainly works on my test system. Commented Apr 24, 2018 at 19:52
  • Is there any way you could share with me the code the you did to implement this. I still can't get the timer to pause. There are not typos. I implemented everything that you recommended. and it doesn't work. I have been trying to get this to work for several days now and I am at my wits end. I would appreciate it. Thanks Commented Apr 25, 2018 at 16:53
  • Here is a StackBlitz Commented Apr 25, 2018 at 20:15

Not the answer you're looking for? Browse other questions tagged or ask your own question.