mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
fix(angular): virtual-scroll (#16729)
fixes #16725 fixes #16432 fixes #16023 fixes #14591 fixes #16050 fixes #15587
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import { ChangeDetectorRef, ContentChild, Directive, ElementRef, EmbeddedViewRef } from '@angular/core';
|
||||
import { ContentChild, Directive, ElementRef, EmbeddedViewRef, NgZone } from '@angular/core';
|
||||
import { Cell, CellType } from '@ionic/core';
|
||||
|
||||
import { proxyInputs } from '../proxies';
|
||||
|
||||
@ -21,15 +22,18 @@ import { VirtualContext } from './virtual-utils';
|
||||
})
|
||||
export class VirtualScroll {
|
||||
|
||||
private refMap = new WeakMap<HTMLElement, EmbeddedViewRef<VirtualContext>> ();
|
||||
|
||||
@ContentChild(VirtualItem) itmTmp!: VirtualItem;
|
||||
@ContentChild(VirtualHeader) hdrTmp!: VirtualHeader;
|
||||
@ContentChild(VirtualFooter) ftrTmp!: VirtualFooter;
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
public cd: ChangeDetectorRef,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
el.nativeElement.nodeRender = this.nodeRender.bind(this);
|
||||
const nativeEl = el.nativeElement as HTMLIonVirtualScrollElement;
|
||||
nativeEl.nodeRender = this.nodeRender.bind(this);
|
||||
|
||||
proxyInputs(this, this.el.nativeElement, [
|
||||
'approxItemHeight',
|
||||
@ -42,7 +46,8 @@ export class VirtualScroll {
|
||||
]);
|
||||
}
|
||||
|
||||
private nodeRender(el: HTMLElement | null, cell: any, index: number) {
|
||||
private nodeRender(el: HTMLElement | null, cell: Cell, index: number): HTMLElement {
|
||||
return this.zone.run(() => {
|
||||
if (!el) {
|
||||
const view = this.itmTmp.viewContainer.createEmbeddedView(
|
||||
this.getComponent(cell.type),
|
||||
@ -50,32 +55,33 @@ export class VirtualScroll {
|
||||
index
|
||||
);
|
||||
el = getElement(view);
|
||||
(el as any)['$ionView'] = view;
|
||||
this.refMap.set(el, view);
|
||||
}
|
||||
const node = (el as any)['$ionView'];
|
||||
const node = this.refMap.get(el)!;
|
||||
const ctx = node.context as VirtualContext;
|
||||
ctx.$implicit = cell.value;
|
||||
ctx.index = cell.index;
|
||||
node.detectChanges();
|
||||
node.markForCheck();
|
||||
return el;
|
||||
});
|
||||
}
|
||||
|
||||
private getComponent(type: number) {
|
||||
private getComponent(type: CellType) {
|
||||
switch (type) {
|
||||
case 0: return this.itmTmp.templateRef;
|
||||
case 1: return this.hdrTmp.templateRef;
|
||||
case 2: return this.ftrTmp.templateRef;
|
||||
case 'item': return this.itmTmp.templateRef;
|
||||
case 'header': return this.hdrTmp.templateRef;
|
||||
case 'footer': return this.ftrTmp.templateRef;
|
||||
}
|
||||
throw new Error('template for virtual item was not provided');
|
||||
}
|
||||
}
|
||||
|
||||
function getElement(view: EmbeddedViewRef<VirtualContext>): HTMLElement | null {
|
||||
function getElement(view: EmbeddedViewRef<VirtualContext>): HTMLElement {
|
||||
const rootNodes = view.rootNodes;
|
||||
for (let i = 0; i < rootNodes.length; i++) {
|
||||
if (rootNodes[i].nodeType === 1) {
|
||||
return rootNodes[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
throw new Error('virtual element was not created');
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import { TabsComponent } from './tabs/tabs.component';
|
||||
import { TabsTab1Component } from './tabs-tab1/tabs-tab1.component';
|
||||
import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.component';
|
||||
import { TabsTab2Component } from './tabs-tab2/tabs-tab2.component';
|
||||
import { VirtualScrollComponent } from './virtual-scroll/virtual-scroll.component';
|
||||
import { VirtualScrollDetailComponent } from './virtual-scroll-detail/virtual-scroll-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: HomePageComponent },
|
||||
@ -16,6 +18,9 @@ const routes: Routes = [
|
||||
{ path: 'modals', component: ModalComponent },
|
||||
{ path: 'router-link', component: RouterLinkComponent },
|
||||
{ path: 'router-link-page', component: RouterLinkPageComponent },
|
||||
{ path: 'virtual-scroll', component: VirtualScrollComponent },
|
||||
{ path: 'virtual-scroll-detail/:itemId', component: VirtualScrollDetailComponent },
|
||||
|
||||
{ path: 'tabs', redirectTo: '/tabs/account', pathMatch: 'full' },
|
||||
{
|
||||
path: 'tabs',
|
||||
|
@ -15,6 +15,9 @@ import { TabsComponent } from './tabs/tabs.component';
|
||||
import { TabsTab1Component } from './tabs-tab1/tabs-tab1.component';
|
||||
import { TabsTab2Component } from './tabs-tab2/tabs-tab2.component';
|
||||
import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.component';
|
||||
import { VirtualScrollComponent } from './virtual-scroll/virtual-scroll.component';
|
||||
import { VirtualScrollDetailComponent } from './virtual-scroll-detail/virtual-scroll-detail.component';
|
||||
import { VirtualScrollInnerComponent } from './virtual-scroll-inner/virtual-scroll-inner.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -28,7 +31,10 @@ import { TabsTab1NestedComponent } from './tabs-tab1-nested/tabs-tab1-nested.com
|
||||
TabsComponent,
|
||||
TabsTab1Component,
|
||||
TabsTab2Component,
|
||||
TabsTab1NestedComponent
|
||||
TabsTab1NestedComponent,
|
||||
VirtualScrollComponent,
|
||||
VirtualScrollDetailComponent,
|
||||
VirtualScrollInnerComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -27,5 +27,10 @@
|
||||
Tabs test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/virtual-scroll">
|
||||
<ion-label>
|
||||
Virtual Scroll
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
@ -0,0 +1,17 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>virtual-scroll page</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<h1>Item {{itemNu}}</h1>
|
||||
<p>ngOnInit: <span id="ngOnInit">{{onInit}}</span></p>
|
||||
<p>ionViewWillEnter: <span id="ionViewWillEnter">{{willEnter}}</span></p>
|
||||
<p>ionViewDidEnter: <span id="ionViewDidEnter">{{didEnter}}</span></p>
|
||||
<p>ionViewWillLeave: <span id="ionViewWillLeave">{{willLeave}}</span></p>
|
||||
<p>ionViewDidLeave: <span id="ionViewDidLeave">{{didLeave}}</span></p>
|
||||
</ion-content>
|
@ -0,0 +1,45 @@
|
||||
import { Component, NgZone, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-virtual-scroll-detail',
|
||||
templateUrl: './virtual-scroll-detail.component.html',
|
||||
})
|
||||
export class VirtualScrollDetailComponent implements OnInit {
|
||||
|
||||
onInit = 0;
|
||||
willEnter = 0;
|
||||
didEnter = 0;
|
||||
willLeave = 0;
|
||||
didLeave = 0;
|
||||
|
||||
itemNu = 'none';
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.itemNu = this.route.snapshot.paramMap.get('itemId');
|
||||
NgZone.assertInAngularZone();
|
||||
this.onInit++;
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
if (this.onInit !== 1) {
|
||||
throw new Error('ngOnInit was not called');
|
||||
}
|
||||
NgZone.assertInAngularZone();
|
||||
this.willEnter++;
|
||||
}
|
||||
ionViewDidEnter() {
|
||||
NgZone.assertInAngularZone();
|
||||
this.didEnter++;
|
||||
}
|
||||
ionViewWillLeave() {
|
||||
NgZone.assertInAngularZone();
|
||||
this.willLeave++;
|
||||
}
|
||||
ionViewDidLeave() {
|
||||
NgZone.assertInAngularZone();
|
||||
this.didLeave++;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<p>
|
||||
[{{onInit}}] Item {{value}}
|
||||
</p>
|
@ -0,0 +1,17 @@
|
||||
import { Component, OnInit, NgZone, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-virtual-scroll-inner',
|
||||
templateUrl: './virtual-scroll-inner.component.html',
|
||||
})
|
||||
export class VirtualScrollInnerComponent implements OnInit {
|
||||
|
||||
@Input() value: string;
|
||||
onInit = 0;
|
||||
|
||||
ngOnInit() {
|
||||
NgZone.assertInAngularZone();
|
||||
this.onInit++;
|
||||
console.log('created');
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Virtual Scroll Test
|
||||
</ion-title>
|
||||
</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" [routerLink]="['/', 'virtual-scroll-detail', item]">
|
||||
<ion-label>
|
||||
<app-virtual-scroll-inner [value]="item"></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-item>
|
||||
</ion-virtual-scroll>
|
||||
</ion-content>
|
@ -0,0 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HeaderFn } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-virtual-scroll',
|
||||
templateUrl: './virtual-scroll.component.html',
|
||||
})
|
||||
export class VirtualScrollComponent {
|
||||
|
||||
items = Array.from({length: 1000}, (_, i) => i);
|
||||
|
||||
myHeaderFn: HeaderFn = (_, index) => {
|
||||
if ((index % 10) === 0) {
|
||||
return `Header ${index}`;
|
||||
}
|
||||
}
|
||||
|
||||
myFooterFn: HeaderFn = (_, index) => {
|
||||
if ((index % 5) === 0) {
|
||||
return `Footer ${index}`;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user