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:
When your lib is actively using events like mousemove, you can run yourself into troubles - we will talk about this later on.
You are hiding some nasty configurations details of jQuery plugin and exposing nice elegant component to outside world.
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.
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.
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);
});
});
}
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.