Slide in images on scroll using RxJS and Angular

Reading Time: 5 minutes

Loading

Introduction

This is day 13 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to slide in 5 images when scrolling up and down the browser images.

In this blog post, I describe how to use RxJS operators (fromEvent, debounce time, map, startWith) to listen to scroll event and slide in images when they are half shown in the window. When image is in the viewport, the application dynamically adds CSS class slide in image. Otherwise, I remove the class to slide out the image.

let's go

Create a new Angular project in workspace

ng generate application day13-slide-in-on-scroll

Create Scroll feature module

First, we create a Scroll feature module and import it into AppModule. The feature ultimately encapsulates one component that is Scroll.

Then, Import ScrollModule in AppModule

// scroll.module.ts

@NgModule({
  declarations: [
    ScrollComponent,
  ],
  imports: [
    CommonModule
  ],
  exports: [
    ScrollComponent
  ]
})
export class ScrollModule { }

// app.module.ts

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ScrollModule
  ],
  providers:[],
  bootstrap: [AppComponent]
})
export class AppModule { }

Declare component in feature module

In Scroll feature module, we declare ScrollComponent to listen to window’s scroll event, uses scrollY and innerHeight to determine whether or not an image at least 50% visible in the browser window. When the condition is met, enables CSS class to trigger the slide in images effect. Otherwise, the CSS class is removed to cause the images to slide out.

src/app/scroll
├── index.ts
├── scroll
│   ├── scroll.component.html
│   ├── scroll.component.spec.ts
│   └── scroll.component.ts
└── scroll.module.ts

