diff --git a/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.html b/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.html index cbc0571..0e01a7b 100644 --- a/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.html +++ b/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.html @@ -1 +1,9 @@ -

scroll-advanced-item works!

+ + + + + +

{{ item()?.name }}

+

{{ item()?.description }}

+
+
diff --git a/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.ts b/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.ts index 70fc70b..20763e4 100644 --- a/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.ts +++ b/projects/demo/src/app/scroll-strategies/components/scroll-advanced-item/scroll-advanced-item.component.ts @@ -1,12 +1,41 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, effect, ElementRef, inject, input, OnInit } from '@angular/core'; +import { ScrollAdvancedItem } from '../../scroll-strategies.type'; +import { IonAvatar, IonImg, IonItem, IonLabel } from '@ionic/angular/standalone'; +import { ScrollAdvancedCalcService } from '../../scroll-advanced-calc.service'; @Component({ selector: 'app-scroll-advanced-item', templateUrl: './scroll-advanced-item.component.html', styleUrls: ['./scroll-advanced-item.component.scss'], + standalone: true, + imports: [IonItem, IonAvatar, IonImg, IonLabel], }) export class ScrollAdvancedItemComponent implements OnInit { - constructor() {} + item = input(); + #el = inject(ElementRef); + #calcService = inject(ScrollAdvancedCalcService); + + constructor() { + effect(() => + (async (item: ScrollAdvancedItem | undefined) => { + if (item === undefined) { + return; + } + // Wait for rendering + await new Promise((resolve) => requestAnimationFrame(resolve)); + this.#calcService.cacheCalcDynamic.update((cache) => { + if (!cache.some((c) => c.trackId === item.trackId)) { + cache.push({ + trackId: item.trackId, + itemSize: this.#el.nativeElement.getBoundingClientRect().height, + }); + } + // Update, but not notify to computed + return cache; + }); + })(this.item()), + ); + } ngOnInit() {} } diff --git a/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.html b/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.html index e3bd5fc..9364984 100644 --- a/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.html +++ b/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.html @@ -4,7 +4,29 @@ scroll-advanced + + + - + + + +
+ +
+
+ + + +
+
diff --git a/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.ts b/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.ts index 6a8ee4e..37462f3 100644 --- a/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.ts +++ b/projects/demo/src/app/scroll-strategies/pages/scroll-advanced/scroll-advanced.page.ts @@ -1,17 +1,128 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, computed, inject, OnInit, signal, viewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone'; +import { + IonAvatar, + IonBackButton, + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonImg, + IonInfiniteScroll, + IonInfiniteScrollContent, + IonItem, + IonLabel, + IonList, + IonTitle, + IonToolbar, + ViewDidEnter, + ViewDidLeave, + ViewWillEnter, +} from '@ionic/angular/standalone'; +import { CdkDynamicSizeVirtualScroll, DynamicSizeVirtualScrollService, itemDynamicSize } from 'scroll-strategies'; +import { InfiniteScrollCustomEvent } from '@ionic/angular'; +import { DynamicSizeCache, ScrollAdvancedItem } from '../../scroll-strategies.type'; +import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { FixVirtualScrollElementDirective } from 'scroll-header'; +import { ScrollAdvancedItemComponent } from '../../components/scroll-advanced-item/scroll-advanced-item.component'; +import { ScrollAdvancedCalcService } from '../../scroll-advanced-calc.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-scroll-advanced', templateUrl: './scroll-advanced.page.html', styleUrls: ['./scroll-advanced.page.scss'], standalone: true, - imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule, IonBackButton, IonButtons], + imports: [ + IonContent, + IonHeader, + IonTitle, + IonToolbar, + CommonModule, + FormsModule, + IonBackButton, + IonButtons, + IonButton, + IonIcon, + CdkDynamicSizeVirtualScroll, + CdkVirtualForOf, + CdkVirtualScrollViewport, + FixVirtualScrollElementDirective, + IonInfiniteScroll, + IonInfiniteScrollContent, + IonList, + IonItem, + IonAvatar, + IonImg, + IonLabel, + ScrollAdvancedItemComponent, + ], }) -export class ScrollAdvancedPage implements OnInit { +export class ScrollAdvancedPage implements OnInit, ViewDidEnter, ViewDidLeave { + readonly virtualScroll = viewChild(CdkVirtualScrollViewport); + readonly #enterSubscription$: Subscription[] = []; + + readonly #scroll = inject(DynamicSizeVirtualScrollService); + readonly #calcService = inject(ScrollAdvancedCalcService); + + readonly items = signal([]); + readonly page = signal(0); + readonly dynamicSize = computed(() => { + return this.#calcService.changeItemsToDynamicItemSize(this.items(), this.#calcService.cacheCalcDynamic(), this.virtualScroll()); + }); + readonly bindDynamicItemHeight = this.#scroll.getBindDynamicItemHeight(this.dynamicSize); + constructor() {} - ngOnInit() {} + ngOnInit() { + this.items.set(this.#createItems(100)); + } + + ionViewDidEnter() { + this.#enterSubscription$.push( + this.virtualScroll()!.scrolledIndexChange.subscribe(() => { + if (this.#calcService.beforeCacheCalcDynamicSize() !== this.#calcService.cacheCalcDynamic().length) { + this.#calcService.cacheCalcDynamic.update((cache) => [...cache]); + this.#calcService.beforeCacheCalcDynamicSize.set(this.#calcService.cacheCalcDynamic().length); + } + }), + ); + } + + ionViewDidLeave() { + this.#enterSubscription$.forEach((subscription) => subscription.unsubscribe()); + } + + async loadInfinite(event: InfiniteScrollCustomEvent) { + await new Promise((resolve) => setTimeout(resolve, 500)); + this.items.update((items) => { + // You must create new array. + return [...items, ...this.#createItems(100)]; + }); + await event.target.complete(); + } + + #createItems(length: number): ScrollAdvancedItem[] { + this.page.update((page) => page + 1); + return Array.from({ length }).map((_, index) => { + const nameLength = Math.floor(Math.random() * (10 - 5)) + 5; + const descriptionLength = Math.floor(Math.random() * (400 - 20)) + 20; + return { + trackId: this.page() + '-' + index, + name: Array.from({ length: 100 }) + .map(() => Math.random().toString(36)) + .join() + .slice(-nameLength), + description: Array.from({ length: 100 }) + .map(() => Math.random().toString(36)) + .join() + .slice(-descriptionLength), + photo: `https://picsum.photos/200?random=${index}`, + }; + }); + } + + trackByFn = (_: number, item: ScrollAdvancedItem) => item.trackId; } diff --git a/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.html b/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.html index d17b17b..b844284 100644 --- a/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.html +++ b/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.html @@ -4,6 +4,11 @@ scroll-reverse + + + diff --git a/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.ts b/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.ts index a33c2fe..f5faedb 100644 --- a/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.ts +++ b/projects/demo/src/app/scroll-strategies/pages/scroll-reverse/scroll-reverse.page.ts @@ -1,14 +1,14 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular/standalone'; +import { IonBackButton, IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonTitle, IonToolbar } from '@ionic/angular/standalone'; @Component({ selector: 'app-scroll-reverse', templateUrl: './scroll-reverse.page.html', styleUrls: ['./scroll-reverse.page.scss'], standalone: true, - imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule, IonBackButton, IonButtons], + imports: [IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule, IonBackButton, IonButtons, IonButton, IonIcon], }) export class ScrollReversePage implements OnInit { constructor() {} diff --git a/projects/demo/src/app/scroll-strategies/pages/scroll-simple/scroll-simple.page.ts b/projects/demo/src/app/scroll-strategies/pages/scroll-simple/scroll-simple.page.ts index 7ee96d7..3a6db7d 100644 --- a/projects/demo/src/app/scroll-strategies/pages/scroll-simple/scroll-simple.page.ts +++ b/projects/demo/src/app/scroll-strategies/pages/scroll-simple/scroll-simple.page.ts @@ -71,7 +71,7 @@ export class ScrollSimplePage implements OnInit { await event.target.complete(); } - #createItems(length: number) { + #createItems(length: number): Item[] { return Array.from({ length }).map((_, index) => { return { trackId: index, diff --git a/projects/demo/src/app/scroll-strategies/pages/scroll-strategies/scroll-strategies.page.html b/projects/demo/src/app/scroll-strategies/pages/scroll-strategies/scroll-strategies.page.html index 0feb734..4f6e1cb 100644 --- a/projects/demo/src/app/scroll-strategies/pages/scroll-strategies/scroll-strategies.page.html +++ b/projects/demo/src/app/scroll-strategies/pages/scroll-strategies/scroll-strategies.page.html @@ -11,8 +11,8 @@ - Scroll Simple - Scroll Advanced - Scroll Reverse + Scroll Simple + Scroll Advanced + Scroll Reverse diff --git a/projects/demo/src/app/scroll-strategies/scroll-advanced-calc.service.ts b/projects/demo/src/app/scroll-strategies/scroll-advanced-calc.service.ts index c9747ea..ff93271 100644 --- a/projects/demo/src/app/scroll-strategies/scroll-advanced-calc.service.ts +++ b/projects/demo/src/app/scroll-strategies/scroll-advanced-calc.service.ts @@ -1,8 +1,42 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable, signal } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { DynamicSizeCache, ScrollAdvancedItem } from './scroll-strategies.type'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { itemDynamicSize } from 'scroll-strategies'; @Injectable({ providedIn: 'root', }) export class ScrollAdvancedCalcService { + readonly cacheCalcDynamic = signal([]); + readonly beforeCacheCalcDynamicSize = signal(0); + constructor() {} + + changeItemsToDynamicItemSize( + items: ScrollAdvancedItem[], + dynamicSizeCache: DynamicSizeCache[], + virtualScroll: CdkVirtualScrollViewport | undefined, + ): itemDynamicSize[] { + if (virtualScroll === undefined) { + return []; + } + return ( + items?.map((item, index) => { + const cacheSize = dynamicSizeCache.find((cache) => cache.trackId === item.trackId)?.itemSize; + + /** + * 150 is approximate item size. + * This is important for the accuracy of the initial display. + */ + const itemSize = cacheSize || 150; + + return { + itemSize: Math.ceil(itemSize), + trackId: item.trackId, + source: cacheSize !== undefined ? 'cache' : 'temporary', + }; + }) || [] + ); + } } diff --git a/projects/demo/src/app/scroll-strategies/scroll-strategies.type.ts b/projects/demo/src/app/scroll-strategies/scroll-strategies.type.ts new file mode 100644 index 0000000..42eef8e --- /dev/null +++ b/projects/demo/src/app/scroll-strategies/scroll-strategies.type.ts @@ -0,0 +1,8 @@ +export type ScrollAdvancedItem = { + trackId: string; + name: string; + description: string; + photo: string; +}; + +export type DynamicSizeCache = { trackId: number | string; itemSize: number }; diff --git a/projects/demo/src/index.html b/projects/demo/src/index.html index a1f4977..06ac9a8 100644 --- a/projects/demo/src/index.html +++ b/projects/demo/src/index.html @@ -2,7 +2,7 @@ - Demo of @rdlabo/ionic-angular-photo-editor + Demo of ionic-angular-library