fix(virtual-scroll): fixes dynamic changes

This commit is contained in:
Manu MA
2018-12-19 00:27:04 +01:00
committed by GitHub
parent 320eb03168
commit d1cecf142b
11 changed files with 234 additions and 99 deletions

View File

@ -12,7 +12,7 @@ export { IonRouterOutlet } from './navigation/ion-router-outlet';
export { RouterLinkDelegate } from './navigation/router-link-delegate';
export { NavParams } from './navigation/nav-params';
export { VirtualScroll } from './virtual-scroll/virtual-scroll';
export { IonVirtualScroll } from './virtual-scroll/virtual-scroll';
export { VirtualItem } from './virtual-scroll/virtual-item';
export { VirtualHeader } from './virtual-scroll/virtual-header';
export { VirtualFooter } from './virtual-scroll/virtual-footer';

View File

@ -1,41 +1,131 @@
import { ContentChild, Directive, ElementRef, EmbeddedViewRef, NgZone } from '@angular/core';
import { Cell, CellType } from '@ionic/core';
import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EmbeddedViewRef, Input, IterableDiffer, IterableDiffers, NgZone, SimpleChanges, TrackByFunction } from '@angular/core';
import { Cell, CellType, HeaderFn, ItemHeightFn } from '@ionic/core';
import { proxyInputs } from '../proxies';
import { proxyInputs, proxyMethods } from '../proxies';
import { VirtualFooter } from './virtual-footer';
import { VirtualHeader } from './virtual-header';
import { VirtualItem } from './virtual-item';
import { VirtualContext } from './virtual-utils';
@Directive({
selector: 'ion-virtual-scroll',
inputs: [
'approxItemHeight',
'approxHeaderHeight',
'approxFooterHeight',
'headerFn',
'footerFn',
'items',
'itemHeight'
]
})
export class VirtualScroll {
export declare interface IonVirtualScroll {
/**
* This method marks the tail the items array as dirty, so they can be re-rendered. It's equivalent to calling: ```js * virtualScroll.checkRange(lastItemLen, items.length - lastItemLen); * ```
*/
'checkEnd': () => void;
/**
* This method marks a subset of items as dirty, so they can be re-rendered. Items should be marked as dirty any time the content or their style changes. The subset of items to be updated can are specifing by an offset and a length.
*/
'checkRange': (offset: number, len?: number) => void;
/**
* Returns the position of the virtual item at the given index.
*/
'positionForItem': (index: number) => Promise<number>;
}
@Component({
selector: 'ion-virtual-scroll',
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IonVirtualScroll {
private differ?: IterableDiffer<any>;
private nativeEl: HTMLIonVirtualScrollElement;
private refMap = new WeakMap<HTMLElement, EmbeddedViewRef<VirtualContext>> ();
@ContentChild(VirtualItem) itmTmp!: VirtualItem;
@ContentChild(VirtualHeader) hdrTmp!: VirtualHeader;
@ContentChild(VirtualFooter) ftrTmp!: VirtualFooter;
constructor(
private el: ElementRef,
private zone: NgZone,
) {
const nativeEl = el.nativeElement as HTMLIonVirtualScrollElement;
nativeEl.nodeRender = this.nodeRender.bind(this);
/**
* It is important to provide this
* if virtual item height will be significantly larger than the default
* The approximate height of each virtual item template's cell.
* This dimension is used to help determine how many cells should
* be created when initialized, and to help calculate the height of
* the scrollable area. This height value can only use `px` units.
* Note that the actual rendered size of each cell comes from the
* app's CSS, whereas this approximation is used to help calculate
* initial dimensions before the item has been rendered.
*/
@Input() approxItemHeight: number;
proxyInputs(this, this.el.nativeElement, [
/**
* The approximate height of each header template's cell.
* This dimension is used to help determine how many cells should
* be created when initialized, and to help calculate the height of
* the scrollable area. This height value can only use `px` units.
* Note that the actual rendered size of each cell comes from the
* app's CSS, whereas this approximation is used to help calculate
* initial dimensions before the item has been rendered.
*/
@Input() approxHeaderHeight: number;
/**
* The approximate width of each footer template's cell.
* This dimension is used to help determine how many cells should
* be created when initialized, and to help calculate the height of
* the scrollable area. This height value can only use `px` units.
* Note that the actual rendered size of each cell comes from the
* app's CSS, whereas this approximation is used to help calculate
* initial dimensions before the item has been rendered.
*/
@Input() approxFooterHeight: number;
/**
* Section headers and the data used within its given
* template can be dynamically created by passing a function to `headerFn`.
* For example, a large list of contacts usually has dividers between each
* letter in the alphabet. App's can provide their own custom `headerFn`
* which is called with each record within the dataset. The logic within
* the header function can decide if the header template should be used,
* and what data to give to the header template. The function must return
* `null` if a header cell shouldn't be created.
*/
@Input() headerFn?: HeaderFn;
/**
* Section footers and the data used within its given
* template can be dynamically created by passing a function to `footerFn`.
* The logic within the footer function can decide if the footer template
* should be used, and what data to give to the footer template. The function
* must return `null` if a footer cell shouldn't be created.
*/
@Input() footerFn?: HeaderFn;
/**
* The data that builds the templates within the virtual scroll.
* It's important to note that when this data has changed, then the
* entire virtual scroll is reset, which is an expensive operation and
* should be avoided if possible.
*/
@Input() items?: any[];
/**
* An optional function that maps each item within their height.
* When this function is provides, heavy optimizations and fast path can be taked by
* `ion-virtual-scroll` leading to massive performance improvements.
*
* This function allows to skip all DOM reads, which can be Doing so leads
* to massive performance
*/
@Input() itemHeight?: ItemHeightFn;
/**
* Same as `ngForTrackBy` which can be used on `ngFor`.
*/
@Input() trackBy: TrackByFunction<any>;
constructor(
private zone: NgZone,
private iterableDiffers: IterableDiffers,
elementRef: ElementRef,
) {
this.nativeEl = elementRef.nativeElement as HTMLIonVirtualScrollElement;
this.nativeEl.nodeRender = this.nodeRender.bind(this);
proxyInputs(this, this.nativeEl, [
'approxItemHeight',
'approxHeaderHeight',
'approxFooterHeight',
@ -44,24 +134,57 @@ export class VirtualScroll {
'items',
'itemHeight'
]);
proxyMethods(this, this.nativeEl, [
'checkEnd',
'checkRange',
'positionForItem'
]);
}
ngOnChanges(changes: SimpleChanges): void {
if (this.trackBy && 'items' in changes) {
// React on virtualScroll changes only once all inputs have been initialized
const value = changes['items'].currentValue;
if (this.differ === undefined && value != null) {
try {
this.differ = this.iterableDiffers.find(value).create(this.trackBy);
} catch (e) {
throw new Error(
`Cannot find a differ supporting object '${value}'. VirtualScroll only supports binding to Iterables such as Arrays.`);
}
}
}
}
ngDoCheck() {
// and if there actually are changes
const changes = this.differ !== undefined && this.items ? this.differ.diff(this.items) : null;
if (changes === null) {
return;
}
// TODO: optimize
this.checkRange(0);
}
private nodeRender(el: HTMLElement | null, cell: Cell, index: number): HTMLElement {
return this.zone.run(() => {
let node: EmbeddedViewRef<VirtualContext>;
if (!el) {
const view = this.itmTmp.viewContainer.createEmbeddedView(
node = this.itmTmp.viewContainer.createEmbeddedView(
this.getComponent(cell.type),
{ $implicit: null, index },
{ $implicit: cell.value, index },
index
);
el = getElement(view);
this.refMap.set(el, view);
el = getElement(node);
this.refMap.set(el, node);
} else {
node = this.refMap.get(el)!;
const ctx = node.context;
ctx.$implicit = cell.value;
ctx.index = cell.index;
}
const node = this.refMap.get(el)!;
const ctx = node.context as VirtualContext;
ctx.$implicit = cell.value;
ctx.index = cell.index;
node.markForCheck();
// run sync change detections
node.detectChanges();
return el;
});
}

View File

@ -104,7 +104,7 @@ const DECLARATIONS = [
c.VirtualFooter,
c.VirtualHeader,
c.VirtualItem,
c.VirtualScroll
c.IonVirtualScroll
];
const PROVIDERS = [

View File

@ -4,18 +4,26 @@
<ion-title>
Virtual Scroll Test
</ion-title>
<ion-buttons slot="end">
<ion-button (click)="addItems()">
<ion-icon name="add" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-virtual-scroll [items]="items" [headerFn]="myHeaderFn" [footerFn]="myFooterFn">
<ion-item-divider *virtualHeader="let header">{{ header }}</ion-item-divider>
<ion-item-divider *virtualFooter="let footer">-- {{ footer }}</ion-item-divider>
<!-- <ion-item *virtualItem="let item" itemHeight="itemHeight">
{{item.name}}
</ion-item> -->
<ion-item *virtualItem="let item" [routerLink]="['/', 'virtual-scroll-detail', item]">
<ion-label>
<app-virtual-scroll-inner [value]="item"></app-virtual-scroll-inner>
<app-virtual-scroll-inner [value]="item.name"></app-virtual-scroll-inner>
</ion-label>
<ion-icon *ngIf="(item % 2) === 0" name="airplane" slot="start"></ion-icon>
<ion-toggle slot="end" [checked]="(item % 2) === 1"></ion-toggle>
<ion-icon *ngIf="(item.name % 2) === 0" name="airplane" slot="start"></ion-icon>
<ion-toggle slot="end" [(ngModel)]="item.checked"></ion-toggle>
</ion-item>
</ion-virtual-scroll>
</ion-content>

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { HeaderFn } from '@ionic/core';
import { IonVirtualScroll } from '@ionic/angular';
@Component({
selector: 'app-virtual-scroll',
@ -7,7 +8,11 @@ import { HeaderFn } from '@ionic/core';
})
export class VirtualScrollComponent {
items = Array.from({length: 1000}, (_, i) => i);
@ViewChild(IonVirtualScroll) virtualScroll: IonVirtualScroll;
items = Array.from({length: 100}, (_, i) => ({ name: `${i}`, checked: true}));
itemHeight = () => 44;
myHeaderFn: HeaderFn = (_, index) => {
if ((index % 10) === 0) {
@ -20,4 +25,12 @@ export class VirtualScrollComponent {
return `Footer ${index}`;
}
}
addItems() {
console.log('adding items');
this.items.push(
{ name: `New Item`, checked: true}
);
this.virtualScroll.checkEnd();
}
}