Skip to content

Commit

Permalink
feat: Infinite Scroll for Backend Services (OData/GraphQL) (#386)
Browse files Browse the repository at this point in the history
* feat: Infinite Scroll for Backend Services (OData/GraphQL)
  • Loading branch information
ghiscoding authored Aug 7, 2024
1 parent 2bce7d9 commit 8dc8d7b
Show file tree
Hide file tree
Showing 15 changed files with 1,808 additions and 152 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Refer to the **[Docs - Quick Start](https://ghiscoding.gitbook.io/slickgrid-reac
- [Bootstrap 5 demo](https://ghiscoding.github.io/slickgrid-react) / [examples repo](https://github.com/ghiscoding/slickgrid-react-demos/tree/main/bootstrap5-i18n-demo)

#### Working Demos
For a complete set of working demos (over 30 examples), we strongly suggest you to clone the [Slickgrid-React Demos](https://github.com/ghiscoding/slickgrid-react-demos) repository (instructions are provided in the demo repo). The repo provides multiple demos and they are updated every time a new version is out, so it is updated frequently and is also used as the GitHub live demo page.
For a complete set of working demos (40+ examples), we strongly suggest you to clone the [Slickgrid-React Demos](https://github.com/ghiscoding/slickgrid-react-demos) repository (instructions are provided in the demo repo). The repo provides multiple demos and they are updated every time a new version is out, so it is updated frequently and is also used as the GitHub live demo page.

## License
[MIT License](LICENSE)
Expand Down
1 change: 1 addition & 0 deletions docs/TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
* [Grid State & Presets](grid-functionalities/grid-state-preset.md)
* [Grouping & Aggregators](grid-functionalities/grouping-aggregators.md)
* [Header Menu & Header Buttons](grid-functionalities/header-menu-header-buttons.md)
* [Infinite Scroll](grid-functionalities/infinite-scroll.md)
* [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md)
* [Providing data to the grid](grid-functionalities/providing-grid-data.md)
* [Row Selection](grid-functionalities/row-selection.md)
Expand Down
1 change: 1 addition & 0 deletions docs/backend-services/GraphQL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Pagination](graphql/GraphQL-Pagination.md)
- [Sorting](graphql/GraphQL-Sorting.md)
- [Filtering](graphql/GraphQL-Filtering.md)
- [Infinite Scroll](../grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services)

### Description
GraphQL Backend Service (for Pagination purposes) to get data from a backend server with the help of GraphQL.
Expand Down
1 change: 1 addition & 0 deletions docs/backend-services/OData.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [Passing Extra Arguments](#passing-extra-arguments-to-the-query)
- [OData options](#odata-options)
- [Override the filter query](#override-the-filter-query)
- [Infinite Scroll](../grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services)

### Description
OData Backend Service (for Pagination purposes) to get data from a backend server with the help of OData.
Expand Down
162 changes: 162 additions & 0 deletions docs/grid-functionalities/infinite-scroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
## Description

Infinite scrolling allows the grid to lazy-load rows from the server (or locally) when reaching the scroll bottom (end) position.
In its simplest form, the more the user scrolls down, the more rows will get loaded and appended to the in-memory dataset.

### Demo

[JSON Data - Demo Page](https://ghiscoding.github.io/slickgrid-react/#/slickgrid/Example38) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-react/blob/master/src/examples/slickgrid/Example38.tsx)

[OData Backend Service - Demo Page](https://ghiscoding.github.io/slickgrid-react/#/slickgrid/Example39) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-react/blob/master/src/examples/slickgrid/Example39.tsx)

[GraphQL Backend Service - Demo Page](https://ghiscoding.github.io/slickgrid-react/#/slickgrid/Example40) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-react/blob/master/src/examples/slickgrid/Example40.tsx)

> ![WARNING]
> Pagination Grid Preset (`presets.pagination`) is **not** supported with Infinite Scroll
## Infinite Scroll with JSON data

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).

#### Code Sample
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.

```tsx
export class Example {
scrollEndCalled = false;
reactGrid: SlickgridReactInstance;

reactGridReady(reactGrid: SlickgridReactInstance) {
this.reactGrid = reactGrid;
}

// add onScroll listener which will detect when we reach the scroll end
// if so, then append items to the dataset
handleOnScroll(event) {
const args = event.detail?.args;
const viewportElm = args.grid.getViewportNode();
if (
['mousewheel', 'scroll'].includes(args.triggeredBy || '')
&& !this.scrollEndCalled
&& viewportElm.scrollTop > 0
&& Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight
) {
// onScroll end reached, add more items
// for demo purposes, we'll mock next subset of data at last id index + 1
const startIdx = this.reactGrid.dataView?.getItemCount() || 0;
const newItems = this.loadData(startIdx, FETCH_SIZE);
this.reactGrid.dataView?.addItems(newItems);
this.scrollEndCalled = false; //
}
}

// do we want to reset the dataset when Sorting?
// if answering Yes then use the code below
handleOnSort() {
if (this.shouldResetOnSort) {
const newData = this.loadData(0, FETCH_SIZE);
this.reactGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
this.reactGrid.dataView?.setItems(newData);
this.reactGrid.dataView?.reSort();
}
}

render() {
return (
<SlickgridReact gridId="grid1"
columnDefinitions={this.state.columnDefinitions}
gridOptions={this.state.gridOptions}
dataset={this.state.dataset}
onReactGridCreated={$event => this.reactGridReady($event.detail)}
onScroll={$event => this.handleOnScroll($event.$detail.args)}
onSort={$event => this.handleOnSort())}
/>
);
}
}
```

---

## Infinite Scroll with Backend Services

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).

#### Code Sample
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.

```tsx
export class Example {
reactGrid: SlickgridReactInstance;

reactGridReady(reactGrid: SlickgridReactInstance) {
this.reactGrid = reactGrid;
}

initializeGrid() {
this.columnDefinitions = [ /* ... */ ];

this.gridOptions = {
presets: {
// NOTE: pagination preset is NOT supported with infinite scroll
// filters: [{ columnId: 'gender', searchTerms: ['female'] }]
},
backendServiceApi: {
service: new GridOdataService(), // or any Backend Service
options: {
// enable infinite scroll via Boolean OR via { fetchSize: number }
infiniteScroll: { fetchSize: 30 }, // or use true, in that case it would use default size of 25

preProcess: () => {
this.displaySpinner(true);
},
process: (query) => this.getCustomerApiCall(query),
postProcess: (response) => {
this.displaySpinner(false);
this.getCustomerCallback(response);
},
// we could use local in-memory Filtering (please note that it only filters against what is currently loaded)
// that is when we want to avoid reloading the entire dataset every time
// useLocalFiltering: true,
} as OdataServiceApi,
};
}

// Web API call
getCustomerApiCall(odataQuery) {
return this.http.get(`/api/getCustomers?${odataQuery}`);
}

getCustomerCallback(data: { '@odata.count': number; infiniteScrollBottomHit: boolean; metrics: Metrics; query: string; value: any[]; }) {
// totalItems property needs to be filled for pagination to work correctly
const totalItemCount: number = data['@odata.count'];
this.metrics.totalItemCount = totalItemCount;

// even if we're not showing pagination, it is still used behind the scene to fetch next set of data (next page basically)
// once pagination totalItems is filled, we can update the dataset

// 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)
// or if we're on first data fetching (no scroll bottom ever occured yet)
if (!data.infiniteScrollBottomHit) {
// initial load not scroll hit yet, full dataset assignment
this.reactGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
this.dataset = data.value;
this.metrics.itemCount = data.value.length;
} else {
// scroll hit, for better perf we can simply use the DataView directly for better perf (which is better compare to replacing the entire dataset)
this.reactGrid.dataView?.addItems(data.value);
}
}

render() {
return (
<SlickgridReact gridId="grid1"
columnDefinitions={this.state.columnDefinitions}
gridOptions={this.state.gridOptions}
dataset={this.state.dataset}
onReactGridCreated={$event => this.reactGridReady($event.detail)}
/>
);
}
}
```
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@
"/src/slickgrid-react"
],
"dependencies": {
"@slickgrid-universal/common": "~5.4.0",
"@slickgrid-universal/custom-footer-component": "~5.4.0",
"@slickgrid-universal/empty-warning-component": "~5.4.0",
"@slickgrid-universal/event-pub-sub": "~5.4.0",
"@slickgrid-universal/pagination-component": "~5.4.0",
"@slickgrid-universal/common": "~5.5.0",
"@slickgrid-universal/custom-footer-component": "~5.5.0",
"@slickgrid-universal/empty-warning-component": "~5.5.0",
"@slickgrid-universal/event-pub-sub": "~5.5.0",
"@slickgrid-universal/pagination-component": "~5.5.0",
"dequal": "^2.0.3",
"i18next": "^23.12.2",
"sortablejs": "^1.15.2"
Expand All @@ -99,18 +99,18 @@
"@formkit/tempo": "^0.1.2",
"@popperjs/core": "^2.11.8",
"@release-it/conventional-changelog": "^8.0.1",
"@slickgrid-universal/composite-editor-component": "~5.4.0",
"@slickgrid-universal/custom-tooltip-plugin": "~5.4.0",
"@slickgrid-universal/excel-export": "~5.4.0",
"@slickgrid-universal/graphql": "~5.4.0",
"@slickgrid-universal/odata": "~5.4.0",
"@slickgrid-universal/rxjs-observable": "~5.4.0",
"@slickgrid-universal/text-export": "~5.4.0",
"@slickgrid-universal/composite-editor-component": "~5.5.0",
"@slickgrid-universal/custom-tooltip-plugin": "~5.5.0",
"@slickgrid-universal/excel-export": "~5.5.0",
"@slickgrid-universal/graphql": "~5.5.0",
"@slickgrid-universal/odata": "~5.5.0",
"@slickgrid-universal/rxjs-observable": "~5.5.0",
"@slickgrid-universal/text-export": "~5.5.0",
"@types/dompurify": "^3.0.5",
"@types/fnando__sparkline": "^0.3.7",
"@types/i18next-xhr-backend": "^1.4.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.14",
"@types/node": "^22.1.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/sortablejs": "^1.15.8",
Expand Down Expand Up @@ -163,7 +163,7 @@
"style-loader": "4.0.0",
"ts-jest": "^29.2.4",
"ts-loader": "^9.5.1",
"typescript": "^5.5.3",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.1",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
Expand All @@ -176,4 +176,4 @@
"resolutions": {
"caniuse-lite": "1.0.30001649"
}
}
}
4 changes: 4 additions & 0 deletions src/examples/slickgrid/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import Example34 from './Example34';
import Example35 from './Example35';
import Example36 from './Example36';
import Example37 from './Example37';
import Example38 from './Example38';
import Example39 from './Example39';

const routes: Array<{ path: string; route: string; component: any; title: string; }> = [
{ path: 'example1', route: '/example1', component: <Example1 />, title: '1- Basic Grid / 2 Grids' },
Expand Down Expand Up @@ -74,6 +76,8 @@ const routes: Array<{ path: string; route: string; component: any; title: string
{ path: 'example35', route: '/example35', component: <Example35 />, title: '35- Row Based Editing' },
{ path: 'example36', route: '/example36', component: <Example36 />, title: '36- Excel Export Formulas' },
{ path: 'example37', route: '/example37', component: <Example37 />, title: '37- Footer Totals Row' },
{ path: 'example38', route: '/example38', component: <Example38 />, title: '38- Infinite Scroll with OData' },
{ path: 'example39', route: '/example39', component: <Example39 />, title: '39- Infinite Scroll with GraphQL' },
];

export default function Routes() {
Expand Down
Loading

0 comments on commit 8dc8d7b

Please sign in to comment.