Connie Leung
Posted on January 29, 2023
Introduction
This is day 27 of Wes Bos's JavaScript 30 challenge and I am going to use RxJS and Angular to click and slide a series of div elements. When mouse click occurs, I add a CSS class to the parent <div> element to perform scale transformation. When mouse is up or it exits the browser, I remove the class to undo the CSS transformation.
In this blog post, I describe how to use RxJS fromEvent to listen to mousedown event and emit the event to concatMap operator. In the callback of concatMap, it streams mousemove events, updates the scrollLeft property of the div element and flattens the inner Observables. Moreover, the Angular component resolves another Observable in the inline template in order to toggle CSS class in ngClass property.
Create a new Angular project
ng generate application day27-click-and-drag
Create Slider feature module
First, we create a Slider feature module and import it into AppModule. The feature module encapsulates SliderComponent
with a list of <div> elements.
Import SliderModule in AppModule
// slider.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SliderComponent } from './slider/slider.component';
@NgModule({
declarations: [
SliderComponent
],
imports: [
CommonModule
],
exports: [
SliderComponent
]
})
export class SliderModule { }
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { SliderModule } from './slider';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
SliderModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Declare Slider component in feature module
In Slider feature module, we declare SliderComponent
to build the application. SliderComponent
depends on inline template and SCSS file because styling is long in this tutorial.
src/app
├── app.component.ts
├── app.module.ts
└── slider
├── index.ts
├── slider
│ ├── slider.component.scss
│ └── slider.component.ts
└── slider.module.ts
// slider.component.ts
import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Observable, of } from 'rxjs';
@Component({
selector: 'app-slider',
template: `
<div class="items" [ngClass]="{ active: active$ | async }" #items>
<div *ngFor="let index of panels" class="item">{{index}}</div>
</div>
`,
styleUrls: ['./slider.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SliderComponent implements OnInit, OnDestroy {
@ViewChild('items', { static: true, read: ElementRef })
slider!: ElementRef<HTMLDivElement>;
active$!: Observable<boolean>;
panels = [...Array(25).keys()].map(i => i < 9 ? `0${i + 1}` : `${i + 1}`);
ngOnInit(): void {
this.active$ = of(false);
}
ngOnDestroy(): void {
}
}
SliderComponent
generates 25 <div> elements numbered between 01 and 25. When I click any <div> element, I can slide from left to right and vice versa. Moreover, the parent <div> of the <div> elements includes an active CSS class when sliding occurs.
this.active$
is a boolean Observable, it resolves and toggles the active CSS class of the <div> element. When the class is found in the element , scale transformation occurs and background color changes.
Next, I delete boilerplate codes in AppComponent
and render SliderComponent
in inline template.
// app.component.ts
import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
@Component({
selector: 'app-root',
template: '<app-slider></app-slider>',
styles: [`
:host {
display: block;
}
`]
})
export class AppComponent {
title = 'Day 27 Click and Drag';
constructor(titleService: Title) {
titleService.setTitle(this.title);
}
}
Use RxJS and Angular to implement SliderComponent
I am going to rewrite active$ Observable and create a click-and-slide subscription in ngOnInit method.
Use ViewChild to obtain reference to div element
@ViewChild('items', { static: true, read: ElementRef })
slider!: ElementRef<HTMLDivElement>;
Declare subscription instance member and unsubscribe in ngDestroy()
subscription = new Subscription();
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
// slider.component.ts
ngOnInit(): void {
const sliderNative = this.slider.nativeElement;
const mouseDown$ = fromEvent(sliderNative, 'mousedown');
const mouseLeave$ = fromEvent(sliderNative, 'mouseleave');
const mouseUp$ = fromEvent(sliderNative, 'mouseup');
const stop$ = merge(mouseLeave$, mouseUp$);
const mouseMove$ = fromEvent(sliderNative, 'mousemove');
this.active$ = merge(mouseDown$.pipe(map(() => true)), stop$.pipe(map(() => false)))
.pipe(startWith(false));
this.subscription = mouseDown$.pipe(
filter((moveDownEvent) => moveDownEvent instanceof MouseEvent),
map((moveDownEvent) => moveDownEvent as MouseEvent),
concatMap((moveDownEvent) => {
const startX = moveDownEvent.pageX - sliderNative.offsetLeft;
const scrollLeft = sliderNative.scrollLeft;
return mouseMove$.pipe(
filter((moveEvent) => moveEvent instanceof MouseEvent),
map((moveEvent) => moveEvent as MouseEvent),
tap((moveEvent) => moveEvent.preventDefault()),
map((e) => {
const x = e.pageX - sliderNative.offsetLeft;
const walk = (x - startX) * 3;
sliderNative.scrollLeft = scrollLeft - walk;
}),
takeUntil(stop$)
);
}),
).subscribe();
}
- const mouseDown$ = fromEvent(sliderNative, ‘mousedown’) – listens to mousedown event of the div elements
- const mouseLeave$ = fromEvent(sliderNative, ‘mouseleave’) – listens to mouseleave event of the div elements
- const mouseUp$ = fromEvent(sliderNative, ‘mouseup’) – listens to mouseup event of the div elements
- const mouseMove$ = fromEvent(sliderNative, ‘mousemove’) – listens to mousemove event of the div elements
- const stop$ = merge(mouseLeave$, mouseUp$); – stop click-and-slide when mouse exits the browser or mouse up
Toggle active class
this.active$ = merge(
mouseDown$.pipe(map(() => true)), stop$.pipe(map(() => false))
).pipe(startWith(false))
- mouseDown$.pipe(map(() => true) – when mouse is clicked, add active class
- stop$.pipe(map(() => false)) – when click and slide stops, remove active class
- merge(mouseDown$.pipe(map(() => true)), stop$.pipe(map(() => false))) – merge multiple streams to toggle active class
- startWith(false)) – the initial value of this.active$ observable is false
Click and Slide HTML elements
mouseDown$.pipe(
filter((moveDownEvent) => moveDownEvent instanceof MouseEvent),
map((moveDownEvent) => moveDownEvent as MouseEvent),
concatMap((moveDownEvent) => {
const startX = moveDownEvent.pageX - sliderNative.offsetLeft;
const scrollLeft = sliderNative.scrollLeft;
return mouseMove$.pipe(
filter((moveEvent) => moveEvent instanceof MouseEvent),
map((moveEvent) => moveEvent as MouseEvent),
tap((moveEvent) => moveEvent.preventDefault()),
map((e) => {
const x = e.pageX - sliderNative.offsetLeft;
const walk = (x - startX) * 3;
sliderNative.scrollLeft = scrollLeft - walk;
}),
takeUntil(stop$)
);
})
)
- mouseDown$.pipe(…) – observe mousedown event
- filter((moveDownEvent) => moveDownEvent instanceof MouseEvent) – filter event is MouseEvent
- map((moveDownEvent) => moveDownEvent as MouseEvent) – cast event as MouseEvent
- concatMap(…) – create a new Observable that flattens mouseMove$ inner Observables
- mouseMove$.pipe(…) – when sliding occurs, I update the scrollLeft property of div element
- tap((moveEvent) => moveEvent.preventDefault()) – invoke preventDefault method of the mouse event
- takeUntil(stop$) – sliding stops when mouse exits browser or mouse up
Final Thoughts
In this post, I show how to use RxJS and Angular to click and slide div elements until mouse up or mouse leave occurs. fromEvent observes mousedown event and emits the event to concatMap to stream mousemove events. For each mousemove event, scrollLeft property of the div element is calculated and concatMap flattens the Observable. Then, the div element can scroll left or right.
Moreover, when mouse down occurs, the Observable emits true to add the active class to div element. Otherwise, the Observable emits false to remove the class.
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.
Resources:
- Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day27-click-and-drag
- Live demo: https://railsstudent.github.io/ng-rxjs-30/day27-click-and-drag/
- Wes Bos's JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30
Posted on January 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.