diff --git a/coverage/coverage.svg b/coverage/coverage.svg
index db7d8a5..506419f 100644
--- a/coverage/coverage.svg
+++ b/coverage/coverage.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 982c4cb..940e4b8 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "@lithiumjs/ngx-virtual-scroll",
"description": "A fast virtual scrolling solution for Angular that natively supports items with unequal heights. Built with @lithiumjs/angular.",
"repository": "https://github.com/lVlyke/lithium-ngx-virtual-scroll",
- "version": "0.0.4",
+ "version": "0.0.5",
"main": "index.js",
"author": "Mychal Thompson ",
"license": "MIT",
@@ -14,9 +14,9 @@
"test": "node ./scripts/test.js"
},
"peerDependencies": {
- "@angular/core": "6.x.x - 11.x.x",
- "@angular/common": "6.x.x - 11.x.x",
- "@lithiumjs/angular": ">=5.0.0",
+ "@angular/core": "6.x.x - 12.x.x",
+ "@angular/common": "6.x.x - 12.x.x",
+ "@lithiumjs/angular": ">=7.0.0",
"rxjs": "6.x.x"
},
"devDependencies": {
@@ -24,7 +24,7 @@
"@angular/compiler-cli": "^11.2.4",
"@angular/core": "^11.2.4",
"@angular/common": "^11.2.4",
- "@lithiumjs/angular": "^6.0.0-beta.0",
+ "@lithiumjs/angular": "^7.0.0",
"@types/jasmine": "^3.3.13",
"detest-bdd": "^1.1.1",
"fs-extra": "^8.1.0",
diff --git a/src/components/virtual-scroll/virtual-scroll.component.ts b/src/components/virtual-scroll/virtual-scroll.component.ts
index e0d91c8..a4c5221 100644
--- a/src/components/virtual-scroll/virtual-scroll.component.ts
+++ b/src/components/virtual-scroll/virtual-scroll.component.ts
@@ -6,22 +6,15 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef
} from "@angular/core";
-import { LiComponent, StateEmitter, OnDestroy, AutoPush } from "@lithiumjs/angular";
-import { Subject, Observable, combineLatest, of, forkJoin, fromEvent, asyncScheduler } from "rxjs";
+import { OnDestroy, AutoPush, DeclareState, ComponentState, ComponentStateRef } from "@lithiumjs/angular";
+import { Observable, combineLatest, of, forkJoin, fromEvent, asyncScheduler } from "rxjs";
import { map, throttleTime, filter, mergeMap, take, delay, tap } from "rxjs/operators";
import { VirtualItem } from "../../directives/virtual-item.directive";
-export function DEFAULT_SCROLL_POSITION(): VirtualScroll.ScrollPosition {
- return { x: 0, y: 0 };
-}
-
-export function EMPTY_ARRAY(): T[] {
- return [];
-}
-
@Component({
selector: "li-virtual-scroll",
changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [ComponentState.create(VirtualScroll)],
template: `
@@ -39,104 +32,88 @@ export function EMPTY_ARRAY(): T[] {
`
})
-export class VirtualScroll extends LiComponent {
+export class VirtualScroll {
private static readonly DEFAULT_BUFFER_LENGTH = 3;
- private static readonly DEFAULT_SCROLL_THROTTLE_MS = 50;
-
- @ContentChild(VirtualItem)
- public readonly virtualItem: VirtualItem;
+ private static readonly DEFAULT_SCROLL_THROTTLE_MS = 100;
@Input()
- public items: T[];
-
- @StateEmitter({ propertyName: "items", initial: EMPTY_ARRAY })
- public readonly items$: Subject;
+ public items: T[] = [];
@Input()
- public bufferLength: number;
-
- @StateEmitter({ propertyName: "bufferLength", initialValue: VirtualScroll.DEFAULT_BUFFER_LENGTH })
- public readonly bufferLength$: Subject;
+ public bufferLength = VirtualScroll.DEFAULT_BUFFER_LENGTH;
@Input()
- public scrollContainer: HTMLElement;
-
- @StateEmitter({ propertyName: "scrollContainer" })
- public readonly scrollContainer$: Subject;
+ @DeclareState()
+ public scrollContainer?: HTMLElement;
@Input()
- public eventCapture: boolean;
+ public eventCapture = false;
- @StateEmitter({ propertyName: "eventCapture", initialValue: false })
- public readonly eventCapture$: Subject;
+ @ContentChild(VirtualItem)
+ public virtualItem!: VirtualItem;
- @StateEmitter({ initial: DEFAULT_SCROLL_POSITION })
- private readonly scrollPosition$: Subject;
+ @OnDestroy()
+ private readonly onDestroy$!: Observable;
- @StateEmitter({ initial: EMPTY_ARRAY })
- private readonly renderedItems$: Subject[]>;
+ @DeclareState("renderedItems")
+ private _renderedItems: VirtualScroll.RenderedItem[] = [];
- @StateEmitter({ initialValue: 0 })
- private readonly referenceWidth$: Subject;
+ @DeclareState("scrollPosition")
+ private _scrollPosition: VirtualScroll.ScrollPosition = { x: 0, y: 0 };
- @StateEmitter({ initialValue: 0 })
- private readonly referenceHeight$: Subject;
+ @DeclareState("referenceWidth")
+ private _referenceWidth = 0;
- @OnDestroy()
- private readonly onDestroy$: Observable;
+ @DeclareState("referenceWidth")
+ private _referenceHeight = 0;
- private readonly listElement: HTMLElement;
+ private _listElement!: HTMLElement;
constructor(
private readonly cdRef: ChangeDetectorRef,
+ stateRef: ComponentStateRef>,
{ nativeElement: listElement }: ElementRef
) {
- super();
-
AutoPush.enable(this, cdRef);
- this.listElement = listElement;
-
- this.scrollContainer$.next(listElement);
+ this.scrollContainer = this._listElement = listElement;
- const scrollSubscription = combineLatest([
- this.scrollContainer$,
- this.eventCapture$
- ]).pipe(
+ const scrollSubscription = combineLatest(stateRef.getAll(
+ "scrollContainer",
+ "eventCapture"
+ )).pipe(
tap(([scrollContainer]) => this.applyScrollContainerStyles(scrollContainer === listElement)),
- mergeMap(([scrollContainer, capture]) => fromEvent(scrollContainer, "scroll", { capture }))
+ mergeMap(([scrollContainer, capture]) => fromEvent(scrollContainer!, "scroll", { capture }))
).subscribe((scrollEvent) => {
- this.scrollPosition$.next({
+ this._scrollPosition = {
x: (scrollEvent.target as HTMLElement).scrollLeft,
y: (scrollEvent.target as HTMLElement).scrollTop
- });
+ };
});
this.onDestroy$.subscribe(() => scrollSubscription.unsubscribe());
- this.items$.pipe(
+ stateRef.get("items").pipe(
map((items): VirtualScroll.RenderedItem[] => items.map((item) => ({
item,
visible: false
})))
- ).subscribe(this.renderedItems$);
+ ).subscribe(renderedItems => this._renderedItems = renderedItems);
// Make the first element visible (TODO- always?)
- this.renderedItems$.pipe(
+ stateRef.get("renderedItems").pipe(
filter(renderedItems => renderedItems.length > 0),
take(1)
).subscribe(renderedItems => renderedItems[0].visible = true);
combineLatest([
- this.scrollPosition$.pipe(throttleTime(
+ stateRef.get("scrollPosition").pipe(throttleTime(
VirtualScroll.DEFAULT_SCROLL_THROTTLE_MS, // TODO - Make customizable
asyncScheduler,
{ leading: true, trailing: true }
)),
- this.renderedItems$,
- this.scrollContainer$,
- this.bufferLength$
+ ...stateRef.getAll("renderedItems", "scrollContainer", "bufferLength")
]).pipe(
filter(([, renderedItems]) => renderedItems.length > 0),
delay(0), // Wait for DOM rendering to occur
@@ -144,10 +121,10 @@ export class VirtualScroll extends LiComponent {
const renderedBounds: VirtualScroll.Rect = {
left: scrollPosition.x,
top: scrollPosition.y,
- right: scrollPosition.x + scrollContainer.clientWidth,
- bottom: scrollPosition.y + scrollContainer.clientHeight
+ right: scrollPosition.x + scrollContainer!.clientWidth,
+ bottom: scrollPosition.y + scrollContainer!.clientHeight
};
- const bufferLengthPx = (scrollContainer.clientHeight) * bufferLength;
+ const bufferLengthPx = (scrollContainer!.clientHeight) * bufferLength;
const [bestRenderedIndex, renderedElement] = this.findBestOnScreenItem(renderedItems);
renderedBounds.top -= bufferLengthPx;
@@ -155,8 +132,8 @@ export class VirtualScroll extends LiComponent {
if (renderedElement) {
// TODO
- this.referenceWidth$.next(renderedElement.clientWidth);
- this.referenceHeight$.next(renderedElement.clientHeight);
+ this._referenceWidth = renderedElement.clientWidth;
+ this._referenceHeight = renderedElement.clientHeight;
const offset = { x: renderedElement.offsetLeft, y: renderedElement.offsetTop };
const elementDimensions = {
@@ -173,20 +150,20 @@ export class VirtualScroll extends LiComponent {
delay(VirtualScroll.DEFAULT_SCROLL_THROTTLE_MS * 2),
tap(() => {
// Re-check the rendering status if there are no rendered items or if the scroll position changed quickly
- if (this.listElement.scrollLeft !== scrollPosition.x
- || this.listElement.scrollTop !== scrollPosition.y
+ if (this._listElement.scrollLeft !== scrollPosition.x
+ || this._listElement.scrollTop !== scrollPosition.y
|| this.getAllRenderedElements().length === 0) {
- this.scrollPosition$.next({ x: this.listElement.scrollLeft, y: this.listElement.scrollTop });
+ this._scrollPosition = { x: this._listElement.scrollLeft, y: this._listElement.scrollTop };
}
})
);
- } else if (renderedItems.length > 0) {
+ } else if (renderedItems.length > 0 && this._referenceWidth > 0 && this._referenceHeight > 0) {
// If there are no rendered items, walk the list from the beginning to find the rendered segment
return this.walkList(renderedItems, renderedBounds, 0, 1, false, {
left: 0,
top: 0,
- right: this.referenceWidth,
- bottom: this.referenceHeight
+ right: this._referenceWidth,
+ bottom: this._referenceHeight
});
} else {
return of(null);
@@ -195,14 +172,29 @@ export class VirtualScroll extends LiComponent {
).subscribe();
}
+ public get renderedItems(): VirtualScroll.RenderedItem[] {
+ return this._renderedItems;
+ }
+
+ public get scrollPosition(): VirtualScroll.ScrollPosition {
+ return this._scrollPosition;
+ }
+
+ public get referenceWidth(): number {
+ return this._referenceWidth;
+ }
+
+ public get referenceHeight(): number {
+ return this._referenceHeight;
+ }
+
public checkScroll(scrollPosition?: VirtualScroll.ScrollPosition): void {
- (scrollPosition ? of(scrollPosition) : this.scrollPosition$.pipe(take(1)))
- .subscribe(scrollPosition => this.scrollPosition$.next(scrollPosition));
+ this._scrollPosition = scrollPosition ?? this._scrollPosition;
}
private applyScrollContainerStyles(apply: boolean) {
- this.listElement.style.overflowY = apply ? "scroll" : "initial";
- this.listElement.style.display = apply ? "block" : "initial";
+ this._listElement.style.overflowY = apply ? "scroll" : "initial";
+ this._listElement.style.display = apply ? "block" : "initial";
}
private findBestOnScreenItem(renderedItems: VirtualScroll.RenderedItem[]): [number, HTMLElement] {
@@ -211,7 +203,7 @@ export class VirtualScroll extends LiComponent {
// Grab any rendered element (that is currently being rendered in the DOM)
let bestRenderedIndex = minRenderedIndex;
- let renderedElement: HTMLElement;
+ let renderedElement: HTMLElement | null;
do {
renderedElement = this.getRenderedElement(bestRenderedIndex);
} while (!renderedElement && ++bestRenderedIndex < (maxRenderedIndex === -1 ? renderedItems.length : maxRenderedIndex));
@@ -224,15 +216,15 @@ export class VirtualScroll extends LiComponent {
}
}
- return [bestRenderedIndex, renderedElement];
+ return [bestRenderedIndex, renderedElement!];
}
- private getRenderedElement(renderedIndex: number): HTMLElement {
- return this.listElement.querySelector(`[data-li-virtual-index="${renderedIndex}"]`);
+ private getRenderedElement(renderedIndex: number): HTMLElement | null {
+ return this._listElement.querySelector(`[data-li-virtual-index="${renderedIndex}"]`);
}
private getAllRenderedElements(): NodeListOf {
- return this.listElement.querySelectorAll(".li-virtual-item");
+ return this._listElement.querySelectorAll(".li-virtual-item");
}
private intersects(a: VirtualScroll.Rect, b: VirtualScroll.Rect): boolean {
@@ -251,14 +243,12 @@ export class VirtualScroll extends LiComponent {
const nextIndex = index + direction;
// Stop walking the list if we hit an unrendered segment
- if (stopOnUnrendered && !lastElementDimensions && !item?.visible) {
+ if (nextIndex >= renderedItems.length || (stopOnUnrendered && !lastElementDimensions && !item?.visible)) {
return of(renderedItems);
}
- let renderedElement: HTMLElement;
- const visible = item?.visible && (renderedElement = this.getRenderedElement(index));
- if (visible) {
-
+ let renderedElement: HTMLElement | null;
+ if (item?.visible && (renderedElement = this.getRenderedElement(index))) {
const offset = { x: renderedElement.offsetLeft, y: renderedElement.offsetTop };
// Update the element dimensions based on the current element
@@ -274,7 +264,7 @@ export class VirtualScroll extends LiComponent {
item.visible = false;
this.cdRef.markForCheck();
}
- } else {
+ } else if (lastElementDimensions) {
const lastElementSize = {
x: lastElementDimensions.right - lastElementDimensions.left,
y: lastElementDimensions.bottom - lastElementDimensions.top,
@@ -283,11 +273,11 @@ export class VirtualScroll extends LiComponent {
const offsetY = lastElementSize.y * direction;
// Walk to the next item in the list
- if (lastElementDimensions.right + offsetX <= this.listElement.scrollWidth && lastElementDimensions.left + offsetX >= 0) {
+ if (lastElementDimensions.right + offsetX <= this._listElement.scrollWidth && lastElementDimensions.left + offsetX >= 0) {
lastElementDimensions.left += offsetX;
lastElementDimensions.right += offsetX;
} else {
- lastElementDimensions.left = direction > 0 ? 0 : this.listElement.scrollWidth;
+ lastElementDimensions.left = direction > 0 ? 0 : this._listElement.scrollWidth;
lastElementDimensions.right = lastElementDimensions.left + offsetX;
lastElementDimensions.top += offsetY;
@@ -296,14 +286,13 @@ export class VirtualScroll extends LiComponent {
// If the current item should be rendered, make it visible
if (this.intersects(renderedBounds, lastElementDimensions)) {
- item.visible = true;
- this.cdRef.detectChanges();
-
- // Wait for the DOM element to render, then continue walking the list
- return of(null).pipe(
- delay(0),
- mergeMap(() => this.walkList(renderedItems, renderedBounds, index, direction, stopOnUnrendered, lastElementDimensions))
- );
+ if (!!item) {
+ item.visible = true;
+ this.cdRef.markForCheck();
+ }
+
+ // Continue walking the list
+ return this.walkList(renderedItems, renderedBounds, nextIndex, direction, stopOnUnrendered, lastElementDimensions);
}
}
diff --git a/src/directives/virtual-item.directive.ts b/src/directives/virtual-item.directive.ts
index 8138dda..d7865e9 100644
--- a/src/directives/virtual-item.directive.ts
+++ b/src/directives/virtual-item.directive.ts
@@ -1,14 +1,11 @@
import { Directive, TemplateRef } from "@angular/core";
-import { LiComponent } from "@lithiumjs/angular";
@Directive({
selector: "[liVirtualItem]"
})
-export class VirtualItem extends LiComponent {
+export class VirtualItem {
constructor(
public readonly templateRef: TemplateRef
- ) {
- super();
- }
+ ) {}
}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 1672d29..4ee59ee 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
@@ -29,6 +30,9 @@
"rewriteTsConfig": false
},
"angularCompilerOptions": {
- "skipTemplateCodegen": true
+ "skipTemplateCodegen": true,
+ "strictMetadataEmit": true,
+ "strictTemplates": true,
+ "fullTemplateTypeCheck": true
}
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index c602fb9..a61fd25 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -239,12 +239,12 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
-"@lithiumjs/angular@^6.0.0-beta.0":
- version "6.0.0-beta.0"
- resolved "https://registry.yarnpkg.com/@lithiumjs/angular/-/angular-6.0.0-beta.0.tgz#cb6592404649d4a351a14091f9ec887ac82ed982"
- integrity sha512-WYkJAWbQ/Hit+Feru8didgHfvZGHWflp589sz5Jh8YocdGZNt7X+FKsutZKBezToV9IwZRtPeR7mshgHvA+Byg==
+"@lithiumjs/angular@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@lithiumjs/angular/-/angular-7.0.0.tgz#8932f6ec29d4ebed7ecaf88243468fe374ba63e3"
+ integrity sha512-KHykfp163GidULSV5zT31KmKH5eAMZ2vF6n8uoJa/230p7qgA8ttv8Eh4njosBLd7X4/c7qKTWBYYmEX5dHPhQ==
dependencies:
- tslib "^2.0.0"
+ tslib "^2.1.0"
"@rollup/plugin-commonjs@^17.0.0":
version "17.1.0"
@@ -2627,6 +2627,11 @@ tslib@^2.0.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
+tslib@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
+ integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"