Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 23, 2023 04:30 pm GMT

Follow along link highlighter using RxJS and Angular

Introduction

This is day 22 of Wes Bos's JavaScript 30 challenge and I am going to use RxJS and Angular to create a highlighter that follows a link when cursor hovers it. The follow along link highlighter updates CSS width, height and transform when mouseenter event occurs on the links of Angular components.

In this blog post, I describe how to use RxJS fromEvent to listen to mouseenter event of anchor elements and update the BehaviorSubject in Highlighter service. Angular components observe the BehaviorSubject and emit CSS width, height and transform to an Observable stream. The stream resolves in the inline template by async pipe and the follow along link highlighter effect occurs.

Create a new Angular project

ng generate application day22-follow-along-link-highlighter

Create Highlighter feature module

First, we create a Highlighter feature module and import it into AppModule. The feature module encapsulates HighlighterPageComponent, HighlighterMenuComponent, HighlighterContentComponent and HighlightAnchorDirective.

Import HighlighterhModule in AppModule

// highlighter.module.tsimport { CommonModule } from '@angular/common';import { NgModule } from '@angular/core';import { HighlightAnchorDirective } from './directives/highlight-anchor.directive';import { HighlighterContentComponent } from './highlighter-content/highlighter-content.component';import { HighlighterMenuComponent } from './highlighter-menu/highlighter-menu.component';import { HighlighterPageComponent } from './highlighter-page/highlighter-page.component';@NgModule({  declarations: [    HighlighterPageComponent,    HighlightAnchorDirective,    HighlighterMenuComponent,    HighlighterContentComponent,  ],  imports: [    CommonModule  ],  exports: [    HighlighterPageComponent  ]})export class HighlighterModule { }
// app.module.tsimport { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { AppComponent } from './app.component';import { CoreModule } from './core';import { HighlighterModule } from './highlighter';@NgModule({  declarations: [    AppComponent  ],  imports: [    BrowserModule,    HighlighterModule,    CoreModule,  ],  providers: [],  bootstrap: [AppComponent]})export class AppModule { }

Declare Highlighter components in feature module

In Highlighter feature module, we declare three Angular components, HighlighterPageComponent, HighlighterMenuComponent and HighlighterContentComponent to build the application.

src/app app.component.ts app.module.ts core    core.module.ts    index.ts    services        window.service.ts highlighter     directives        highlight-anchor.directive.ts     helpers        mouseenter-stream.helper.ts     highlighter-content        highlighter-content.component.ts     highlighter-menu        highlighter-menu.component.ts     highlighter-page        highlighter-page.component.ts     highlighter.interface.ts     highlighter.module.ts     index.ts     services         highlighter.service.ts

HighlighterPageComponent acts like a shell that encloses HighlighterMenuComponent and HighlighterContentComponent. For your information, <app-highlighter-page> is the tag of HighlighterPageComponent.

// highlighter-page.component.tsimport { ChangeDetectionStrategy, Component } from '@angular/core';import { HighlighterService } from '../services/highlighter.service';@Component({  selector: 'app-highlighter-page',  template: `    <ng-container>          <app-highlighter-menu></app-highlighter-menu>      <app-highlighter-content></app-highlighter-content>      <ng-container *ngIf="highlightStyle$ | async as hls">        <span class="highlight" [ngStyle]="hls"></span>      </ng-container>    </ng-container>  `,  styles: [`...omitted due to brevity ...`],  changeDetection: ChangeDetectionStrategy.OnPush})export class HighlighterPageComponent {  highlightStyle$ = this.highlighterService.highlighterStyle$  constructor(private highlighterService: HighlighterService) {}}

HighlighterService is a simple service that stores CSS width, height and transform of follow along link highlighter in a BehaviorSubject.

// highlighter.interface.tsexport interface HighlighterStyle {    width: string,    height: string,    transform: string,}
// highlighter.service.tsimport { Injectable } from '@angular/core';import { BehaviorSubject } from 'rxjs';import { HighlighterStyle } from '../highlighter.interface';@Injectable({  providedIn: 'root'})export class HighlighterService {  private readonly highlighterStyleSub = new BehaviorSubject<HighlighterStyle>({      width: '0px',      height: '0px',      transform: ''  });  readonly highlighterStyle$ = this.highlighterStyleSub.asObservable();  updateStyle(style: HighlighterStyle) {    this.highlighterStyleSub.next(style);  }}

HighlighterMenuComponent encapsulates a menu and each menu item encloses an anchor element whereas HighlighterContentComponent is consisted of several paragraphs with 19 embedded anchor elements.