In ScrollComponent, we define app selector, and inline CSS styles. scroll.component.html is the template file of the HTML codes. We will add the RxJS codes to implement the functions in later sections. For your information, <app-scroll> is the tag of ScrollComponent.

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-scroll',
  templateUrl: './scroll.component.html',
  styles: [`
    ... omit inline styles for brevity
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ScrollComponent {}

Next, I delete boilerplate codes in AppComponent and render KeySequenceDetectionComponent in inline template.

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  template: '<app-scroll></app-scroll>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 13 Slide in on Scroll';

  constructor(titleService: Title) {
    titleService.setTitle(this.title);
  }
}

Add window service to listen to scroll event

In order to detect scrolling on native Window, I write a window service to inject to ScrollComponent to listen to scroll event. The sample code is from Brian Love’s blog post here.

// core/services/window.service.ts

import { isPlatformBrowser } from "@angular/common";
import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core';

/* Create a new injection token for injecting the window into a component. */
export const WINDOW = new InjectionToken('WindowToken');

/* Define abstract class for obtaining reference to the global window object. */
export abstract class WindowRef {
  get nativeWindow(): Window | Object {
    throw new Error('Not implemented.');
  }
}

/* Define class that implements the abstract class and returns the native window object. */
export class BrowserWindowRef extends WindowRef {

  constructor() {
    super();
  }

  override get nativeWindow(): Object | Window {
    return window;    
  }
}

/* Create an factory function that returns the native window object. */
export function windowFactory(browserWindowRef: BrowserWindowRef, platformId: Object): Window | Object {
  if (isPlatformBrowser(platformId)) {
    return browserWindowRef.nativeWindow;
  }
  return new Object();
}

/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */
const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
const windowProvider: FactoryProvider = {
  provide: WINDOW,
  useFactory: windowFactory,
  deps: [ WindowRef, PLATFORM_ID ]
};

/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
  browserWindowProvider,
  windowProvider
];

Then, we provide WINDOW injection token in CoreModule and import CoreModule to AppModule.

// core.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WINDOW_PROVIDERS } from './services/window.service';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [WINDOW_PROVIDERS]
})
export class CoreModule { }

// app.module.ts

... other import statements ...
import { CoreModule } from './core';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ... other imports ...
    CoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Slide in images using RxJS and CSS transformation

Now, I am going to apply RxJS to implement scroll event in ScrollComponent and animate the effect of sliding images.

I take step-by-step approach to do the following:

  • Inject window provider to the component
  • Listen to window scroll event and derive an observable of Observable<boolean[]>
  • Use async pipe to resolve the observable. When the value is true, add active class to allow the images to slide in. When the value is false, remove active class to slide out the images.

Inject Window to ScrollComponent

First, I inject the provider, WINDOW, to the constructor of ScrollComponent

// scroll.component.ts 

import { WINDOW } from '../../core';

constructor(@Inject(WINDOW) private window: Window) { }

Implement RxJS logic to determine the state of CSS active class

Second, I modify HTML template to add #img reference to all <img> elements. Then, I can use ViewChildren to query all image elements in the code.

// scroll.component.html

<img src="https://unsplash.it/400/400" class="align-left slide-in" #img>
<img src="https://unsplash.it/400/401" class="align-right slide-in" #img>
<img src="https://unsplash.it/200/500" class="align-left slide-in" #img>
<img src="https://unsplash.it/200/200" class="align-right slide-in" #img>
<img src="https://unsplash.it/400/501" class="align-right slide-in" #img>   
// scroll.component.ts 

@ViewChildren('img')
sliderImages!: QueryList<ElementRef<HTMLImageElement>>;

Declare isSlideIn$ observable to determine the state of CSS class, active, for each image.

isSlideIn$ = fromEvent(this.window, 'scroll')
    .pipe(
      debounceTime(20),
      map(() => this.slideImages()),
      startWith([false, false, false, false, false])
    );

private slideImages() {
    const { scrollY, innerHeight } = this.window;
    return this.sliderImages.map(({ nativeElement: sliderImage }) => {
      // half way through the image
      const slideInAt = (scrollY + innerHeight) - sliderImage.height / 2;
      // bottom of the image
      const imageBottom = sliderImage.offsetTop + sliderImage.height;
      const isHalfShown = slideInAt > sliderImage.offsetTop;
      const isNotScrolledPast = scrollY < imageBottom;
      return isHalfShown && isNotScrolledPast;
    });
 }
  • fromEvent(this.window, ‘scroll’) listens to window scroll event
  • debounceTime(20) debounces 20 seconds so the events do not fire rapidly
  • map(() => this.slideImages()) determine to enable or disable css active class
  • startWith([false, false, false, false, false]) provides the initial values to disable css active class of all images

Use async pipe to resolve observable and bind the values to css classes

Lastly, I modify the HTML template to use async pipe to resolve isSlideIn$ observable. Finally, array elements are binded to [class.active] to perform CSS transformation in the HTML template.

// scroll.component.html

<div class="site-wrap" *ngIf="isSlideIn$ | async as isSlideIn">
   ... omit plain texts for brevity ...

<img src="https://unsplash.it/400/400" class="align-left slide-in" #img [class.active]="isSlideIn[0]">
<img src="https://unsplash.it/400/401" class="align-right slide-in" #img [class.active]="isSlideIn[1]">
<img src="https://unsplash.it/200/500" class="align-left slide-in" #img [class.active]="isSlideIn[2]">
<img src="https://unsplash.it/200/200" class="align-right slide-in" #img [class.active]="isSlideIn[3]">
<img src="https://unsplash.it/400/501" class="align-right slide-in" #img [class.active]="isSlideIn[4]">   
</div>

Finally, I have a simple page that demonstrates slide in/slide out images when window scrolls either up or down.

Final Thoughts

In this post, I show how to use RxJS and Angular to provide CSS animations in HTML template. The solution is applicable because the page has limited number of static images. I can use async pipe to resolve the observable and bind the observable value to css classes manually. At run time, the css classes are turned on and off to display the animations.

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:

  1. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day13-slide-in-on-scroll
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day13-slide-in-on-scroll/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30
  4. Angular Window Provider: https://brianflove.com/2018-01-11/angular-window-provider