Created
November 6, 2017 00:13
-
-
Save bennadel/5157610fd9b360bf2e796070e39447af to your computer and use it in GitHub Desktop.
Lazy Loading Images With The IntersectionObserver API In Angular 5.0.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
interface Contact { | |
id: number; | |
name: string; | |
avatarUrl: string; | |
} | |
@Component({ | |
selector: "my-app", | |
styleUrls: [ "./app.component.less" ], | |
template: | |
` | |
<p> | |
<a (click)="toggleContacts()">Toggle Contacts</a> | |
</p> | |
<ul *ngIf="isShowingContacts" class="contacts"> | |
<li *ngFor="let contact of contacts"> | |
<img [lazySrc]="contact.avatarUrl" lazySrcVisible="visible" /> | |
<span>{{ contact.name }} - {{ contact.id }}</span> | |
</li> | |
</ul> | |
<p> | |
<a (click)="popContact()">Pop Contact</a> | |
— | |
<a (click)="pushContact()">Push Contact</a> | |
</p> | |
` | |
}) | |
export class AppComponent { | |
public contacts: Contact[]; | |
public isShowingContacts: boolean; | |
public maxID: number; | |
// I initialize the app component. | |
constructor() { | |
this.contacts = []; | |
this.isShowingContacts = false; | |
this.maxID = 0; | |
for ( var i = 1 ; i < 50 ; i++ ) { | |
this.pushContact(); | |
} | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I remove a contact from the top of the collection. | |
public popContact() : void { | |
this.contacts.shift(); | |
} | |
// I add a new contact to the bottom of the collection. | |
public pushContact() : void { | |
this.contacts.push({ | |
id: ++this.maxID, | |
name: "Frances McDormand", | |
avatarUrl: `./app/frances-mcdormand.jpg?id=${ this.maxID }` | |
}); | |
} | |
// I toggle the showing of the contact list. | |
public toggleContacts() : void { | |
this.isShowingContacts = ! this.isShowingContacts; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Directive } from "@angular/core"; | |
import { ElementRef } from "@angular/core"; | |
import { OnDestroy } from "@angular/core"; | |
import { OnInit } from "@angular/core"; | |
import { Renderer2 } from "@angular/core"; | |
// Import the application components and services. | |
import { LazyTarget } from "./lazy-viewport.ts"; | |
import { LazyViewport } from "./lazy-viewport.ts"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@Directive({ | |
selector: "[lazySrc]", | |
inputs: [ | |
"src: lazySrc", | |
"visibleClass: lazySrcVisible" | |
] | |
}) | |
export class LazySrcDirective implements OnInit, OnDestroy, LazyTarget { | |
public element: Element; | |
public src: string; | |
public visibleClass: string; | |
private lazyViewport: LazyViewport; | |
private renderer: Renderer2; | |
// I initialize the lazy-src directive. | |
constructor( | |
elementRef: ElementRef, | |
lazyViewport: LazyViewport, | |
renderer: Renderer2 | |
) { | |
this.element = elementRef.nativeElement; | |
this.lazyViewport = lazyViewport; | |
this.renderer = renderer; | |
this.src = ""; | |
this.visibleClass = ""; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I get called once when the directive is being destroyed. | |
public ngOnDestroy() : void { | |
// If we haven't detached from the LazyViewport, do so now. | |
( this.lazyViewport ) && this.lazyViewport.removeTarget( this ); | |
} | |
// I get called once after the inputs have been bound for the first time. | |
public ngOnInit() : void { | |
// Attached this directive the LazyViewport so that we can be alerted to changes | |
// in this element's visibility on the page. | |
this.lazyViewport.addTarget( this ); | |
} | |
// I get called by the LazyViewport service when the element associated with this | |
// directive has its visibility changed. | |
public updateVisibility( isVisible: boolean, ratio: number ) : void { | |
// When this target starts being tracked by the viewport, the initial visibility | |
// will be reported, even if it is not visible. As such, let's ignore the first | |
// visibility update. | |
if ( ! isVisible ) { | |
return; | |
} | |
// Now that the element is visible, load the underlying SRC value. And, since we | |
// no longer need to worry about loading, we can detach from the LazyViewport. | |
this.lazyViewport.removeTarget( this ); | |
this.lazyViewport = null; | |
this.renderer.setProperty( this.element, "src", this.src ); | |
// If an active class has been provided, add it to the element. | |
( this.visibleClass ) && this.renderer.addClass( this.element, this.visibleClass ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Directive } from "@angular/core"; | |
import { ElementRef } from "@angular/core"; | |
import { OnDestroy } from "@angular/core"; | |
import { OnInit } from "@angular/core"; | |
// Import the application components and services. | |
import { LazyViewport } from "./lazy-viewport.ts"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@Directive({ | |
selector: "[lazyViewport]", | |
inputs: [ "offset: lazyViewportOffset" ], | |
// The primary role of this directive is to override the default LazyViewport | |
// instance at this point in the component tree. This way, any lazy-directives | |
// that are descendants of this element will receive this instance when using | |
// dependency-injection. | |
providers: [ | |
{ | |
provide: LazyViewport, | |
useClass: LazyViewport | |
} | |
] | |
}) | |
export class LazyViewportDirective implements OnInit, OnDestroy { | |
public offset: number; | |
private elementRef: ElementRef; | |
private lazyViewport: LazyViewport; | |
// I initialize the lazy-viewport directive. | |
constructor( | |
elementRef: ElementRef, | |
lazyViewport: LazyViewport | |
) { | |
this.elementRef = elementRef; | |
this.lazyViewport = lazyViewport; | |
this.offset = 0; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I get called once when the directive is being destroyed. | |
public ngOnDestroy() : void { | |
this.lazyViewport.teardown(); | |
} | |
// I get called once after the inputs have been bound for the first time. | |
public ngOnInit() : void { | |
// Ensure that the offset value is numeric when we go to initialize the viewport. | |
if ( isNaN( +this.offset ) ) { | |
console.warn( new Error( `[lazyViewportOffset] must be a number. Currently defined as [${ this.offset }].` ) ); | |
this.offset = 0; | |
} | |
// Now that this LazyViewport directive has overridden the instance of | |
// LazyViewport in the dependency-injection tree, we have to initialize it | |
// to use the current element as the observer root. | |
this.lazyViewport.setup( this.elementRef.nativeElement, +this.offset ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface LazyTarget { | |
element: Element; | |
updateVisibility: ( isVisible: boolean, ratio: number ) => void; | |
} | |
export class LazyViewport { | |
private observer: IntersectionObserver; | |
private targets: Map<Element, LazyTarget>; | |
// I initialize the lazy-viewport service. | |
constructor() { | |
this.observer = null; | |
// The IntersectionObserver watches Elements. However, when an element visibility | |
// changes, we have to alert an Angular Directive instance. As such, we're going | |
// to keep a map of Elements-to-Directives. This way, when our observer callback | |
// is invoked, we'll be able to extract the appropriate Directive from the | |
// Element-based observer entries collection. | |
this.targets = new Map(); | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I add the given LazyTarget implementation to the collection of objects being | |
// tracked by the IntersectionObserver. | |
public addTarget( target: LazyTarget ) : void { | |
if ( this.observer ) { | |
this.targets.set( target.element, target ); | |
this.observer.observe( target.element ); | |
// If we don't actually have an observer (lacking browser support), then we're | |
// going to punt on the feature for now and just immediately tell the target | |
// that it is visible on the page. | |
} else { | |
target.updateVisibility( true, 1.0 ); | |
} | |
} | |
// I setup the IntersectionObserver with the given element as the root. | |
public setup( element: Element = null, offset: number = 0 ) : void { | |
// While the IntersectionObserver is supported in the modern browsers, it will | |
// never be added to Internet Explorer (IE) and is not in my version of Safari | |
// (at the time of this post). As such, we'll only use it if it's available. | |
// And, if it's not, we'll fall-back to non-lazy behaviors. | |
if ( ! global[ "IntersectionObserver" ] ) { | |
return; | |
} | |
this.observer = new IntersectionObserver( | |
this.handleIntersectionUpdate, | |
{ | |
root: element, | |
rootMargin: `${ offset }px` | |
} | |
); | |
} | |
// I remove the given LazyTarget implementation from the collection of objects being | |
// tracked by the IntersectionObserver. | |
public removeTarget( target: LazyTarget ) : void { | |
// If the IntersectionObserver isn't supported, we never started tracking the | |
// given target in the first place. | |
if ( this.observer ) { | |
this.targets.delete( target.element ); | |
this.observer.unobserve( target.element ); | |
} | |
} | |
// I teardown this service instance. | |
public teardown() : void { | |
if ( this.observer ) { | |
this.observer.disconnect(); | |
this.observer = null; | |
} | |
this.targets.clear(); | |
this.targets = null; | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I handle changes in the visibility for elements being tracked by the intersection | |
// observer. | |
// -- | |
// CAUTION: Using fat-arrow binding for method. | |
private handleIntersectionUpdate = ( entries: IntersectionObserverEntry[] ) : void => { | |
for ( var entry of entries ) { | |
var lazyTarget = this.targets.get( entry.target ); | |
( lazyTarget ) && lazyTarget.updateVisibility( | |
entry.isIntersecting, | |
entry.intersectionRatio | |
); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { NgModule } from "@angular/core"; | |
// Import the application components and services. | |
import { LazySrcDirective } from "./lazy-src.directive.ts"; | |
import { LazyViewport } from "./lazy-viewport.ts"; | |
import { LazyViewportDirective } from "./lazy-viewport.directive.ts"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@NgModule({ | |
declarations: [ | |
LazySrcDirective, | |
LazyViewportDirective | |
], | |
exports: [ | |
LazySrcDirective, | |
LazyViewportDirective | |
], | |
providers: [ | |
// Setup the default LazyViewport instance without an associated element. This | |
// will create a IntersectionObserver that uses the browser's viewport as the | |
// observer root. This way, an instance of LazyViewport is always available for | |
// injection into other directives and services. | |
// -- | |
// NOTE: This service will be overridden at lower-levels in the component tree | |
// whenever a [lazyViewport] directive is applied. | |
{ | |
provide: LazyViewport, | |
useFactory: function() { | |
var viewport = new LazyViewport(); | |
viewport.setup( /* No root. */ ); | |
return( viewport ); | |
} | |
} | |
] | |
}) | |
export class LazyModule { | |
// ... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment