UNPKG

lzy-load

Version:

LzyLoad is an Angular Lib for loading content on demand

342 lines (334 loc) 14.7 kB
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