diff --git a/packages/demo/src/examples/slickgrid/example40.html b/packages/demo/src/examples/slickgrid/example40.html
new file mode 100644
index 000000000..40dd695f1
--- /dev/null
+++ b/packages/demo/src/examples/slickgrid/example40.html
@@ -0,0 +1,71 @@
+
+
+ Example 40: Infinite Scroll from JSON data
+
+
+ code
+
+
+
+
+
+
+
+ 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.
+
+ NOTES: presets.pagination
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).
+
+
+
+
+
+
+
+
+ Clear all Filter & Sorts
+
+
+ Set Filters Dynamically
+
+
+ Set Sorting Dynamically
+
+
+ Group by Duration
+
+
+ Reset Dataset onSort
:
+
+ ON
+
+
+ OFF
+
+
+
+
+
+ Metrics:
+
+ ${metrics.endTime | dateFormat: 'DD MMM, h:mm:ss a'} —
+ ${metrics.totalItemCount}
+ items
+
+
+
+
+
+
diff --git a/packages/demo/src/examples/slickgrid/example40.ts b/packages/demo/src/examples/slickgrid/example40.ts
new file mode 100644
index 000000000..d31964452
--- /dev/null
+++ b/packages/demo/src/examples/slickgrid/example40.ts
@@ -0,0 +1,164 @@
+import {
+ type AureliaGridInstance,
+ Aggregators,
+ type Column,
+ FieldType,
+ Formatters,
+ type GridOption,
+ type Grouping,
+ type Metrics,
+ type OnRowCountChangedEventArgs,
+ SortComparers,
+ SortDirectionNumber,
+} from 'aurelia-slickgrid';
+
+const FETCH_SIZE = 50;
+
+export class Example40 {
+ aureliaGrid: AureliaGridInstance;
+ columnDefinitions!: Column[];
+ dataset: any[] = [];
+ gridOptions!: GridOption;
+ metrics!: Partial;
+ scrollEndCalled = false;
+ shouldResetOnSort = false;
+
+ constructor() {
+ this.defineGrid();
+ this.dataset = this.loadData(0, FETCH_SIZE);
+ this.metrics = {
+ itemCount: FETCH_SIZE,
+ totalItemCount: FETCH_SIZE,
+ };
+ }
+
+ aureliaGridReady(aureliaGrid: AureliaGridInstance) {
+ this.aureliaGrid = aureliaGrid;
+ }
+
+ 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: 'percentComplete', 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',
+ rightPadding: 10
+ },
+ enableAutoResize: true,
+ enableFiltering: true,
+ enableGrouping: true,
+ editable: false,
+ rowHeight: 33,
+ };
+ }
+
+ // add onScroll listener which will detect when we reach the scroll end
+ // if so, then append items to the dataset
+ handleOnScroll(args: any) {
+ const viewportElm = args.grid.getViewportNode();
+ if (
+ ['mousewheel', 'scroll'].includes(args.triggeredBy || '')
+ && !this.scrollEndCalled
+ && viewportElm.scrollTop > 0
+ && Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight
+ ) {
+ console.log('onScroll end reached, add more items');
+ const startIdx = this.aureliaGrid.dataView?.getItemCount() || 0;
+ const newItems = this.loadData(startIdx, FETCH_SIZE);
+ this.aureliaGrid.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.aureliaGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
+ this.aureliaGrid.dataView?.setItems(newData);
+ this.aureliaGrid.dataView?.reSort();
+ }
+ }
+
+ groupByDuration() {
+ this.aureliaGrid?.dataView?.setGrouping({
+ getter: 'duration',
+ formatter: (g) => `Duration: ${g.value} (${g.count} items) `,
+ 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.aureliaGrid?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]);
+ this.aureliaGrid?.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: boolean) {
+ this.shouldResetOnSort = shouldReset;
+ }
+
+ clearAllFiltersAndSorts() {
+ if (this.aureliaGrid?.gridService) {
+ this.aureliaGrid.gridService.clearAllFiltersAndSorts();
+ }
+ }
+
+ setFiltersDynamically() {
+ // we can Set Filters Dynamically (or different filters) afterward through the FilterService
+ this.aureliaGrid?.filterService.updateFilters([
+ { columnId: 'percentComplete', searchTerms: ['50'], operator: '>=' },
+ ]);
+ }
+
+ refreshMetrics(args: OnRowCountChangedEventArgs) {
+ if (this.aureliaGrid && args?.current >= 0) {
+ this.metrics.itemCount = this.aureliaGrid.dataView?.getFilteredItemCount() || 0;
+ this.metrics.totalItemCount = args.itemCount || 0;
+ }
+ }
+
+ setSortingDynamically() {
+ this.aureliaGrid?.sortService.updateSorting([
+ { columnId: 'title', direction: 'DESC' },
+ ]);
+ }
+}
diff --git a/packages/demo/src/my-app.ts b/packages/demo/src/my-app.ts
index 636b4e7a6..c2810c515 100644
--- a/packages/demo/src/my-app.ts
+++ b/packages/demo/src/my-app.ts
@@ -44,6 +44,7 @@ export class MyApp {
{ path: 'example37', component: () => import('./examples/slickgrid/example37'), title: '37- Footer Totals Row' },
{ path: 'example38', component: () => import('./examples/slickgrid/example38'), title: '38- Infinite Scroll with OData' },
{ path: 'example39', component: () => import('./examples/slickgrid/example39'), title: '39- Infinite Scroll with GraphQL' },
+ { path: 'example40', component: () => import('./examples/slickgrid/example40'), title: '40- Infinite Scroll from JSON data' },
{ path: 'home', component: () => import('./home-page'), title: 'Home' },
];
diff --git a/test/cypress/e2e/example40.cy.ts b/test/cypress/e2e/example40.cy.ts
new file mode 100644
index 000000000..cbc166bc7
--- /dev/null
+++ b/test/cypress/e2e/example40.cy.ts
@@ -0,0 +1,110 @@
+describe('Example 40 - Infinite Scroll from JSON data', () => {
+ const GRID_ROW_HEIGHT = 33;
+ const titles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+
+ it('should display Example title', () => {
+ cy.visit(`${Cypress.config('baseUrl')}/example40`);
+ cy.get('h2').should('contain', 'Example 40: Infinite Scroll from JSON data');
+ });
+
+ it('should have exact Column Titles in the grid', () => {
+ cy.get('#grid40')
+ .find('.slick-header-columns')
+ .children()
+ .each(($child, index) => expect($child.text()).to.eq(titles[index]));
+ });
+
+ it('should expect first row to include "Task 0" and other specific properties', () => {
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains(/[0-9]/);
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains(/[0-9]/);
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/);
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/);
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).find('.mdi.mdi-check').should('have.length', 1);
+ });
+
+ it('should scroll to bottom of the grid and expect next batch of 50 items appended to current dataset for a total of 100 items', () => {
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '50');
+
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('bottom');
+
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '100');
+ });
+
+ it('should scroll to bottom of the grid again and expect 50 more items for a total of now 150 items', () => {
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '100');
+
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('bottom');
+
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '150');
+ });
+
+ it('should disable onSort for data reset and expect same dataset length of 150 items after sorting by Title', () => {
+ cy.get('[data-test="onsort-off"]').click();
+
+ cy.get('[data-id="title"]')
+ .click();
+
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '150');
+
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('top');
+
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 10');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 100');
+ });
+
+ it('should enable onSort for data reset and expect dataset to be reset to 50 items after sorting by Title', () => {
+ cy.get('[data-test="onsort-on"]').click();
+
+ cy.get('[data-id="title"]')
+ .click();
+
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '50');
+
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('top');
+
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 9');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 8');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 7');
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 6');
+ });
+
+ it('should "Group by Duration" and expect 50 items grouped', () => {
+ cy.get('[data-test="group-by-duration"]').click();
+
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '50');
+
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('top');
+
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/);
+ });
+
+ it('should scroll to the bottom "Group by Duration" and expect 50 more items for a total of 100 items grouped', () => {
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('bottom');
+
+ cy.get('[data-test="totalItemCount"]')
+ .should('have.text', '100');
+
+ cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
+ .scrollTo('top');
+
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
+ cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/);
+ });
+});