Reputation: 56
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
Reputation: 8062
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
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.
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
}
interval()
If you insist on using RxJS, you'd need to mind multiple things
ReplaySubject
(shown here) or using a template reference variable with fromEvent
function (not shown).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.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