paint-brush
Wrap any jQuery plugin with Angular 2 component — case study.by@MichalMajewski
46,232 reads
46,232 reads

Wrap any jQuery plugin with Angular 2 component — case study.

by Michal MajewskiMarch 25th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Angular like most of nowadays fronted frameworks, works on higher level of abstraction than plain JavaScript is operating on. Because of that, at some point you will find yourself in a situation, that you will need to move some jQuery or plain JavaScript library into Angular world. There are plenty reason for doing that:

Company Mentioned

Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Wrap any jQuery plugin with Angular 2 component — case study.
Michal Majewski HackerNoon profile picture

Angular like most of nowadays fronted frameworks, works on higher level of abstraction than plain JavaScript is operating on. Because of that, at some point you will find yourself in a situation, that you will need to move some jQuery or plain JavaScript library into Angular world. There are plenty reason for doing that:

  • Performance

When your lib is actively using events like mousemove, you can run yourself into troubles - we will talk about this later on.

  • Facade pattern

You are hiding some nasty configurations details of jQuery plugin and exposing nice elegant component to outside world.

  • Re-usability
  • Better architecture

If you will wrap your library into so called “dummy component” you can benefit from all goodness, that unidirectional data flow programming is coming with.

So let’s start some coding

We gonna wrap slick carousel into Angular component. Slick carousel is one of many slider implementations and my favorite one. If you don’t know it yet, I highly recommend you to give it a try.

First thing, we supposed to start from is defining, what is our imaginary API for that component. I think something like below will be good starting point.

<slick-carousel class="carousel">
  <div  *ngFor="let slide of slides" class="slide">{{slide.caption}}</div>
</slick-carousel>

Then our implementation can look like on below snippet.

import { Component, ElementRef} from '@angular/core';
import $ from "jquery";
require('slick-carousel');

@Component({
  selector: 'slick-carousel',
  template: `<ng-content></ng-content>`
})
export class SlickCarouselComponent {
  constructor(private el: ElementRef) {
  }

  $carousel: JQuery | any;

  ngAfterViewInit() {
    this.$carousel = $(this.el.nativeElement).slick({});
  }
}

Our component template will consists with only one ng-content element. All slides will be injected into this element. We are waiting for Angular to render component and then, with ngAfterViewInit() life cycle hook, we are just simply initializing our plugin. That’s pretty much it. But we can do it little bit better.

Performance

One thing, we can improve is performance of that solution. Angular change detection mechanism is being triggered on every event, that our code is subscribed into. In our example we are using JQuery plugin, that is actively listening on every mousemove event, while we are swiping through slides. That results with change detection being called multiple times, as you can see on below screen.

If in our component we have code like this <footer [class]="getClass()"></footer> , then getClass() method will be called dozens of times, while we are swiping through elements in carousel. That’s also reason we should avoid binding to methods and rather do mapping to ViewModel within ngOnChanges life hook once, but that’s some different story. To omit that problem, we can make use of NgZone class from angular and move slick carousel outside angular world.

ngAfterViewInit() {
  this.zone.runOutsideAngular(()=>{
    this.$carousel = $(this.el.nativeElement).slick({});
  });
}

When we wrap some code with runOutsideAngular block, angular will stop listening on events from that code. If we want Angular to be deaf on mousemove events, but we wanna react on swipe events, we can return back to angular zone via this.zone.run(()=>), like on below example.

ngAfterViewInit() {
  this.zone.runOutsideAngular(() => {
    this.$carousel = $(this.el.nativeElement).slick({});
    this.$carousel.on('swipe', (event, slick, direction) => {
      this.zone.run(() => ++this.counter);
    });
  });
}

Content changes

We didn’t cover so far possibility for dynamic changes of carousel slides. In our implementation we are initializing carousel just once. When we update list of slides, our carousel component won’t know anything about that and slick carousel will not add newly created slides. That’s happening because in our example *ngFor is directive which is responsible for creating slides and it is not communicating with <slick-carousel>. There are many techniques, how to inform parent component about changes on its content components. One option is to use dependency injection and get parent component inside child component constructor with@Host() decorator. Other Option is to implement your own *ngFor logic and create children dynamically based on template provided by component user. Other option is to have @ContentChildren() inside parent controller and monitor your children from there. We will cover today option with @Host() decorator.

First we need to start with creating directive for each slide.

@Directive({
  selector: '[slick-carousel-item]',
})
export class SlickCarouselItem {
  constructor(private el: ElementRef, @Host() private carousel: SlickCarouselComponent) {
  }
  ngAfterViewInit() {
    this.carousel.addSlide(this);
  }
  ngOnDestroy() {
    this.carousel.removeSlide(this);
  }
}

Inside this directive we are injecting parent component via @Host() decorator. When directive is initialized we are informing parent component about new item and we are removing from parent when ngOnDestroy is happening. Our Carousel implementation looks now like below.

@Component({
  selector: 'slick-carousel',
  template: `<ng-content></ng-content>`
})
export class SlickCarouselComponent {
  constructor(private el: ElementRef, private zone: NgZone) {
  }

  $carousel: JQuery | any;

  initialized = false;

  initCarousel() {
    this.zone.runOutsideAngular(() => {
      this.$carousel = $(this.el.nativeElement).slick({});
    });
    this.initialized = true;
  }

  slides = [];

  addSlide(slide) {
    !this.initialized && this.initCarousel();
    this.slides.push(slide);
    this.$carousel.slick('slickAdd', slide.el.nativeElement);
  }

  removeSlide(slide) {
    const idx = this.slides.indexOf(slide);
    this.$carousel.slick('slickRemove', idx);
    this.slides = this.slides.filter(s => s != slide);
  }
}

We have now working angular component that uses under the hood JQuery plugin. We can dynamically add and remove slides and our component is not slowing down Angular change detection.