-
Notifications
You must be signed in to change notification settings - Fork 269
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* [ACA-4729] Add infinite scroll to version list * [ACA-4729] CR fixes * [ACA-4729] CR fixes * [ACA-4729] Items count fix for infinite scroll datasource
- Loading branch information
1 parent
2c0ad71
commit a7e7934
Showing
13 changed files
with
488 additions
and
151 deletions.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
docs/content-services/components/infinite-scroll-datasource.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
--- | ||
Title: Infinite Scroll Datasource | ||
Added: v6.6.0 | ||
Status: Active | ||
Last reviewed: 2024-01-15 | ||
--- | ||
|
||
# [Infinite Scroll Datasource](../../../lib/content-services/src/lib/infinite-scroll-datasource/infinite-scroll-datasource.ts "Defined in infinite-scroll-datasource.ts") | ||
|
||
Contains abstract class acting as a baseline for various datasources for infinite scrolls. | ||
|
||
## Basic Usage | ||
|
||
First step to use infinite scroll datasource in any component is creating a datasource class extending `InfiniteScrollDatasource` using specific source of data e.g. one of the content endpoints. | ||
```ts | ||
export class VersionListDataSource extends InfiniteScrollDatasource<VersionEntry> { | ||
constructor(private versionsApi: VersionsApi, private node: Node) { | ||
super(); | ||
} | ||
|
||
getNextBatch(pagingOptions: ContentPagingQuery): Observable<VersionEntry[]> { | ||
return from(this.versionsApi.listVersionHistory(this.node.id, pagingOptions)).pipe( | ||
take(1), | ||
map((versionPaging) => versionPaging.list.entries) | ||
); | ||
} | ||
} | ||
``` | ||
|
||
Then in component that will have the infinite scroll define the datasource as instance of a class created in previous step, optionally you can set custom size of the items batch or listen to loading state changes: | ||
```ts | ||
this.versionsDataSource = new VersionListDataSource(this.versionsApi, this.node); | ||
this.versionsDataSource.batchSize = 50; | ||
this.versionsDataSource.isLoading.pipe(takeUntil(this.onDestroy$)).subscribe((isLoading) => this.isLoading = isLoading); | ||
``` | ||
|
||
Final step is to add the [CdkVirtualScrollViewport](https://material.angular.io/cdk/scrolling/api#CdkVirtualScrollViewport) with [CdkVirtualFor](https://material.angular.io/cdk/scrolling/api#CdkVirtualForOf) loop displaying items from the datasource. | ||
```html | ||
<cdk-virtual-scroll-viewport appendOnly itemSize="88"> | ||
<div *cdkVirtualFor="let version of versionsDataSource"></div> | ||
</cdk-virtual-scroll-viewport> | ||
``` | ||
|
||
When user will scroll down to the bottom of the list next batch of items will be fetched until all items are visible. | ||
|
||
## Class members | ||
|
||
### Properties | ||
|
||
| Name | Type | Default value | Description | | ||
| ---- | ---- | ------------- | ----------- | | ||
| batchSize | `number` | 100 | Determines how much items will be fetched within one batch. | | ||
| firstItem | `T` | | Returns the first item ever fetched. | | ||
| isLoading | [`Observable`](https://rxjs.dev/api/index/class/Observable)`<boolean>` | | Observable representing the state of loading the first batch. | | ||
| itemsCount | `number` | | Number of items fetched so far. | | ||
|
||
### Methods | ||
|
||
- **connect**(collectionViewer: [`CollectionViewer`](https://material.angular.io/cdk/collections/api)): [`Observable`](https://rxjs.dev/api/index/class/Observable)`<T>`<br/> | ||
Called by the virtual scroll viewport to receive a stream that emits the data array that should be rendered. | ||
- collectionViewer:_ [`CollectionViewer`](https://material.angular.io/cdk/collections/api) - collection viewer providing view changes that are listened to so that next batch can be fetched | ||
- **Returns** [`Observable`](https://rxjs.dev/api/index/class/Observable)`<T>` - Data stream containing fetched items. | ||
- **disconnect**(): void<br/> | ||
Called when viewport is destroyed, disconnects the datasource, unsubscribes from the view changes. | ||
- **reset**(): void<br/> | ||
Resets the datasource by fetching the first batch. |
18 changes: 18 additions & 0 deletions
18
lib/content-services/src/lib/infinite-scroll-datasource/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/*! | ||
* @license | ||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export * from './public-api'; |
159 changes: 159 additions & 0 deletions
159
lib/content-services/src/lib/infinite-scroll-datasource/infinite-scroll-datasource.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/*! | ||
* @license | ||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { ContentPagingQuery } from '@alfresco/js-api'; | ||
import { ScrollingModule } from '@angular/cdk/scrolling'; | ||
import { Component, OnInit } from '@angular/core'; | ||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; | ||
import { By } from '@angular/platform-browser'; | ||
import { TranslateModule } from '@ngx-translate/core'; | ||
import { from, Observable } from 'rxjs'; | ||
import { ContentTestingModule } from '../testing/content.testing.module'; | ||
import { InfiniteScrollDatasource } from './infinite-scroll-datasource'; | ||
|
||
class TestData { | ||
testId: number; | ||
testDescription: string; | ||
|
||
constructor(input?: Partial<TestData>) { | ||
if (input) { | ||
Object.assign(this, input); | ||
} | ||
} | ||
} | ||
|
||
class TestDataSource extends InfiniteScrollDatasource<TestData> { | ||
testDataBatch1: TestData[] = [ | ||
{ | ||
testId: 1, | ||
testDescription: 'test1' | ||
}, | ||
{ | ||
testId: 2, | ||
testDescription: 'test2' | ||
}, | ||
{ | ||
testId: 3, | ||
testDescription: 'test3' | ||
}, | ||
{ | ||
testId: 4, | ||
testDescription: 'test4' | ||
} | ||
]; | ||
testDataBatch2: TestData[] = [ | ||
{ | ||
testId: 5, | ||
testDescription: 'test5' | ||
}, | ||
{ | ||
testId: 6, | ||
testDescription: 'test6' | ||
} | ||
]; | ||
|
||
getNextBatch(pagingOptions: ContentPagingQuery): Observable<TestData[]> { | ||
if (pagingOptions.skipCount === 4) { | ||
return from([this.testDataBatch2]); | ||
} else if (pagingOptions.skipCount === 0) { | ||
return from([this.testDataBatch1]); | ||
} else { | ||
return from([]); | ||
} | ||
} | ||
} | ||
|
||
@Component({ | ||
template: ` <cdk-virtual-scroll-viewport appendOnly itemSize="300" style="height: 500px; width: 100%;"> | ||
<div *cdkVirtualFor="let item of testDatasource" class="test-item" style="display: block; height: 100%; width: 100%;"> | ||
{{ item.testDescription }} | ||
</div> | ||
</cdk-virtual-scroll-viewport>` | ||
}) | ||
class TestComponent implements OnInit { | ||
testDatasource = new TestDataSource(); | ||
|
||
ngOnInit() { | ||
this.testDatasource.batchSize = 4; | ||
} | ||
} | ||
|
||
describe('InfiniteScrollDatasource', () => { | ||
let fixture: ComponentFixture<TestComponent>; | ||
let component: TestComponent; | ||
|
||
const getRenderedItems = (): HTMLDivElement[] => fixture.debugElement.queryAll(By.css('.test-item')).map(element => element.nativeElement); | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [TranslateModule.forRoot(), ContentTestingModule, ScrollingModule], | ||
declarations: [TestComponent] | ||
}); | ||
fixture = TestBed.createComponent(TestComponent); | ||
component = fixture.componentInstance; | ||
}); | ||
|
||
it('should connect to the datasource and fetch first batch of items on init', async () => { | ||
spyOn(component.testDatasource, 'connect').and.callThrough(); | ||
spyOn(component.testDatasource, 'getNextBatch').and.callThrough(); | ||
fixture.autoDetectChanges(); | ||
await fixture.whenStable(); | ||
await fixture.whenRenderingDone(); | ||
|
||
expect(component.testDatasource.connect).toHaveBeenCalled(); | ||
expect(component.testDatasource.itemsCount).toBe(4); | ||
expect(component.testDatasource.getNextBatch).toHaveBeenCalledWith({ skipCount: 0, maxItems: 4 }); | ||
const renderedItems = getRenderedItems(); | ||
// only 3 elements fit the viewport | ||
expect(renderedItems.length).toBe(3); | ||
expect(renderedItems[0].innerText).toBe('test1'); | ||
expect(renderedItems[2].innerText).toBe('test3'); | ||
}); | ||
|
||
it('should load next batch when user scrolls towards the end of the list', fakeAsync(() => { | ||
fixture.autoDetectChanges(); | ||
const stable = fixture.whenStable(); | ||
const renderingDone = fixture.whenRenderingDone(); | ||
Promise.all([stable, renderingDone]).then(() => { | ||
spyOn(component.testDatasource, 'getNextBatch').and.callThrough(); | ||
const viewport = fixture.debugElement.query(By.css('cdk-virtual-scroll-viewport')).nativeElement; | ||
viewport.scrollTop = 400; | ||
tick(100); | ||
|
||
const renderedItems = getRenderedItems(); | ||
expect(component.testDatasource.getNextBatch).toHaveBeenCalledWith({ skipCount: 4, maxItems: 4 }); | ||
expect(component.testDatasource.itemsCount).toBe(6); | ||
expect(renderedItems[3].innerText).toBe('test4'); | ||
}); | ||
})); | ||
|
||
it('should reset the datastream and fetch first batch on reset', fakeAsync(() => { | ||
fixture.autoDetectChanges(); | ||
const stable = fixture.whenStable(); | ||
const renderingDone = fixture.whenRenderingDone(); | ||
Promise.all([stable, renderingDone]).then(() => { | ||
spyOn(component.testDatasource, 'getNextBatch').and.callThrough(); | ||
component.testDatasource.reset(); | ||
tick(100); | ||
|
||
const renderedItems = getRenderedItems(); | ||
expect(component.testDatasource.getNextBatch).toHaveBeenCalledWith({ skipCount: 0, maxItems: 4 }); | ||
expect(renderedItems.length).toBe(3); | ||
expect(renderedItems[2].innerText).toBe('test3'); | ||
}); | ||
})); | ||
}); |
82 changes: 82 additions & 0 deletions
82
lib/content-services/src/lib/infinite-scroll-datasource/infinite-scroll-datasource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/*! | ||
* @license | ||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { ContentPagingQuery } from '@alfresco/js-api'; | ||
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; | ||
import { BehaviorSubject, forkJoin, Observable, Subject, Subscription } from 'rxjs'; | ||
import { take, tap } from 'rxjs/operators'; | ||
|
||
export abstract class InfiniteScrollDatasource<T> extends DataSource<T> { | ||
protected readonly dataStream = new BehaviorSubject<T[]>([]); | ||
private isLoading$ = new Subject<boolean>(); | ||
private subscription = new Subscription(); | ||
private batchesFetched = 0; | ||
private _itemsCount = 0; | ||
private _firstItem: T; | ||
|
||
/* Determines size of each batch to be fetched */ | ||
batchSize = 100; | ||
|
||
/* Observable with initial and on reset loading state */ | ||
isLoading = this.isLoading$.asObservable(); | ||
|
||
get itemsCount(): number { | ||
return this._itemsCount; | ||
} | ||
|
||
get firstItem(): T { | ||
return this._firstItem; | ||
} | ||
|
||
abstract getNextBatch(pagingOptions: ContentPagingQuery): Observable<T[]>; | ||
|
||
connect(collectionViewer: CollectionViewer): Observable<T[]> { | ||
this.reset(); | ||
this.subscription.add( | ||
collectionViewer.viewChange.subscribe((range) => { | ||
if (this.batchesFetched * this.batchSize <= range.end) { | ||
forkJoin([ | ||
this.dataStream.asObservable().pipe(take(1)), | ||
this.getNextBatch({ skipCount: this.batchSize * this.batchesFetched, maxItems: this.batchSize }).pipe( | ||
take(1), | ||
tap((nextBatch) => (this._itemsCount += nextBatch.length)) | ||
) | ||
]).subscribe((batchesArray) => this.dataStream.next([...batchesArray[0], ...batchesArray[1]])); | ||
this.batchesFetched += 1; | ||
} | ||
}) | ||
); | ||
return this.dataStream; | ||
} | ||
|
||
disconnect(): void { | ||
this.subscription.unsubscribe(); | ||
} | ||
|
||
reset(): void { | ||
this.isLoading$.next(true); | ||
this.getNextBatch({ skipCount: 0, maxItems: this.batchSize }) | ||
.pipe(take(1)) | ||
.subscribe((firstBatch) => { | ||
this._itemsCount = firstBatch.length; | ||
this._firstItem = firstBatch[0]; | ||
this.dataStream.next(firstBatch); | ||
this.isLoading$.next(false); | ||
}); | ||
this.batchesFetched = 1; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
lib/content-services/src/lib/infinite-scroll-datasource/public-api.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/*! | ||
* @license | ||
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export * from './infinite-scroll-datasource'; |
Oops, something went wrong.