Created
April 16, 2018 17:13
-
-
Save allenhwkim/ff1bcdfac6d08ef5ff8aab3656239b50 to your computer and use it in GitHub Desktop.
Angular Virtual List https://stackblitz.com/edit/ngui-in-view-virtual-list
This file contains hidden or 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
| <div class="pages"> | |
| <h2> Virtual List </h2> | |
| <hr/> | |
| <div #pages></div> | |
| <!-- | |
| the following will add <dyn-page></dyn-page> into <div #pages></div> | |
| When it's added, it will be pushed down out of view. | |
| User scrolls down, in view, then it will add dyn-page again | |
| <dyn-page> listens to (inView) and (outView) event, | |
| then it empties the contents when out of view, | |
| and restores it back when it comes into view again | |
| --> | |
| <ngui-in-view (inView)="addPage($event, pageTemplate)"></ngui-in-view> | |
| </div> | |
| <div class="num-dom-elements"> | |
| Total Number of DOM elements: {{numDomElements}} | |
| </div> | |
| <ng-template #pageTemplate let-items="items"> | |
| <div *ngIf="items else loading"> | |
| <div *ngFor="let num of items">row number: {{num}}</div> | |
| </div> | |
| <ng-template #loading>Loading</ng-template> | |
| </ng-template> |
This file contains hidden or 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 { Component, ElementRef, ViewChild, TemplateRef, ViewContainerRef } from '@angular/core'; | |
| import {Observable} from 'rxjs/Observable'; | |
| import 'rxjs/add/observable/of'; | |
| import 'rxjs/add/operator/delay'; | |
| import { DynamicComponentService } from './dynamic.component.service'; | |
| import { DynPageComponent } from './dyn-page.component'; | |
| @Component({ | |
| selector: 'my-app', | |
| templateUrl: './app.component.html', | |
| styles: [ ` | |
| .num-dom-elements { | |
| position: fixed; padding: 5px; | |
| bottom: 0; right: 0; background: #333;color: #fff; | |
| }`] | |
| }) | |
| export class AppComponent { | |
| /** prevents loading the same page repeteadely */ | |
| pageLoading: boolean; | |
| /** page control variable */ | |
| lastPage = 0; | |
| get numDomElements(): number { | |
| console.log(this.element.nativeElement); | |
| return this.element.nativeElement.querySelectorAll('*').length; | |
| } | |
| /** <dyn-page> will be added as children of #pages */ | |
| @ViewChild('pages', {read:ViewContainerRef}) vcr: ViewContainerRef; | |
| constructor( | |
| public element: ElementRef, | |
| public dcs: DynamicComponentService | |
| ) {} | |
| addPage(entry, template) { | |
| if (!this.pageLoading) { | |
| this.pageLoading = true; | |
| let compRef = this.dcs.createComponent(DynPageComponent, this.vcr); | |
| let dynPageComp = compRef.instance; | |
| dynPageComp.template = template; //use custom template | |
| dynPageComp.page = this.lastPage++; | |
| this.dcs.insertComponent(compRef); | |
| this.loadItems(dynPageComp.page, 50).delay(1000).subscribe(items => { | |
| dynPageComp.items = items; | |
| this.pageLoading = false; | |
| }); | |
| } | |
| } | |
| loadItems(pageNum, limit) { | |
| let items = Array.from(Array(50), (_,x) => (pageNum * limit) + x); | |
| return Observable.of(items); | |
| } | |
| } |
This file contains hidden or 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 { NgModule } from '@angular/core'; | |
| import { BrowserModule } from '@angular/platform-browser'; | |
| import { FormsModule } from '@angular/forms'; | |
| import { AppComponent } from './app.component'; | |
| import { NguiInViewComponent } from './ngui-in-view.component'; | |
| import { NguiInViewDirective } from './ngui-in-view.directive'; | |
| import { DynPageComponent } from './dyn-page.component'; | |
| import { DynamicComponentService } from './dynamic.component.service'; | |
| @NgModule({ | |
| imports: [ BrowserModule, FormsModule ], | |
| declarations: [ AppComponent, NguiInViewComponent, NguiInViewDirective, DynPageComponent ], | |
| entryComponents: [ DynPageComponent ], | |
| providers: [ DynamicComponentService ], | |
| bootstrap: [ AppComponent ] | |
| }) | |
| export class AppModule { } |
This file contains hidden or 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 { | |
| Component, | |
| ElementRef, | |
| Input, | |
| Renderer2, | |
| TemplateRef | |
| } from '@angular/core'; | |
| /** | |
| * this will listen to inView and outView events, | |
| * so that it empties contents when out of view after backup items | |
| * and restores the contents when in view | |
| */ | |
| @Component({ | |
| selector: 'dyn-page', | |
| template: ` | |
| <div class="dyn-page" | |
| (nguiInView)="restoreItems()" | |
| (nguiOutView)="emptyItems()"> | |
| <ng-container | |
| [ngTemplateOutlet]="template||defaultTemplate" | |
| [ngTemplateOutletContext]="{items: items, outView: outView}"> | |
| </ng-container> | |
| <div *ngIf="outView">{{this.itemsBackup.length}} items hidden</div> | |
| </div> | |
| <ng-template #defaultTemplate> | |
| <div *ngIf="items else loading"> | |
| Error: [template] is not given. | |
| </div> | |
| <ng-template #loading>Loading...</ng-template> | |
| </ng-template> | |
| `, | |
| styles: [`:root {display: block}`]}) | |
| export class DynPageComponent { | |
| @Input('template') template: TemplateRef<any>; | |
| @Input('items') items; | |
| page: number = 0; | |
| limit: number = 50; | |
| start: any; | |
| end: any; | |
| outView: boolean = false; | |
| itemsBackup: any[] = []; | |
| constructor(public element: ElementRef,public renderer: Renderer2) {} | |
| restoreItems() { | |
| if (this.outView) { | |
| this.outView = false; | |
| this.items = Array.from(this.itemsBackup || []); | |
| this.itemsBackup = undefined; | |
| } | |
| } | |
| emptyItems() { | |
| if (this.items && !this.outView) { | |
| // set height before emptying contents | |
| let height = this.element.nativeElement.getBoundingClientRect().height; | |
| console.log('height', this.element.nativeElement, height); | |
| this.renderer.setStyle(this.element.nativeElement, 'height', height+'px'); | |
| this.outView = true; | |
| this.itemsBackup = Array.from(this.items||[]); | |
| this.items = undefined; | |
| } | |
| } | |
| } | |
This file contains hidden or 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
| /** | |
| * Example | |
| * | |
| * import { DynamicComponentService } from './dynamic.component.service'; | |
| * import { MyDynamicComponent } from './my-1.component'; | |
| * | |
| * @Component({ | |
| * template: ` ... <div #dymamic></div>` | |
| * }) | |
| * export class MyComponent { | |
| * @ViewChild('dynamic', {read:ViewContainerRef}) vcr: ViewContainerRef; | |
| * | |
| * constructor(public dcs: DynamicComponentService) {} | |
| * | |
| * insertComp() { | |
| * let compRef = this.dcs.createComponent(MyDynamicComponent, this.vcr); | |
| * ths.dcs.insertComonent(cmpRef); | |
| * compRef.instance.items = [1,2,3]; // dealing with @input | |
| * compRef.instance.output$.subscribe(val => {}); // dealing with @output | |
| * } | |
| * } | |
| */ | |
| import { | |
| Component, | |
| ComponentRef, | |
| ComponentFactoryResolver, | |
| Inject, | |
| Injectable, | |
| ReflectiveInjector, | |
| ViewContainerRef | |
| } from '@angular/core'; | |
| @Injectable() | |
| export class DynamicComponentService { | |
| factoryResolver: ComponentFactoryResolver; | |
| rootViewContainer: ViewContainerRef; | |
| constructor(@Inject(ComponentFactoryResolver) factoryResolver) { | |
| this.factoryResolver = factoryResolver; | |
| } | |
| // returns component reference | |
| // The reason to seperate `createCompnent` and `insertComponent` is | |
| // to allow some actions before we insert into a hostView. | |
| // e.g styling, setting attributes, etc | |
| createComponent(component: any, into?: ViewContainerRef): ComponentRef<any> { | |
| this.rootViewContainer = into || this.rootViewContainer; | |
| const factory = this.factoryResolver.resolveComponentFactory(component); | |
| return factory.create(this.rootViewContainer.parentInjector); | |
| } | |
| // insert component | |
| insertComponent(componentRef: ComponentRef<any>): Component { | |
| const compId = `dyn-comp-${Math.floor(Math.random() * 10 ** 7) + 10 ** 6}`; | |
| componentRef.location.nativeElement.id = compId; | |
| this.rootViewContainer.insert(componentRef.hostView); | |
| return componentRef.instance; | |
| } | |
| } |
This file contains hidden or 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 { | |
| Component, | |
| ElementRef, | |
| EventEmitter, | |
| Inject, | |
| Input, | |
| OnDestroy, | |
| OnInit, | |
| Output, | |
| PLATFORM_ID, | |
| Renderer2, | |
| ViewChild | |
| } from '@angular/core'; | |
| import { isPlatformBrowser } from '@angular/common'; | |
| import { konsole } from './konsole'; | |
| @Component({ | |
| selector: 'ngui-in-view', | |
| template: ` | |
| <ng-content *ngIf="showContents"></ng-content> | |
| `, | |
| styles: [':root {display: block;}'] | |
| }) | |
| export class NguiInViewComponent implements OnInit, OnDestroy { | |
| observer: IntersectionObserver; | |
| showContents: boolean = false; | |
| @Input() options: any = {threshold: [0.1, 0.2, 0.3, 0.4, 0.5]}; | |
| @Output('inView') inView$: EventEmitter<any> = new EventEmitter(); | |
| @Output('notInView') notInView$: EventEmitter<any> = new EventEmitter(); | |
| constructor( | |
| public element: ElementRef, | |
| public renderer: Renderer2, | |
| @Inject(PLATFORM_ID) private platformId: any) { | |
| } | |
| ngOnInit(): void { | |
| if (isPlatformBrowser(this.platformId)) { | |
| this.observer = new IntersectionObserver(this.handleIntersect.bind(this), this.options); | |
| this.observer.observe(this.element.nativeElement); | |
| } | |
| } | |
| ngOnDestroy(): void { | |
| if (isPlatformBrowser(this.platformId)) { | |
| konsole.log('destroying ngui-in-view, disconnecting'); | |
| this.observer.disconnect(); | |
| } | |
| } | |
| handleIntersect(entries, observer): void { | |
| entries.forEach((entry: IntersectionObserverEntry) => { | |
| if (entry.isIntersecting) { | |
| konsole.log('element in view, emitting inView'); | |
| this.showContents = true; | |
| if (this.inView$.observers.length === 0) { | |
| this.defaultInViewHandler(entry); | |
| } | |
| this.inView$.emit(entry); | |
| } else { | |
| konsole.log('element not in view, emitting notInView'); | |
| this.notInView$.emit(entry); | |
| } | |
| }); | |
| } | |
| defaultInViewHandler(entry) { | |
| if (entry.intersectionRatio < 0.5) { | |
| entry.target.style.opacity = entry.intersectionRatio * (1/0.5); | |
| } else { | |
| entry.target.style.opacity = 1; | |
| } | |
| } | |
| } |
This file contains hidden or 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 { | |
| Directive, | |
| ElementRef, | |
| EventEmitter, | |
| Inject, | |
| Input, | |
| OnDestroy, | |
| OnInit, | |
| Output, | |
| PLATFORM_ID, | |
| Renderer2, | |
| ViewChild | |
| } from '@angular/core'; | |
| import { isPlatformBrowser } from '@angular/common'; | |
| import { konsole } from './konsole'; | |
| @Directive({ | |
| selector: '[nguiInView], [nguiOutView]' | |
| }) | |
| export class NguiInViewDirective implements OnInit, OnDestroy { | |
| observer: IntersectionObserver; | |
| @Input() options: any = {}; | |
| @Output('nguiInView') inView$: EventEmitter<any> = new EventEmitter(); | |
| @Output('nguiOutView') outView$: EventEmitter<any> = new EventEmitter(); | |
| constructor( | |
| public element: ElementRef, | |
| public renderer: Renderer2, | |
| @Inject(PLATFORM_ID) private platformId: any) { | |
| } | |
| ngOnInit(): void { | |
| if (isPlatformBrowser(this.platformId)) { | |
| this.observer = new IntersectionObserver(this.handleIntersect.bind(this), this.options); | |
| this.observer.observe(this.element.nativeElement); | |
| } | |
| } | |
| ngOnDestroy(): void { | |
| if (isPlatformBrowser(this.platformId)) { | |
| konsole.log('destroying ngui-in-view, disconnecting'); | |
| this.observer.disconnect(); | |
| } | |
| } | |
| handleIntersect(entries, observer): void { | |
| entries.forEach((entry: IntersectionObserverEntry) => { | |
| if (entry.isIntersecting) { | |
| this.inView$.emit(entry); | |
| } else { | |
| this.outView$.emit(entry); | |
| } | |
| }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment