lzy-load
Version:
LzyLoad is an Angular Lib for loading content on demand
342 lines (334 loc) • 14.7 kB
JavaScript
import { Directive, ElementRef, Renderer2, Input, Component, ContentChildren, HostListener, NgModule } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/internal/operators';
const LL_PRO_NAME = 'lzyLoadItemPos';
class LzyLoadItemDirective {
constructor(hostElement, renderer) {
this.hostElement = hostElement;
this.renderer = renderer;
// Field that represent the current state of the observed element
this.isVisible = false;
// Field to know if the content should be loaded
this.loadContent = false;
}
set pos(value) {
if (value !== null && value !== undefined) {
this._pos = value;
// Marking the html element with the position that it is on the array so is easier to access it in the elements array
this.renderer.setProperty(this.hostElement.nativeElement, LL_PRO_NAME, this._pos);
}
}
get pos() {
return this._pos;
}
}
LzyLoadItemDirective.decorators = [
{ type: Directive, args: [{
selector: '[lzyLoadItem]',
exportAs: 'lzyLoadItem'
},] }
];
LzyLoadItemDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: Renderer2 }
];
LzyLoadItemDirective.propDecorators = {
pos: [{ type: Input }]
};
var LoadStrategy;
(function (LoadStrategy) {
// Loading the elements when they get visible
LoadStrategy[LoadStrategy["OnVisible"] = 0] = "OnVisible";
/* Using an number of loaded elements. The number is gonna be calculated
as a percent of the number of visible elements in the view */
LoadStrategy[LoadStrategy["PctOfVisibleElem"] = 1] = "PctOfVisibleElem";
/* Loading a fixed lenght of element in advance plus the visible items in the viewport */
LoadStrategy[LoadStrategy["AheadOfVisElem"] = 2] = "AheadOfVisElem";
/* Using an number of loaded elements. Is calculated using as reference
the total number of elements to display. Can't be greater thant the lenght of list*/
LoadStrategy[LoadStrategy["PctOfTotElem"] = 3] = "PctOfTotElem";
})(LoadStrategy || (LoadStrategy = {}));
var UnloadStrategy;
(function (UnloadStrategy) {
/* Don't temove the elements once they have been loaded */
UnloadStrategy[UnloadStrategy["KeepLoaded"] = 0] = "KeepLoaded";
/* Unload the elements when they got out of the loaded elements' range in the view. This mode is gonna depend of the
selected LoadingMode */
UnloadStrategy[UnloadStrategy["LeavLoadedRange"] = 1] = "LeavLoadedRange";
})(UnloadStrategy || (UnloadStrategy = {}));
/*
* Important note: Is required that the items marked as lazy-load-items using the "LazyLoadItemDirective"
* are present in the template at the moment of the initialization of the component because the
* ContentChildren query is evaluated just before the AfterContentInit life circle,
* if the items are being added dynamically later to the template then is possible that the parent of this component needs to run manualy
* change detection to notify this component that the template has changed. Example of this case is in the cases-table in
* gallery-management
*/
class LzyLoadContComponent {
constructor(renderer, hostElement) {
this.renderer = renderer;
this.hostElement = hostElement;
// Component fields
this.init = false;
this.loadingRangeAmount = 0;
this.preloadAmo = 0;
this.scrollEvent = new Subject();
this.onResizeEvent = new Subject();
// new properties
this.numOfVisiElem = 0;
this.loadedRange = [];
this.settings = {
loadStrategy: LoadStrategy.PctOfVisibleElem,
unloadStrategy: UnloadStrategy.KeepLoaded,
percent: 2
};
// Subscription that is gonna handle the unsubscribe to all the other subscriptions
this.subscription = new Subscription();
}
ngOnInit() {
this.registerIO();
this.initScroll();
this.initScreenResizing();
}
registerIO() {
// Initializing the IO options
this.IOOptions = {
root: null,
rootMargin: '0px',
threshold: [0.1]
};
// If the container was passed then use it as a root for the IO
if (this.container) {
this.IOOptions.root = this.container;
}
else {
// If the container input was not provided then use the self component as a container
this.container = this.hostElement.nativeElement;
this.IOOptions.root = this.container;
}
}
initScroll() {
// Listening for scroll event in the container
this.renderer.listen(this.container, 'scroll', () => {
this.scrollEvent.next();
});
// subscribing to scroll events to preload the items that need to be
// preload in both directions up and down of the current position
// including 100 mil of debounce time
this.subscription.add(this.scrollEvent.pipe(debounceTime(100)).subscribe(() => {
this.updateLoadedElements();
}));
}
initScreenResizing() {
// Subscribing to the resize event and giving it a 300 miliseconds delay to prevent trigger this code too offten
this.subscription.add(this.onResizeEvent.pipe(debounceTime(300)).subscribe(() => {
// Updating the settings after changing the viewport
this.calSettings();
// resetting the component after the change in the layout
this.updateLoadedElements();
}));
}
// Used for initialize the component settings
initialize() {
this.initializeIntersectionObs();
}
resetAndRecalculateSettings() {
// Resetting the isReady variable to false that will trigger the initial calculations for the first
// execution of the IO callback
this.init = false;
// Disconnecting from the observer and its targets
if (this.observer) {
this.observer.disconnect();
}
// Initializing again the observer with the current elements
this.initializeIntersectionObs();
}
ngAfterContentInit() {
// Listening for changes in the elements
this.subscription.add(this.itemsIO.changes.subscribe(() => {
this.resetAndRecalculateSettings();
}));
// Initializing if are items to observe for
if (this.itemsIO.length > 0) {
this.initialize();
}
}
// Calculate preload amounts
calSettings() {
switch (this.settings.loadStrategy) {
case LoadStrategy.AheadOfVisElem: {
this.loadingRangeAmount = Math.ceil(this.settings.number + this.numOfVisiElem);
// Calculating the amount of items to be load in both directions
this.preloadAmo = this.settings.number;
break;
}
case LoadStrategy.PctOfVisibleElem: {
// Calculating the amount of preload elements depending of the amount of visible items in the view
this.loadingRangeAmount = Math.ceil(this.settings.factor * this.numOfVisiElem);
// Calculating the amount of items to be load in both directions
this.preloadAmo = Math.ceil((this.loadingRangeAmount - this.numOfVisiElem) / 2);
break;
}
case LoadStrategy.OnVisible: {
this.loadingRangeAmount = this.numOfVisiElem;
this.preloadAmo = 0;
break;
}
case LoadStrategy.PctOfTotElem: {
// Calculating the amount of preload elements depending of the amount of visible items in the view
this.loadingRangeAmount = Math.ceil(this.settings.percent * this.itemsIO.length);
// Calculating the amount of items to be load in both directions
this.preloadAmo = Math.ceil((this.loadingRangeAmount - this.numOfVisiElem) / 2);
break;
}
}
}
// loading the content
inLoadingRange(element) {
element.loadContent = true;
}
// unloading the content
outOfLoadingRange(element) {
if (this.settings.unloadStrategy === UnloadStrategy.LeavLoadedRange) {
element.loadContent = false;
}
}
// Calculate the amount of visible elements that are in the exact moment and update the property in the component
getVisibleItems() {
this.numOfVisiElem = this.itemsIO.filter((item) => {
return item.isVisible;
}).length;
return this.numOfVisiElem;
}
// Initialize the IO and the targets to observe
initializeIntersectionObs() {
// callback provided to the IO
const ioCallBack = (entries) => {
const updEntriesVisibility = () => {
entries.forEach((entry) => {
// when an element is intersecting the view
const itemElement = this.itemsIO.toArray()[entry.target[LL_PRO_NAME]];
if (itemElement) {
itemElement.isVisible = entry.isIntersecting;
}
});
};
// differentiating between the first call to the function and the rest. The first is always gonna be
// the initialization of all the targets
if (!this.init) {
// Updating the visibility of each element
updEntriesVisibility();
// Calculating the amount of visible items and updating the property "visibleItemsAmo" for first time
this.getVisibleItems();
// calculating amount of visible items depending if the selected model for the component needs it
this.calSettings();
// Preloading the items
this.updateLoadedElements();
this.init = true;
}
else {
// This code will be executed always after the first time
// Updating the visibility of each element
updEntriesVisibility();
}
};
// Creating the observer
this.observer = new IntersectionObserver(ioCallBack, this.IOOptions);
// Registering the items with the observer
this.itemsIO.forEach((item) => {
this.observer.observe(item.hostElement.nativeElement);
});
}
// calculate the new range of elements in the list that should be preload
calLoadedRange(visibleItems) {
const newRange = [0, 0];
// getting the range of current visible items
const firstVisibleItem = visibleItems[0];
const lastVisibleItem = visibleItems[visibleItems.length - 1];
// Last item position in the array of items
const lastItemPosInArray = (this.itemsIO.length - 1);
// Setting the new start position to the current position menus the preload amount divided by two
// in order to preload forward and in backward
newRange[0] = Math.max(0, (firstVisibleItem.pos - this.preloadAmo));
if (newRange[0] === 0) {
// prefetching the elements ahead of the last visible item
newRange[1] = Math.min(this.loadingRangeAmount, lastItemPosInArray);
}
else if ((lastVisibleItem.pos + this.preloadAmo) >= lastItemPosInArray) {
// pre-fetching the elements ahead and backward of the range of visible items
newRange[1] = lastItemPosInArray;
newRange[0] = Math.max(0, lastItemPosInArray - this.loadingRangeAmount);
}
else {
newRange[1] = (lastVisibleItem.pos + this.preloadAmo);
}
return newRange;
}
// Function that calculate the current visible items and the ones that need to be preload
updateLoadedElements() {
// Getting the current visible elements in the container
const visibleItems = this.itemsIO.filter((item) => {
return item.isVisible;
});
// if are not visible items, then do nothing
if (visibleItems.length > 0) {
// Calculating the elements should be preload
this.loadedRange = this.calLoadedRange(visibleItems);
this.itemsIO.forEach((item, index) => {
if (index >= this.loadedRange[0] && index <= this.loadedRange[1]) {
// element within the loading range
this.inLoadingRange(item);
}
else {
// element out the loading range
this.outOfLoadingRange(item);
}
});
}
}
// Listening to the resize event so we can recalculate the amount of visible items in the list and other
// important properties that are use for the component
onResize() {
this.onResizeEvent.next();
}
ngOnDestroy() {
if (this.observer) {
this.observer.disconnect();
}
this.subscription.unsubscribe();
}
}
LzyLoadContComponent.decorators = [
{ type: Component, args: [{
selector: 'lzy-load-cont',
template: "<ng-content></ng-content>",
styles: [":host{background:transparent;border:inherit;color:inherit;display:flex;margin:0;padding:0}"]
},] }
];
LzyLoadContComponent.ctorParameters = () => [
{ type: Renderer2 },
{ type: ElementRef }
];
LzyLoadContComponent.propDecorators = {
settings: [{ type: Input }],
container: [{ type: Input }],
itemsIO: [{ type: ContentChildren, args: [LzyLoadItemDirective, { descendants: true },] }],
onResize: [{ type: HostListener, args: ['window:resize', [],] }]
};
class LzyLoadModule {
}
LzyLoadModule.decorators = [
{ type: NgModule, args: [{
declarations: [LzyLoadContComponent, LzyLoadItemDirective],
imports: [],
exports: [LzyLoadContComponent, LzyLoadItemDirective]
},] }
];
/*
* Public API Surface of lzy-load
*/
/**
* Generated bundle index. Do not edit.
*/
export { LL_PRO_NAME, LoadStrategy, LzyLoadContComponent, LzyLoadItemDirective, LzyLoadModule, UnloadStrategy };
//# sourceMappingURL=lzy-load.js.map