Skip to content
This repository was archived by the owner on Jun 1, 2025. It is now read-only.

Commit d1ff9f2

Browse files
authored
Merge pull request #1449 from ghiscoding/feat/infinite-scroll
feat: Infinite Scroll for Backend Services (OData/GraphQL)
2 parents e9e1766 + 0244330 commit d1ff9f2

20 files changed

+1840
-122
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ npm install angular-slickgrid
3939
- [Bootstrap 5 (single Locale)](https://github.com/ghiscoding/angular-slickgrid-demos/tree/master/bootstrap5-demo-with-locales) / [examples repo](https://github.com/ghiscoding/angular-slickgrid-demos/tree/master/bootstrap5-demo-with-locales) - Code Sample with a single Locale (without `ngx-translate`)
4040

4141
#### Working Demo
42-
For a complete set of working demos (over 30 examples), we strongly suggest you clone [Angular-Slickgrid Demos](https://github.com/ghiscoding/angular-slickgrid-demos) repository (instructions are provided in the demo repo). The repo provides multiple demos and they are updated for every new project release, so it is updated frequently and is also used as the GitHub live demo page for both the [Bootstrap 5 demo](https://ghiscoding.github.io/Angular-Slickgrid) and [Bootstrap 5 demo (single Locale)](https://ghiscoding.github.io/angular-slickgrid-demos).
42+
For a complete set of working demos (40+ examples), we strongly suggest you clone [Angular-Slickgrid Demos](https://github.com/ghiscoding/angular-slickgrid-demos) repository (instructions are provided in the demo repo). The repo provides multiple demos and they are updated for every new project release, so it is updated frequently and is also used as the GitHub live demo page for both the [Bootstrap 5 demo](https://ghiscoding.github.io/Angular-Slickgrid) and [Bootstrap 5 demo (single Locale)](https://ghiscoding.github.io/angular-slickgrid-demos).
4343

4444
```sh
4545
git clone https://github.com/ghiscoding/angular-slickgrid-demos

docs/TOC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
* [Grid State & Presets](grid-functionalities/Grid-State-&-Preset.md)
5959
* [Grouping & Aggregators](grid-functionalities/grouping-and-aggregators.md)
6060
* [Header Menu & Header Buttons](grid-functionalities/Header-Menu-&-Header-Buttons.md)
61+
* [Infinite Scroll](grid-functionalities/infinite-scroll.md)
6162
* [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md)
6263
* [Providing data to the grid](grid-functionalities/providing-grid-data.md)
6364
* [Row Detail](grid-functionalities/row-detail.md)

docs/backend-services/GraphQL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [Pagination](graphql/GraphQL-Pagination.md)
77
- [Sorting](graphql/GraphQL-Sorting.md)
88
- [Filtering](graphql/GraphQL-Filtering.md)
9+
- [Infinite Scroll](../grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services)
910

1011
### Description
1112
GraphQL Backend Service (for Pagination purposes) to get data from a backend server with the help of GraphQL.
@@ -14,7 +15,7 @@ GraphQL Backend Service (for Pagination purposes) to get data from a backend ser
1415
[Demo Page](https://ghiscoding.github.io/Angular-Slickgrid/#/gridgraphql) / [Demo Component](https://github.com/ghiscoding/angular-slickgrid/blob/master/src/app/examples/grid-graphql.component.ts)
1516

1617
### Note
17-
You can use it when you need to support **Pagination** (though you could disable Pagination if you wish), that is when your dataset is rather large and has typically more than 5k rows, with a GraphQL endpoint. If your dataset is small (less than 5k rows), then you might be better off with [regular grid](https://ghiscoding.github.io/Angular-Slickgrid/#/basic) with the "dataset.bind" property. SlickGrid can easily handle million of rows using a DataView object, but personally when the dataset is known to be large, I usually use a backend service (OData or GraphQL) and when it's small I go with a [regular grid](https://ghiscoding.github.io/Angular-Slickgrid/#/basic).
18+
You can use it when you need to support **Pagination** (though you could disable Pagination if you wish), that is when your dataset is rather large and has typically more than 5k rows, with a GraphQL endpoint. If your dataset is small (less than 5k rows), then you might be better off with [regular grid](https://ghiscoding.github.io/Angular-Slickgrid/#/basic) with the "dataset.bind" property. SlickGrid can easily handle million of rows using a DataView object, but personally when the dataset is known to be large, I usually use a backend service (OData or GraphQL) and when it's small I go with a [regular grid](https://ghiscoding.github.io/Angular-Slickgrid/#/basic).
1819

1920
## Implementation
2021
To connect a backend service into `Slickgrid-Universal`, you simply need to modify your `gridOptions` and add a declaration of `backendServiceApi`. See below for the signature and an example further down below.

docs/backend-services/OData.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- [Passing Extra Arguments](#passing-extra-arguments-to-the-query)
55
- [OData options](#odata-options)
66
- [Override the filter query](#override-the-filter-query)
7+
- [Infinite Scroll](../grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services)
78

89
### Description
910
OData Backend Service (for Pagination purposes) to get data from a backend server with the help of OData.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
## Description
2+
3+
Infinite scrolling allows the grid to lazy-load rows from the server (or locally) when reaching the scroll bottom (end) position.
4+
In its simplest form, the more the user scrolls down, the more rows will get loaded and appended to the in-memory dataset.
5+
6+
### Demo
7+
[JSON Data - Demo Page](https://ghiscoding.github.io/Angular-Slickgrid/#/infinite-json) / [Demo ViewModel](https://github.com/ghiscoding/angular-slickgrid/blob/master/src/app/examples/grid-infinite-json.component.ts)
8+
9+
[OData Backend Service - Demo Page](https://ghiscoding.github.io/Angular-Slickgrid/#/infinite-odata) / [Demo ViewModel](https://github.com/ghiscoding/angular-slickgrid/blob/master/src/app/examples/grid-infinite-odata.component.ts)
10+
11+
[GraphQL Backend Service - Demo Page](https://ghiscoding.github.io/Angular-Slickgrid/#/infinite-graphql) / [Demo ViewModel](https://github.com/ghiscoding/angular-slickgrid/blob/master/src/app/examples/grid-infinite-graphql.component.ts)
12+
13+
> ![WARNING]
14+
> Pagination Grid Preset (`presets.pagination`) is **not** supported with Infinite Scroll
15+
16+
## Infinite Scroll with JSON data
17+
18+
As describe above, when used with a local JSON dataset, it will add data to the in-memory dataset whenever we scroll to the bottom until we reach the end of the dataset (if ever).
19+
20+
#### Code Sample
21+
When used with a local JSON dataset, the Infinite Scroll is a feature that must be implemented by yourself. You implement by subscribing to 1 main event (`onScroll`) and if you want to reset the data when Sorting then you'll also need to subscribe to the (`onSort`) event. So the idea is to have simple code in the `onScroll` event to detect when we reach the scroll end and then use the DataView `addItems()` to append data to the existing dataset (in-memory) and that's about it.
22+
23+
##### View
24+
```html
25+
<angular-slickgrid
26+
gridId="grid2"
27+
[columnDefinitions]="columnDefinitions"
28+
[gridOptions]="gridOptions"
29+
[dataset]="dataset"
30+
(onAngularGridCreated)="angularGridReady($event.detail)"
31+
(onScroll)="handleOnScroll($event.$detail.args)"
32+
(onSort)="handleOnSort()">
33+
</angular-slickgrid>
34+
```
35+
36+
```ts
37+
export class Example implements OnInit {
38+
scrollEndCalled = false;
39+
40+
// add onScroll listener which will detect when we reach the scroll end
41+
// if so, then append items to the dataset
42+
handleOnScroll(event) {
43+
const args = event.detail?.args;
44+
const viewportElm = args.grid.getViewportNode();
45+
if (
46+
['mousewheel', 'scroll'].includes(args.triggeredBy || '')
47+
&& !this.scrollEndCalled
48+
&& viewportElm.scrollTop > 0
49+
&& Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight
50+
) {
51+
// onScroll end reached, add more items
52+
// for demo purposes, we'll mock next subset of data at last id index + 1
53+
const startIdx = this.angularGrid.dataView?.getItemCount() || 0;
54+
const newItems = this.loadData(startIdx, FETCH_SIZE);
55+
this.angularGrid.dataView?.addItems(newItems);
56+
this.scrollEndCalled = false; //
57+
}
58+
}
59+
60+
// do we want to reset the dataset when Sorting?
61+
// if answering Yes then use the code below
62+
handleOnSort() {
63+
if (this.shouldResetOnSort) {
64+
const newData = this.loadData(0, FETCH_SIZE);
65+
this.angularGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
66+
this.angularGrid.dataView?.setItems(newData);
67+
this.angularGrid.dataView?.reSort();
68+
}
69+
}
70+
}
71+
```
72+
73+
---
74+
75+
## Infinite Scroll with Backend Services
76+
77+
As describe above, when used with the Backend Service API, it will add data to the in-memory dataset whenever we scroll to the bottom. However there is one thing to note that might surprise you which is that even if Pagination is hidden in the UI, but the fact is that behind the scene that is exactly what it uses (mainly the Pagination Service `.goToNextPage()` to fetch the next set of data).
78+
79+
#### Code Sample
80+
We'll use the OData Backend Service to demo Infinite Scroll with a Backend Service, however the implementation is similar for any Backend Services. The main difference with the Infinite Scroll implementation is around the `onProcess` and the callback that we use within (which is the `getCustomerCallback` in our use case). This callback will receive a data object that include the `infiniteScrollBottomHit` boolean property, this prop will be `true` only on the 2nd and more passes which will help us make a distinction between the first page load and any other subset of data to append to our in-memory dataset. With this property in mind, we'll assign the entire dataset on 1st pass with `this.dataset = data.value` (when `infiniteScrollBottomHit: false`) but for any other passes, we'll want to use the DataView `addItems()` to append data to the existing dataset (in-memory) and that's about it.
81+
82+
##### View
83+
```html
84+
<angular-slickgrid
85+
gridId="grid2"
86+
[columnDefinitions]="columnDefinitions"
87+
[gridOptions]="gridOptions"
88+
[dataset]="dataset"
89+
(onAngularGridCreated)="angularGridReady($event.detail)">
90+
</angular-slickgrid>
91+
```
92+
93+
```ts
94+
export class Example implements OnInit {
95+
initializeGrid() {
96+
this.columnDefinitions = [ /* ... */ ];
97+
98+
this.gridOptions = {
99+
presets: {
100+
// NOTE: pagination preset is NOT supported with infinite scroll
101+
// filters: [{ columnId: 'gender', searchTerms: ['female'] }]
102+
},
103+
backendServiceApi: {
104+
service: new GridOdataService(), // or any Backend Service
105+
options: {
106+
// enable infinite scroll via Boolean OR via { fetchSize: number }
107+
infiniteScroll: { fetchSize: 30 }, // or use true, in that case it would use default size of 25
108+
109+
preProcess: () => {
110+
this.displaySpinner(true);
111+
},
112+
process: (query) => this.getCustomerApiCall(query),
113+
postProcess: (response) => {
114+
this.displaySpinner(false);
115+
this.getCustomerCallback(response);
116+
},
117+
// we could use local in-memory Filtering (please note that it only filters against what is currently loaded)
118+
// that is when we want to avoid reloading the entire dataset every time
119+
// useLocalFiltering: true,
120+
} as OdataServiceApi,
121+
};
122+
}
123+
124+
// Web API call
125+
getCustomerApiCall(odataQuery) {
126+
return this.http.get(`/api/getCustomers?${odataQuery}`);
127+
}
128+
129+
getCustomerCallback(data: { '@odata.count': number; infiniteScrollBottomHit: boolean; metrics: Metrics; query: string; value: any[]; }) {
130+
// totalItems property needs to be filled for pagination to work correctly
131+
const totalItemCount: number = data['@odata.count'];
132+
this.metrics.totalItemCount = totalItemCount;
133+
134+
// even if we're not showing pagination, it is still used behind the scene to fetch next set of data (next page basically)
135+
// once pagination totalItems is filled, we can update the dataset
136+
137+
// infinite scroll has an extra data property to determine if we hit an infinite scroll and there's still more data (in that case we need append data)
138+
// or if we're on first data fetching (no scroll bottom ever occured yet)
139+
if (!data.infiniteScrollBottomHit) {
140+
// initial load not scroll hit yet, full dataset assignment
141+
this.angularGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
142+
this.dataset = data.value;
143+
this.metrics.itemCount = data.value.length;
144+
} else {
145+
// scroll hit, for better perf we can simply use the DataView directly for better perf (which is better compare to replacing the entire dataset)
146+
this.angularGrid.dataView?.addItems(data.value);
147+
}
148+
}
149+
}
150+
```

package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@
5050
},
5151
"dependencies": {
5252
"@ngx-translate/core": "^15.0.0",
53-
"@slickgrid-universal/common": "~5.4.0",
54-
"@slickgrid-universal/custom-footer-component": "~5.4.0",
55-
"@slickgrid-universal/empty-warning-component": "~5.4.0",
56-
"@slickgrid-universal/event-pub-sub": "~5.4.0",
57-
"@slickgrid-universal/pagination-component": "~5.4.0",
58-
"@slickgrid-universal/row-detail-view-plugin": "~5.4.0",
59-
"@slickgrid-universal/rxjs-observable": "~5.4.0",
53+
"@slickgrid-universal/common": "~5.5.0",
54+
"@slickgrid-universal/custom-footer-component": "~5.5.0",
55+
"@slickgrid-universal/empty-warning-component": "~5.5.0",
56+
"@slickgrid-universal/event-pub-sub": "~5.5.0",
57+
"@slickgrid-universal/pagination-component": "~5.5.0",
58+
"@slickgrid-universal/row-detail-view-plugin": "~5.5.0",
59+
"@slickgrid-universal/rxjs-observable": "~5.5.0",
6060
"dequal": "^2.0.3",
6161
"rxjs": "^7.8.1"
6262
},
@@ -86,12 +86,12 @@
8686
"@ngx-translate/http-loader": "^8.0.0",
8787
"@popperjs/core": "^2.11.8",
8888
"@release-it/conventional-changelog": "^8.0.1",
89-
"@slickgrid-universal/composite-editor-component": "~5.4.0",
90-
"@slickgrid-universal/custom-tooltip-plugin": "~5.4.0",
91-
"@slickgrid-universal/excel-export": "~5.4.0",
92-
"@slickgrid-universal/graphql": "~5.4.0",
93-
"@slickgrid-universal/odata": "~5.4.0",
94-
"@slickgrid-universal/text-export": "~5.4.0",
89+
"@slickgrid-universal/composite-editor-component": "~5.5.0",
90+
"@slickgrid-universal/custom-tooltip-plugin": "~5.5.0",
91+
"@slickgrid-universal/excel-export": "~5.5.0",
92+
"@slickgrid-universal/graphql": "~5.5.0",
93+
"@slickgrid-universal/odata": "~5.5.0",
94+
"@slickgrid-universal/text-export": "~5.5.0",
9595
"@types/dompurify": "^3.0.5",
9696
"@types/fnando__sparkline": "^0.3.7",
9797
"@types/jest": "^29.5.12",

src/app/app-routing.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { GridFrozenComponent } from './examples/grid-frozen.component';
1818
import { GridGraphqlComponent } from './examples/grid-graphql.component';
1919
import { GridGraphqlWithoutPaginationComponent } from './examples/grid-graphql-nopage.component';
2020
import { GridGroupingComponent } from './examples/grid-grouping.component';
21+
import { GridInfiniteGraphqlComponent } from './examples/grid-infinite-graphql.component';
22+
import { GridInfiniteOdataComponent } from './examples/grid-infinite-odata.component';
2123
import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component';
2224
import { GridHeaderFooterComponent } from './examples/grid-header-footer.component';
2325
import { GridHeaderMenuComponent } from './examples/grid-headermenu.component';
@@ -67,6 +69,8 @@ const routes: Routes = [
6769
{ path: 'draggrouping', component: GridDraggableGroupingComponent },
6870
{ path: 'grouping', component: GridGroupingComponent },
6971
{ path: 'header-footer', component: GridHeaderFooterComponent },
72+
{ path: 'infinite-graphql', component: GridInfiniteGraphqlComponent },
73+
{ path: 'infinite-odata', component: GridInfiniteOdataComponent },
7074
{ path: 'localization', component: GridLocalizationComponent },
7175
{ path: 'clientside', component: GridClientSideComponent },
7276
{ path: 'odata', component: GridOdataComponent },

src/app/app.component.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,16 @@
176176
37- Footer Totals Row
177177
</a>
178178
</li>
179+
<li class="nav-item">
180+
<a class="nav-link" routerLinkActive="active" [routerLink]="['/infinite-odata']">
181+
38- Infinite Scroll with OData
182+
</a>
183+
</li>
184+
<li class="nav-item">
185+
<a class="nav-link" routerLinkActive="active" [routerLink]="['/infinite-graphql']">
186+
39- Infinite Scroll with GraphQL
187+
</a>
188+
</li>
179189
</ul>
180190
</section>
181191

src/app/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { GridGraphqlWithoutPaginationComponent } from './examples/grid-graphql-n
3737
import { GridGroupingComponent } from './examples/grid-grouping.component';
3838
import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component';
3939
import { GridHeaderMenuComponent } from './examples/grid-headermenu.component';
40+
import { GridInfiniteGraphqlComponent } from './examples/grid-infinite-graphql.component';
41+
import { GridInfiniteOdataComponent } from './examples/grid-infinite-odata.component';
4042
import { GridLocalizationComponent } from './examples/grid-localization.component';
4143
import { GridMenuComponent } from './examples/grid-menu.component';
4244
import { GridOdataComponent } from './examples/grid-odata.component';
@@ -118,6 +120,8 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
118120
GridHeaderButtonComponent,
119121
GridHeaderFooterComponent,
120122
GridHeaderMenuComponent,
123+
GridInfiniteGraphqlComponent,
124+
GridInfiniteOdataComponent,
121125
GridLocalizationComponent,
122126
GridMenuComponent,
123127
GridOdataComponent,

0 commit comments

Comments
 (0)