// highlighter-menu.component.tsimport { ChangeDetectionStrategy, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';import { Subscription } from 'rxjs';import { WINDOW } from '../../core';import { createMouseEnterStream } from '../helpers/mouseenter-stream.helper';import { HighlighterService } from '../services/highlighter.service';@Component({  selector: 'app-highlighter-menu',  template: `    <nav>      <ul class="menu">        <li><a href="" #home>Home</a></li>        <li><a href="" #order>Order Status</a></li>        <li><a href="" #tweet>Tweets</a></li>        <li><a href="" #history>Read Our History</a></li>        <li><a href="" #contact>Contact Us</a></li>      </ul>    </nav>  `,  styles: [`...omitted due to brevity...`],  changeDetection: ChangeDetectionStrategy.OnPush})export class HighlighterMenuComponent implements OnInit, OnDestroy {  @ViewChild('home', { static: true, read: ElementRef })  home!: ElementRef<HTMLAnchorElement>;  @ViewChild('order', { static: true, read: ElementRef })  order!: ElementRef<HTMLAnchorElement>;  @ViewChild('tweet', { static: true, read: ElementRef })  tweet!: ElementRef<HTMLAnchorElement>;  @ViewChild('history', { static: true, read: ElementRef })  history!: ElementRef<HTMLAnchorElement>;  @ViewChild('contact', { static: true, read: ElementRef })  contact!: ElementRef<HTMLAnchorElement>;  subscription!: Subscription;  constructor(private highlighterService: HighlighterService, @Inject(WINDOW) private window: Window) {}  ngOnInit(): void {}  ngOnDestroy(): void {    this.subscription.unsubscribe();  }}
// highlighter-content.component.tsimport { AfterViewInit, ChangeDetectionStrategy, Component, Inject, OnDestroy, QueryList, ViewChildren } from '@angular/core';import { Subscription } from 'rxjs';import { WINDOW } from '../../core';import { HighlightAnchorDirective } from '../directives/highlight-anchor.directive';import { createMouseEnterStream } from '../helpers/mouseenter-stream.helper';import { HighlighterService } from '../services/highlighter.service';@Component({  selector: 'app-highlighter-content',  template: `    <div class="wrapper">      <p>Lorem ipsum dolor sit amet, <a href="">consectetur</a> adipisicing elit. Est <a href="">explicabo</a> unde natus necessitatibus esse obcaecati distinctio, aut itaque, qui vitae!</p>      <p>Aspernatur sapiente quae sint <a href="">soluta</a> modi, atque praesentium laborum pariatur earum <a href="">quaerat</a> cupiditate consequuntur facilis ullam dignissimos, aperiam quam veniam.</p>      <p>Cum ipsam quod, incidunt sit ex <a href="">tempore</a> placeat maxime <a href="">corrupti</a> possimus <a href="">veritatis</a> ipsum fugit recusandae est doloremque? Hic, <a href="">quibusdam</a>, nulla.</p>      <p>Esse quibusdam, ad, ducimus cupiditate <a href="">nulla</a>, quae magni odit <a href="">totam</a> ut consequatur eveniet sunt quam provident sapiente dicta neque quod.</p>      <p>Aliquam <a href="">dicta</a> sequi culpa fugiat <a href="">consequuntur</a> pariatur optio ad minima, maxime <a href="">odio</a>, distinctio magni impedit tempore enim repellendus <a href="">repudiandae</a> quas!</p>    </div>  `,  styles: [`...omitted due to brevty...`],  changeDetection: ChangeDetectionStrategy.OnPush})export class HighlighterContentComponent implements AfterViewInit, OnDestroy {  @ViewChildren(HighlightAnchorDirective)  anchors!: QueryList<HighlightAnchorDirective>;  subscription!: Subscription;  constructor(private highlighterService: HighlighterService, @Inject(WINDOW) private window: Window) { }  ngAfterViewInit(): void {}  ngOnDestroy(): void {    this.subscription.unsubscribe();  }}

There are 19 anchor elements in this component; it is tedious to include template reference variables and reference them by 19 @ViewChild decorators. Therefore, I declare a HighlightAnchorDirective and pass the directive to @ViewChildren decorator to obtain all references to anchor elements.

// highlight-anchor.directive.tsimport { Directive, ElementRef } from '@angular/core';@Directive({  selector: 'a'})export class HighlightAnchorDirective {  nativeElement!: HTMLAnchorElement;  constructor(el: ElementRef<HTMLAnchorElement>) {     this.nativeElement = el.nativeElement;  }}

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

