Click and slide HTML elements using RxJS and Angular
Tags: angular, tutorial, rxjs, webdev
Published: 2023-01-29 12:48:56

## 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 &lt;div&gt; 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
```bash
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 &lt;div&gt; elements.

Import SliderModule in AppModule

```typescript
// 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 { }
```

```typescript
// 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.

```bash
src/app
├── app.component.ts
├── app.module.ts
└── slider
    ├── index.ts
    ├── slider
    │   ├── slider.component.scss
    │   └── slider.component.ts
    └── slider.module.ts
```


```typescript
// 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 &lt;div&gt; elements numbered between 01 and 25. When I click any &lt;div&gt; element, I can slide from left to right and vice versa. Moreover, the parent &lt;div&gt; of the &lt;div&gt; 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 &lt;div&gt; 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.

```typescript
// 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

```typescript
@ViewChild('items', { static: true, read: ElementRef })
slider!: ElementRef<HTMLDivElement>;
```

Declare subscription instance member and unsubscribe in ngDestroy()


```typescript
subscription = new Subscription();

ngOnDestroy(): void {
    this.subscription.unsubscribe();
}
```

```typescript
// 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
```typescript
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
```typescript
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