Skip to content

Instantly share code, notes, and snippets.

@allenhwkim
Created April 16, 2018 17:13
Show Gist options
  • Select an option

  • Save allenhwkim/ff1bcdfac6d08ef5ff8aab3656239b50 to your computer and use it in GitHub Desktop.

Select an option

Save allenhwkim/ff1bcdfac6d08ef5ff8aab3656239b50 to your computer and use it in GitHub Desktop.
<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>
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);
}
}
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 { }
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;
}
}
}
/**
* 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;
}
}
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;
}
}
}
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