// app.component.tsimport { Component } from '@angular/core';import { Title } from '@angular/platform-browser';@Component({  selector: 'app-root',  template: `<app-highlighter-page></app-highlighter-page>`,  styles: [`    :host {      display: block;    }  `],})export class AppComponent {  title = ' Day 22 Follow along link highlighter';  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 Loves blog post here.

// core/services/window.service.tsimport { 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.tsimport { 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 { }

Create helper to make mouseenter stream

Both HighlighterMenuComponent and HighlighterContentComponent listen to mouseenter event to order to emit CSS properties the follow along link highlighter. Therefore, I create a function that accepts HTMLAnchorElements and returns a merge stream of mouseenter events.

import { ElementRef, QueryList } from '@angular/core';import { fromEvent, map, merge } from 'rxjs';import { HighlightAnchorDirective } from '../directives/highlight-anchor.directive';export function createMouseEnterStream(  elementRefs: ElementRef<HTMLAnchorElement>[] | QueryList<HighlightAnchorDirective>,   window: Window) {    const mouseEnter$ = elementRefs.map(({ nativeElement }) =>       fromEvent(nativeElement, 'mouseenter')        .pipe(          map(() => {            const linkCoords = nativeElement.getBoundingClientRect();            return {              width: linkCoords.width,              height: linkCoords.height,              top: linkCoords.top + window.scrollY,              left: linkCoords.left + window.scrollX            };          })        ));    return merge(...mouseEnter$)      .pipe(        map((coords) => ({          width: `${coords.width}px`,          height: `${coords.height}px`,          transform: `translate(${coords.left}px, ${coords.top}px)`        })),      );     }
  • fromEvent(nativeElement, mouseenter) listens to mouseenter event of anchor element
  • map finds the dimensions and top-left point of the anchor element
  • elementRefs maps to mouseEnter$ that is an array of Observable
  • merge(mouseEnter$) merges mouseenter Observables
  • map returns CSS width, height and transform of the anchor element

Use RxJS and Angular to implement HighlighterMenuComponent

I am going to define an Observable, subscribe it and update the BehaviorSubject in the service.

Use ViewChild to obtain references to anchor elements@ViewChild('home', { static: true, read: ElementRef })home!: ElementRef<HTMLAnchorElement>;@ViewChild('order', { static: true, read: ElementRef })order!: ElementRef<HTMLAnchorElement>;@ViewChild('tweet', { static: true, read: ElementRef })tweet!: ElementRef<HTMLAnchorElement>;@ViewChild('history', { static: true, read: ElementRef })history!: ElementRef<HTMLAnchorElement>;@ViewChild('contact', { static: true, read: ElementRef })contact!: ElementRef<HTMLAnchorElement>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();ngOnDestroy(): void {    this.subscription.unsubscribe();}

Subscribe the observable and update BehaviorSubject in HighlighterService.

// highlighter-menu.component.tsngOnInit(): void {    this.subscription = createMouseEnterStream(        [this.home, this.order, this.tweet, this.history, this.contact],         this.window    ).subscribe((style) => this.highlighterService.updateStyle(style));}

Use RxJS and Angular to implement HighlighterContentComponent

Use ViewChildren to obtain references to anchor elements

@ViewChildren(HighlightAnchorDirective)anchors!: QueryList<HighlightAnchorDirective>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();ngOnDestroy(): void {    this.subscription.unsubscribe();}

Create mouseenter stream, subscribe it and update the CSS styles to the BehaviorSubject in the service.

// highlighter-content.component.tsngAfterViewInit(): void {    this.subscription = createMouseEnterStream(this.anchors, this.window)      .subscribe((style) => this.highlighterService.updateStyle(style));}

The subject invokes HighlighterService to update the BehaviorSubject.

Move the highlighter in HighlighterPageComponent

In HighlighterPageComponent, the constructor injects HighlighterService and I assign this.highlighterService.highlighterStyle$ to highlightStyle$ instance member.

highlightStyle$ = this.highlighterService.highlighterStyle$

In inline template, async pipe resolves highlightStyle$ and updates CSS styles of <span> element. Then, the span element highlights the text of the hovered anchor element.

// highlighter-page.component.ts<ng-container *ngIf="highlightStyle$ | async as hls">    <span class="highlight" [ngStyle]="hls"></span></ng-container>

Final Thoughts

In this post, I show how to use RxJS and Angular to build a highlighter that moves to the hovered anchor element. Child components create Observables to pass CSS properties to shared HighlighterService. Parent component observes the observable and updates the CSS styles of the span element to produce the effect.

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:


Original Link: https://dev.to/railsstudent/follow-along-link-highlighter-using-rxjs-and-angular-2fjo

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To