Kateryna
Kateryna

Reputation: 56

Reset rxjs interval on click in Angular

A simple question regarding intervals in RxJS - I have made a carousel where slides change every minute and there is a detail that dissatisfies me. When the slide is switched manually by user to next or previous - interval will still change the slide automatically but in whatever time there's left till interval fires off the new value, so it disregards anything what the user did. How can I make interval know if the user changed the slide, and only from user action count a minute and then change slide automatically. Other times interval should work automatically as regular.

Will be happy to hear ideas, thanks!!!

Here's what I got:

  source = interval(10000);

  ngOnInit(): void {
    this.source.subscribe(() => this.slide('forward'));
  }

  slide(option): void {
    switch (option) {
      case 'forward':
        this.currentSlide === this.SLIDES.length - 1 ?
          this.currentSlide = DEFAULT_SLIDE : this.currentSlide++;
        break;
      case 'back':
        this.currentSlide === DEFAULT_SLIDE ?
          this.currentSlide = this.SLIDES.length - 1 : this.currentSlide--;
        break;
    }
  }

Template:

<div class="sliderNav">
    <mat-icon (click)="slide('back')">keyboard_arrow_up</mat-icon>
    <p>{{currentSlide + 1}}/{{SLIDES.length}}</p>
    <mat-icon (click)="slide('forward')">keyboard_arrow_down</mat-icon>
</div>

Upvotes: 2

Views: 2435

Answers (2)

Mrk Sef
Mrk Sef

Reputation: 8062

Merging streams with RxJS

A possible solution is to create streams from your button click events and merge them together.

You can then use switchMap to reset your interval every time a source emits. Here's how that might look.

<div class="sliderNav">
    <mat-icon #slideBack>keyboard_arrow_up</mat-icon>
    <p>{{currentSlide + 1}}/{{SLIDES.length}}</p>
    <mat-icon #slideForward>keyboard_arrow_down</mat-icon>
</div>

intervalTime = 10000;

@ViewChild('slideBack', {static: true}) 
slideBack: ElementRef;

@ViewChild('slideForward', {static: true}) 
slideForward: ElementRef;

ngOnInit(): void {

  const [first$, forward$, interval$] = [
    timer(intervalTime),
    fromEvent(slideForward, 'click'),
    interval(intervalTime)
  ].map(v => v.pipe(
    mapTo("forward")
  ));

  const backward$ = fromEvent(slideBack, 'click').pipe(
    mapTo("backward")
  );

  merge(first$, forward$, backward$).pipe(
    switchMap(option => interval$.pipe(
      startWith(option)
    ))
  ).subscribe(option => {

    switch (option) {
      case 'forward':
        this.currentSlide === this.SLIDES.length - 1 ?
          this.currentSlide = DEFAULT_SLIDE : this.currentSlide++;
        break;
      case 'backward':
        this.currentSlide === DEFAULT_SLIDE ?
          this.currentSlide = this.SLIDES.length - 1 : this.currentSlide--;
        break;
    }

  })
}

Upvotes: 1

Barremian
Barremian

Reputation: 31125

I would say RxJS is overkill for this behavior. It could be achieved using plain JS setTimeout() function. Moreover you aren't closing the subscription when the component is destroyed which might lead to a memory leak.

Option 1: JS setTimeout()

timeoutId: number;

ngOnInit(): void {
  this.resetTimer();      // <-- start timer when the component is created
}

resetTimer() {
  if (!!this.timeoutId) {             // <-- Update: clear the timeout
    clearTimeout(this.timeoutId);
  }
  this.timeoutId = setTimeout(() => this.slide('forward'), 10000);
}

/**
 * Reset the timer each time `slide()` function is triggered:
 *   1. When the `setTimeout()` triggers the callback
 *   2. When the user changes the slide manually
 */
slide(option): void {
  this.resetTimer();       // <-- reset timer here

  switch (option) {
    case 'forward':
      this.currentSlide === this.SLIDES.length - 1 ?
        this.currentSlide = DEFAULT_SLIDE : this.currentSlide++;
      break;
    case 'back':
      this.currentSlide === DEFAULT_SLIDE ?
        this.currentSlide = this.SLIDES.length - 1 : this.currentSlide--;
      break;
  }
}

ngOnDestroy() {
  clearTimeout(this.timeoutId);      // <-- clear the timeout when the component is destroyed
}

Option 2: RxJS interval()

If you insist on using RxJS, you'd need to mind multiple things

  1. Obtain the manual inputs from the user using either a multi-cast observable like ReplaySubject (shown here) or using a template reference variable with fromEvent function (not shown).
  2. Use switchMap operator to map from one observable to another. switchMap cancels/closes the inner observable when the outer observable emits. This way the timer would be regenerated.
  3. Close the subscription in the ngOnDestroy() hook to avoid memory leaks from open subscriptions. Illustrated here using the takeUntil operator.

Controller (*.ts)

import { ReplaySubject } from 'rxjs';
import { map, switchMap, takeUntil } from 'rxjs/operators';

clickEvents$ = new ReplaySubject<string>(1);   // <-- buffer 1
closed$ = new ReplaySubject<any>(1);

ngOnInit() {
  this.clickEvents$.asObservable.pipe(
    switchMap((event: string) => interval(10000).pipe(    // <-- cancel inner subscription when outer subscription emits
      map(() => event)                         // <-- map back to the event
    )),
    takeUntil(this.closed$)
  ).subscribe(this.slide.bind(this));          // <-- use `bind()` to preserve `this` in callback
}

slide(option): void {
  switch (option) {
    case 'forward':
      this.currentSlide === this.SLIDES.length - 1 ?
        this.currentSlide = DEFAULT_SLIDE : this.currentSlide++;
      break;
    case 'back':
      this.currentSlide === DEFAULT_SLIDE ?
        this.currentSlide = this.SLIDES.length - 1 : this.currentSlide--;
      break;
  }
}

ngOnDestroy() {
  this.closed$.next();                          // <-- close open subscriptions
}

Template (*.html)

<button (click)="clickEvent$.next('back')">Back</button>
<button (click)="clickEvent$.next('forward')">Forward</button>

Upvotes: 3

Related Questions