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 { RouterLinkDelegate } from './navigation/router-link-delegate';
export { NavParams } from './navigation/nav-params'; 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 { VirtualItem } from './virtual-scroll/virtual-item';
export { VirtualHeader } from './virtual-scroll/virtual-header'; export { VirtualHeader } from './virtual-scroll/virtual-header';
export { VirtualFooter } from './virtual-scroll/virtual-footer'; export { VirtualFooter } from './virtual-scroll/virtual-footer';

View File

@ -1,41 +1,131 @@
import { ContentChild, Directive, ElementRef, EmbeddedViewRef, NgZone } from '@angular/core'; import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EmbeddedViewRef, Input, IterableDiffer, IterableDiffers, NgZone, SimpleChanges, TrackByFunction } from '@angular/core';
import { Cell, CellType } from '@ionic/core'; import { Cell, CellType, HeaderFn, ItemHeightFn } from '@ionic/core';
import { proxyInputs } from '../proxies'; import { proxyInputs, proxyMethods } from '../proxies';
import { VirtualFooter } from './virtual-footer'; import { VirtualFooter } from './virtual-footer';
import { VirtualHeader } from './virtual-header'; import { VirtualHeader } from './virtual-header';
import { VirtualItem } from './virtual-item'; import { VirtualItem } from './virtual-item';
import { VirtualContext } from './virtual-utils'; import { VirtualContext } from './virtual-utils';
@Directive({ export declare interface IonVirtualScroll {
selector: 'ion-virtual-scroll', /**
inputs: [ * 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); * ```
'approxItemHeight', */
'approxHeaderHeight', 'checkEnd': () => void;
'approxFooterHeight', /**
'headerFn', * 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.
'footerFn', */
'items', 'checkRange': (offset: number, len?: number) => void;
'itemHeight' /**
] * Returns the position of the virtual item at the given index.
}) */
export class VirtualScroll { '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>> (); private refMap = new WeakMap<HTMLElement, EmbeddedViewRef<VirtualContext>> ();
@ContentChild(VirtualItem) itmTmp!: VirtualItem; @ContentChild(VirtualItem) itmTmp!: VirtualItem;
@ContentChild(VirtualHeader) hdrTmp!: VirtualHeader; @ContentChild(VirtualHeader) hdrTmp!: VirtualHeader;
@ContentChild(VirtualFooter) ftrTmp!: VirtualFooter; @ContentChild(VirtualFooter) ftrTmp!: VirtualFooter;
constructor( /**
private el: ElementRef, * It is important to provide this
private zone: NgZone, * if virtual item height will be significantly larger than the default
) { * The approximate height of each virtual item template's cell.
const nativeEl = el.nativeElement as HTMLIonVirtualScrollElement; * This dimension is used to help determine how many cells should
nativeEl.nodeRender = this.nodeRender.bind(this); * 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', 'approxItemHeight',
'approxHeaderHeight', 'approxHeaderHeight',
'approxFooterHeight', 'approxFooterHeight',
@ -44,24 +134,57 @@ export class VirtualScroll {
'items', 'items',
'itemHeight' '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 { private nodeRender(el: HTMLElement | null, cell: Cell, index: number): HTMLElement {
return this.zone.run(() => { return this.zone.run(() => {
let node: EmbeddedViewRef<VirtualContext>;
if (!el) { if (!el) {
const view = this.itmTmp.viewContainer.createEmbeddedView( node = this.itmTmp.viewContainer.createEmbeddedView(
this.getComponent(cell.type), this.getComponent(cell.type),
{ $implicit: null, index }, { $implicit: cell.value, index },
index index
); );
el = getElement(view); el = getElement(node);
this.refMap.set(el, view); 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)!; // run sync change detections
const ctx = node.context as VirtualContext; node.detectChanges();
ctx.$implicit = cell.value;
ctx.index = cell.index;
node.markForCheck();
return el; return el;
}); });
} }

View File

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

View File

@ -4,18 +4,26 @@
<ion-title> <ion-title>
Virtual Scroll Test Virtual Scroll Test
</ion-title> </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-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-virtual-scroll [items]="items" [headerFn]="myHeaderFn" [footerFn]="myFooterFn"> <ion-virtual-scroll [items]="items" [headerFn]="myHeaderFn" [footerFn]="myFooterFn">
<ion-item-divider *virtualHeader="let header">{{ header }}</ion-item-divider> <ion-item-divider *virtualHeader="let header">{{ header }}</ion-item-divider>
<ion-item-divider *virtualFooter="let footer">-- {{ footer }}</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-item *virtualItem="let item" [routerLink]="['/', 'virtual-scroll-detail', item]">
<ion-label> <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-label>
<ion-icon *ngIf="(item % 2) === 0" name="airplane" slot="start"></ion-icon> <ion-icon *ngIf="(item.name % 2) === 0" name="airplane" slot="start"></ion-icon>
<ion-toggle slot="end" [checked]="(item % 2) === 1"></ion-toggle> <ion-toggle slot="end" [(ngModel)]="item.checked"></ion-toggle>
</ion-item> </ion-item>
</ion-virtual-scroll> </ion-virtual-scroll>
</ion-content> </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 { HeaderFn } from '@ionic/core';
import { IonVirtualScroll } from '@ionic/angular';
@Component({ @Component({
selector: 'app-virtual-scroll', selector: 'app-virtual-scroll',
@ -7,7 +8,11 @@ import { HeaderFn } from '@ionic/core';
}) })
export class VirtualScrollComponent { 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) => { myHeaderFn: HeaderFn = (_, index) => {
if ((index % 10) === 0) { if ((index % 10) === 0) {
@ -20,4 +25,12 @@ export class VirtualScrollComponent {
return `Footer ${index}`; return `Footer ${index}`;
} }
} }
addItems() {
console.log('adding items');
this.items.push(
{ name: `New Item`, checked: true}
);
this.virtualScroll.checkEnd();
}
} }

View File

@ -1220,8 +1220,8 @@ ion-toolbar,css-prop,--padding-start
ion-toolbar,css-prop,--padding-top ion-toolbar,css-prop,--padding-top
ion-virtual-scroll ion-virtual-scroll
ion-virtual-scroll,prop,approxFooterHeight,number,40,false,false ion-virtual-scroll,prop,approxFooterHeight,number,30,false,false
ion-virtual-scroll,prop,approxHeaderHeight,number,40,false,false ion-virtual-scroll,prop,approxHeaderHeight,number,30,false,false
ion-virtual-scroll,prop,approxItemHeight,number,45,false,false ion-virtual-scroll,prop,approxItemHeight,number,45,false,false
ion-virtual-scroll,prop,footerFn,((item: any, index: number, items: any[]) => string | null | undefined) | undefined,undefined,false,false ion-virtual-scroll,prop,footerFn,((item: any, index: number, items: any[]) => string | null | undefined) | undefined,undefined,false,false
ion-virtual-scroll,prop,headerFn,((item: any, index: number, items: any[]) => string | null | undefined) | undefined,undefined,false,false ion-virtual-scroll,prop,headerFn,((item: any, index: number, items: any[]) => string | null | undefined) | undefined,undefined,false,false
@ -1231,6 +1231,6 @@ ion-virtual-scroll,prop,nodeRender,((el: HTMLElement | null, cell: Cell, domInde
ion-virtual-scroll,prop,renderFooter,((item: any, index: number) => any) | undefined,undefined,false,false ion-virtual-scroll,prop,renderFooter,((item: any, index: number) => any) | undefined,undefined,false,false
ion-virtual-scroll,prop,renderHeader,((item: any, index: number) => any) | undefined,undefined,false,false ion-virtual-scroll,prop,renderHeader,((item: any, index: number) => any) | undefined,undefined,false,false
ion-virtual-scroll,prop,renderItem,((item: any, index: number) => any) | undefined,undefined,false,false ion-virtual-scroll,prop,renderItem,((item: any, index: number) => any) | undefined,undefined,false,false
ion-virtual-scroll,method,markDirty,markDirty(offset: number, len?: number) => void ion-virtual-scroll,method,checkEnd,checkEnd() => void
ion-virtual-scroll,method,markDirtyTail,markDirtyTail() => void ion-virtual-scroll,method,checkRange,checkRange(offset: number, len?: number) => void
ion-virtual-scroll,method,positionForItem,positionForItem(index: number) => Promise<number> ion-virtual-scroll,method,positionForItem,positionForItem(index: number) => Promise<number>

View File

@ -5074,7 +5074,7 @@ export namespace Components {
interface IonVirtualScroll { interface IonVirtualScroll {
/** /**
* 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 value can use either `px` or `%` 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. * 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.
*/ */
'approxFooterHeight': number; 'approxFooterHeight': number;
/** /**
@ -5085,6 +5085,14 @@ export namespace Components {
* 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. * 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.
*/ */
'approxItemHeight': number; 'approxItemHeight': number;
/**
* 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); * ```
*/
'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;
'domRender'?: DomRenderFn; 'domRender'?: DomRenderFn;
/** /**
* 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. * 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.
@ -5103,14 +5111,6 @@ export namespace Components {
*/ */
'items'?: any[]; 'items'?: any[];
/** /**
* 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.
*/
'markDirty': (offset: number, len?: number) => void;
/**
* This method marks the tail the items array as dirty, so they can be re-rendered. It's equivalent to calling: ``` * virtualScroll.markDirty(lastItemLen, items.length - lastItemLen); * ```
*/
'markDirtyTail': () => void;
/**
* NOTE: only Vanilla JS API. * NOTE: only Vanilla JS API.
*/ */
'nodeRender'?: ItemRenderFn; 'nodeRender'?: ItemRenderFn;
@ -5133,7 +5133,7 @@ export namespace Components {
} }
interface IonVirtualScrollAttributes extends StencilHTMLAttributes { interface IonVirtualScrollAttributes extends StencilHTMLAttributes {
/** /**
* 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 value can use either `px` or `%` 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. * 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.
*/ */
'approxFooterHeight'?: number; 'approxFooterHeight'?: number;
/** /**

View File

@ -256,8 +256,8 @@ let rotateImg = 0;
| Property | Attribute | Description | Type | Default | | Property | Attribute | Description | Type | Default |
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------- | | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ----------- |
| `approxFooterHeight` | `approx-footer-height` | 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 value can use either `px` or `%` 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. | `number` | `40` | | `approxFooterHeight` | `approx-footer-height` | 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. | `number` | `30` |
| `approxHeaderHeight` | `approx-header-height` | 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. | `number` | `40` | | `approxHeaderHeight` | `approx-header-height` | 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. | `number` | `30` |
| `approxItemHeight` | `approx-item-height` | 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. | `number` | `45` | | `approxItemHeight` | `approx-item-height` | 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. | `number` | `45` |
| `footerFn` | -- | 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. | `((item: any, index: number, items: any[]) => string \| null \| undefined) \| undefined` | `undefined` | | `footerFn` | -- | 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. | `((item: any, index: number, items: any[]) => string \| null \| undefined) \| undefined` | `undefined` |
| `headerFn` | -- | 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. | `((item: any, index: number, items: any[]) => string \| null \| undefined) \| undefined` | `undefined` | | `headerFn` | -- | 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. | `((item: any, index: number, items: any[]) => string \| null \| undefined) \| undefined` | `undefined` |
@ -271,7 +271,23 @@ let rotateImg = 0;
## Methods ## Methods
### `markDirty(offset: number, len?: number) => void` ### `checkEnd() => void`
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);
* ```
#### Returns
Type: `void`
### `checkRange(offset: number, len?: number) => void`
This method marks a subset of items as dirty, so they can be re-rendered. Items should be marked as 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. dirty any time the content or their style changes.
@ -291,22 +307,6 @@ Type: `void`
### `markDirtyTail() => void`
This method marks the tail the items array as dirty, so they can be re-rendered.
It's equivalent to calling:
```
* virtualScroll.markDirty(lastItemLen, items.length - lastItemLen);
* ```
#### Returns
Type: `void`
### `positionForItem(index: number) => Promise<number>` ### `positionForItem(index: number) => Promise<number>`
Returns the position of the virtual item at the given index. Returns the position of the virtual item at the given index.

View File

@ -51,10 +51,9 @@
<script> <script>
const items = Array.from({ length: 100 }, (x, i) => i); const items = Array.from({ length: 100 }, (x, i) => i);
const virtual = document.getElementById('virtual'); const virtual = document.getElementById('virtual');
virtual.itemHeight = () => 44;
virtual.items = items; virtual.items = items;
virtual.nodeRender = (el, cell) => { virtual.nodeRender = (el, cell) => {
if (cell.type === 0) { if (cell.type === 'item') {
renderItem(el, cell.value); renderItem(el, cell.value);
} }
}; };
@ -75,7 +74,7 @@
append = Array.from({ length: 10 }, (x, i) => "append" + i); append = Array.from({ length: 10 }, (x, i) => "append" + i);
} }
items.push(...append); items.push(...append);
virtual.markDirtyTail(append.length) virtual.checkEnd(append.length);
} }
function getAsyncData() { function getAsyncData() {

View File

@ -181,10 +181,14 @@ export function getShouldUpdate(dirtyIndex: number, currentRange: Range, range:
} }
export function findCellIndex(cells: Cell[], index: number): number { export function findCellIndex(cells: Cell[], index: number): number {
const max = cells[cells.length - 1].index || 0;
if (index === 0) { if (index === 0) {
return 0; return 0;
} else if (index === max + 1) {
return cells.length;
} else {
return cells.findIndex(c => c.index === index);
} }
return cells.findIndex(c => c.index === index);
} }
export function inplaceUpdate(dst: Cell[], src: Cell[], offset: number) { export function inplaceUpdate(dst: Cell[], src: Cell[], offset: number) {

View File

@ -55,18 +55,18 @@ export class VirtualScroll implements ComponentInterface {
* app's CSS, whereas this approximation is used to help calculate * app's CSS, whereas this approximation is used to help calculate
* initial dimensions before the item has been rendered. * initial dimensions before the item has been rendered.
*/ */
@Prop() approxHeaderHeight = 40; @Prop() approxHeaderHeight = 30;
/** /**
* The approximate width of each footer template's cell. * The approximate width of each footer template's cell.
* This dimension is used to help determine how many cells should * This dimension is used to help determine how many cells should
* be created when initialized, and to help calculate the height of * be created when initialized, and to help calculate the height of
* the scrollable area. This value can use either `px` or `%` units. * the scrollable area. This height value can only use `px` units.
* Note that the actual rendered size of each cell comes from the * Note that the actual rendered size of each cell comes from the
* app's CSS, whereas this approximation is used to help calculate * app's CSS, whereas this approximation is used to help calculate
* initial dimensions before the item has been rendered. * initial dimensions before the item has been rendered.
*/ */
@Prop() approxFooterHeight = 40; @Prop() approxFooterHeight = 30;
/** /**
* Section headers and the data used within its given * Section headers and the data used within its given
@ -190,7 +190,7 @@ export class VirtualScroll implements ComponentInterface {
* The subset of items to be updated can are specifing by an offset and a length. * The subset of items to be updated can are specifing by an offset and a length.
*/ */
@Method() @Method()
markDirty(offset: number, len = -1) { checkRange(offset: number, len = -1) {
// TODO: kind of hacky how we do in-place updated of the cells // TODO: kind of hacky how we do in-place updated of the cells
// array. this part needs a complete refactor // array. this part needs a complete refactor
if (!this.items) { if (!this.items) {
@ -200,18 +200,7 @@ export class VirtualScroll implements ComponentInterface {
? this.items.length - offset ? this.items.length - offset
: len; : len;
const max = this.lastItemLen; const cellIndex = findCellIndex(this.cells, offset);
let j = 0;
if (offset > 0 && offset < max) {
j = findCellIndex(this.cells, offset);
} else if (offset === 0) {
j = 0;
} else if (offset === max) {
j = this.cells.length;
} else {
console.warn('bad values for markDirty');
return;
}
const cells = calcCells( const cells = calcCells(
this.items, this.items,
this.itemHeight, this.itemHeight,
@ -220,10 +209,10 @@ export class VirtualScroll implements ComponentInterface {
this.approxHeaderHeight, this.approxHeaderHeight,
this.approxFooterHeight, this.approxFooterHeight,
this.approxItemHeight, this.approxItemHeight,
j, offset, length cellIndex, offset, length
); );
console.debug('[virtual] cells recalculated', cells.length); console.debug('[virtual] cells recalculated', cells.length);
this.cells = inplaceUpdate(this.cells, cells, offset); this.cells = inplaceUpdate(this.cells, cells, cellIndex);
this.lastItemLen = this.items.length; this.lastItemLen = this.items.length;
this.indexDirty = Math.max(offset - 1, 0); this.indexDirty = Math.max(offset - 1, 0);
@ -235,15 +224,14 @@ export class VirtualScroll implements ComponentInterface {
* *
* It's equivalent to calling: * It's equivalent to calling:
* *
* ``` * ```js
* virtualScroll.markDirty(lastItemLen, items.length - lastItemLen); * virtualScroll.checkRange(lastItemLen);
* ``` * ```
*/ */
@Method() @Method()
markDirtyTail() { checkEnd() {
if (this.items) { if (this.items) {
const offset = this.lastItemLen; this.checkRange(this.lastItemLen);
this.markDirty(offset, this.items.length - offset);
} }
} }