Skip to content

Commit

Permalink
feat: add Infinite Scroll with local JSON data
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Aug 1, 2024
1 parent b2ff957 commit ef52d3f
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 5 deletions.
3 changes: 3 additions & 0 deletions docs/grid-functionalities/infinite-scroll.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ In its simplest form, the more the user scrolls down, the more rows will get loa

[GraphQL Backend Service - Demo Page](https://ghiscoding.github.io/aurelia-slickgrid/#/slickgrid/example27) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/tree/master/src/examples/slickgrid/example27.ts)

> ![WARNING]
> Pagination Grid Preset (`presets.pagination`) is **not** supported with Infinite Scroll
## 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).
Expand Down
2 changes: 2 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import Example24 from './examples/example24';
import Example25 from './examples/example25';
import Example26 from './examples/example26';
import Example27 from './examples/example27';
import Example28 from './examples/example28';

export class AppRouting {
constructor(private config: RouterConfig) {
Expand Down Expand Up @@ -61,6 +62,7 @@ export class AppRouting {
{ route: 'example25', name: 'example25', view: './examples/example25.html', viewModel: Example25, title: 'Example25', },
{ route: 'example26', name: 'example26', view: './examples/example26.html', viewModel: Example26, title: 'Example26', },
{ route: 'example27', name: 'example27', view: './examples/example27.html', viewModel: Example27, title: 'Example27', },
{ route: 'example28', name: 'example28', view: './examples/example28.html', viewModel: Example28, title: 'Example28', },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' }
];
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ <h4 class="is-size-4 has-text-white">Slickgrid-Universal</h4>
<a class="navbar-item" onclick.delegate="loadRoute('example27')">
Example27 - GraphQL with Infinite Scroll
</a>
<a class="navbar-item" onclick.delegate="loadRoute('example28')">
Example28 - Infinite Scroll from JSON data
</a>
</div>
</div>
</div>
Expand Down
5 changes: 0 additions & 5 deletions examples/vite-demo-vanilla-bundle/src/examples/example26.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,4 @@ export default class Example26 {
{ columnId: 'name', direction: 'DESC' },
]);
}

throwPageChangeError() {
this.isPageErrorTest = true;
this.sgb.paginationService.goToLastPage();
}
}
60 changes: 60 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example28.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<div class="demo26">
<h3 class="title is-3">
Example 28 - Infinite Scroll from JSON data
<div class="subtitle code-link">
<span class="is-size-6">see</span>
<a class="is-size-5"
target="_blank"
href="https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/vite-demo-vanilla-bundle/src/examples/example26.ts">
<span class="mdi mdi-link-variant"></span> code
</a>
</div>
</h3>

<h6 class="title is-6 italic content">
<ul>
<li>
Infinite scrolling allows the grid to lazy-load rows from the server when reaching the scroll bottom (end) position.
In its simplest form, the more the user scrolls down, the more rows get loaded.
</li>
<li>NOTES: <code>presets.pagination</code> is not supported with Infinite Scroll and will revert to the first page,
simply because since we keep appending data, we always have to start from index zero (no offset).
</li>
</ul>
</h6>

<div class="row">
<button class="button is-small" data-test="clear-filters-sorting"
onclick.delegate="clearAllFiltersAndSorts()" title="Clear all Filters & Sorts">
<span class="mdi mdi-close"></span>
<span>Clear all Filter & Sorts</span>
</button>
<button class="button is-small" data-test="set-dynamic-filter" onclick.delegate="setFiltersDynamically()">
Set Filters Dynamically
</button>
<button class="button is-small" data-test="set-dynamic-sorting" onclick.delegate="setSortingDynamically()">
Set Sorting Dynamically
</button>
<button class="button is-small" data-test="group-by-duration" onclick.delegate="groupByDuration()">
Group by Duration
</button>

<label class="ml-4">Reset Dataset <code>onSort</code>:</label>
<button class="button is-small" data-test="onsort-on" onclick.delegate="onSortReset(true)">
ON
</button>
<button class="button is-small" data-test="onsort-off" onclick.delegate="onSortReset(false)">
OFF
</button>
</div>

<div class="mt-3 mb-2">
<b>Metrics:</b>
<span textcontent.bind="metricsEndTime"></span>
<span textcontent.bind="metricsItemCount" data-test="itemCount"></span> of
<span textcontent.bind="metricsTotalItemCount" data-test="totalItemCount"></span> items
</div>

<div class="grid28">
</div>
</div>
197 changes: 197 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example28.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { BindingEventService } from '@slickgrid-universal/binding';
import { Aggregators, type Column, FieldType, Formatters, type GridOption, type Grouping, type OnRowCountChangedEventArgs, SortComparers, SortDirectionNumber, } from '@slickgrid-universal/common';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';

import { ExampleGridOptions } from './example-grid-options';
import './example26.scss';

const FETCH_SIZE = 50;

export default class Example28 {
private _bindingEventService: BindingEventService;
columnDefinitions: Column[];
gridOptions: GridOption;
scrollEndCalled = false;
shouldResetOnSort = false;
metricsEndTime = '';
metricsItemCount = 0;
metricsTotalItemCount = 0;
sgb: SlickVanillaGridBundle;

odataQuery = '';
processing = false;
errorStatus = '';
errorStatusClass = 'hidden';
status = '';
statusClass = 'is-success';
isPageErrorTest = false;

constructor() {
this._bindingEventService = new BindingEventService();
this.resetAllStatus();
}

attached() {
this.defineGrid();
const gridContainerElm = document.querySelector(`.grid28`) as HTMLDivElement;
const dataset = this.loadData(0, FETCH_SIZE);

this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, dataset);
this.metricsItemCount = FETCH_SIZE;
this.metricsTotalItemCount = FETCH_SIZE;


// bind any of the grid events
this._bindingEventService.bind(gridContainerElm, 'onrowcountchanged', this.refreshMetrics.bind(this) as EventListener);
this._bindingEventService.bind(gridContainerElm, 'onsort', this.handleOnSort.bind(this));
this._bindingEventService.bind(gridContainerElm, 'onscroll', this.handleOnScroll.bind(this));
}

dispose() {
if (this.sgb) {
this.sgb?.dispose();
}
this._bindingEventService.unbindAll();
this.resetAllStatus();
}

resetAllStatus() {
this.status = '';
this.errorStatus = '';
this.statusClass = 'is-success';
this.errorStatusClass = 'hidden';
}

defineGrid() {
this.columnDefinitions = [
{ id: 'title', name: 'Title', field: 'title', sortable: true, minWidth: 100, filterable: true },
{ id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
{ id: '%', name: '% Complete', field: 'percentComplete', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
{ id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, exportWithFormatter: true, filterable: true },
{ id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, exportWithFormatter: true, filterable: true },
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', sortable: true, minWidth: 100, filterable: true, formatter: Formatters.checkmarkMaterial }
];

this.gridOptions = {
autoResize: {
container: '.demo-container',
},
enableAutoResize: true,
enableFiltering: true,
editable: false,
rowHeight: 33,
presets: {
// NOTE: pagination preset is NOT supported with infinite scroll
// filters: [{ columnId: 'gender', searchTerms: ['female'] }]
},
};
}

handleOnSort() {
// reset data loaded
if (this.shouldResetOnSort) {
const newData = this.loadData(0, FETCH_SIZE);
this.sgb.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
this.sgb.dataView?.setItems(newData);
this.sgb.dataView?.reSort();
}
}

// add onScroll listener to append items to the dataset whenever reaching the scroll bottom (scroll end)
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
) {
this.scrollEndCalled = true;
this.handleOnScrollEnd();
}
}

handleOnScrollEnd() {
console.log('onScroll end reached, add more items');
const startIdx = this.sgb.dataView?.getItemCount() || 0;
const newItems = this.loadData(startIdx, FETCH_SIZE);
this.sgb.dataView?.addItems(newItems);
this.scrollEndCalled = false;
}

groupByDuration() {
this.sgb?.dataView?.setGrouping({
getter: 'duration',
formatter: (g) => `Duration: ${g.value} <span class="text-green">(${g.count} items)</span>`,
comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc),
aggregators: [
new Aggregators.Avg('percentComplete'),
new Aggregators.Sum('cost')
],
aggregateCollapsed: false,
lazyTotalsCalculation: true
} as Grouping);

// you need to manually add the sort icon(s) in UI
this.sgb?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]);
this.sgb?.slickGrid?.invalidate(); // invalidate all rows and re-render
}

loadData(startIdx: number, count: number) {
const tmpData: any[] = [];
for (let i = startIdx; i < startIdx + count; i++) {
tmpData.push(this.newItem(i));
}

return tmpData;
}

newItem(idx: number) {
const randomYear = 2000 + Math.floor(Math.random() * 10);
const randomMonth = Math.floor(Math.random() * 11);
const randomDay = Math.floor((Math.random() * 29));
const randomPercent = Math.round(Math.random() * 100);

return {
id: idx,
title: 'Task ' + idx,
duration: Math.round(Math.random() * 100) + '',
percentComplete: randomPercent,
start: new Date(randomYear, randomMonth + 1, randomDay),
finish: new Date(randomYear + 1, randomMonth + 1, randomDay),
effortDriven: (idx % 5 === 0)
};
}

onSortReset(shouldReset) {
this.shouldResetOnSort = shouldReset;
}

clearAllFiltersAndSorts() {
if (this.sgb?.gridService) {
this.sgb.gridService.clearAllFiltersAndSorts();
}
}

setFiltersDynamically() {
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
this.sgb?.filterService.updateFilters([
{ columnId: 'percentComplete', searchTerms: ['>=50'] },
]);
}

refreshMetrics(event: CustomEvent<{ args: OnRowCountChangedEventArgs; }>) {
const args = event?.detail?.args;
if (args?.current >= 0) {
this.metricsItemCount = this.sgb.dataView?.getFilteredItemCount() || 0;
this.metricsTotalItemCount = args.itemCount || 0;
}
}

setSortingDynamically() {
this.sgb?.sortService.updateSorting([
{ columnId: 'title', direction: 'DESC' },
]);
}
}
13 changes: 13 additions & 0 deletions test/cypress/e2e/example26.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,18 @@ describe('Example 26 - OData with Infinite Scroll', () => {
expect($span.text()).to.eq(`$count=true&$top=30&$skip=30&$orderby=Name asc&$filter=(Gender eq 'female')`);
});
});

it('should "Group by Gender" and expect 50 items grouped', () => {
cy.get('[data-test="group-by-gender"]').click();

cy.get('[data-test="itemCount"]')
.should('have.text', '50');

cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
.scrollTo('top');

cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Gender: [female|male]/);
});
});
});
Loading

0 comments on commit ef52d3f

Please sign in to comment.