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

Commit a8fd47d

Browse files
authored
Merge pull request #1450 from ghiscoding/feat/infinite-scroll-local-json
feat: Infinite Scroll for JSON data
2 parents d1ff9f2 + 3a78977 commit a8fd47d

File tree

6 files changed

+357
-0
lines changed

6 files changed

+357
-0
lines changed

src/app/app-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { GridGraphqlComponent } from './examples/grid-graphql.component';
1919
import { GridGraphqlWithoutPaginationComponent } from './examples/grid-graphql-nopage.component';
2020
import { GridGroupingComponent } from './examples/grid-grouping.component';
2121
import { GridInfiniteGraphqlComponent } from './examples/grid-infinite-graphql.component';
22+
import { GridInfiniteJsonComponent } from './examples/grid-infinite-json.component';
2223
import { GridInfiniteOdataComponent } from './examples/grid-infinite-odata.component';
2324
import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component';
2425
import { GridHeaderFooterComponent } from './examples/grid-header-footer.component';
@@ -70,6 +71,7 @@ const routes: Routes = [
7071
{ path: 'grouping', component: GridGroupingComponent },
7172
{ path: 'header-footer', component: GridHeaderFooterComponent },
7273
{ path: 'infinite-graphql', component: GridInfiniteGraphqlComponent },
74+
{ path: 'infinite-json', component: GridInfiniteJsonComponent },
7375
{ path: 'infinite-odata', component: GridInfiniteOdataComponent },
7476
{ path: 'localization', component: GridLocalizationComponent },
7577
{ path: 'clientside', component: GridClientSideComponent },

src/app/app.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@
186186
39- Infinite Scroll with GraphQL
187187
</a>
188188
</li>
189+
<li class="nav-item">
190+
<a class="nav-link" routerLinkActive="active" [routerLink]="['/infinite-json']">
191+
40- Infinite Scroll from JSON data
192+
</a>
193+
</li>
189194
</ul>
190195
</section>
191196

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { GridGroupingComponent } from './examples/grid-grouping.component';
3838
import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component';
3939
import { GridHeaderMenuComponent } from './examples/grid-headermenu.component';
4040
import { GridInfiniteGraphqlComponent } from './examples/grid-infinite-graphql.component';
41+
import { GridInfiniteJsonComponent } from './examples/grid-infinite-json.component';
4142
import { GridInfiniteOdataComponent } from './examples/grid-infinite-odata.component';
4243
import { GridLocalizationComponent } from './examples/grid-localization.component';
4344
import { GridMenuComponent } from './examples/grid-menu.component';
@@ -121,6 +122,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
121122
GridHeaderFooterComponent,
122123
GridHeaderMenuComponent,
123124
GridInfiniteGraphqlComponent,
125+
GridInfiniteJsonComponent,
124126
GridInfiniteOdataComponent,
125127
GridLocalizationComponent,
126128
GridMenuComponent,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<div class="demo40">
2+
<h2>
3+
Example 40: Infinite Scroll from JSON data
4+
<span class="float-end">
5+
<a style="font-size: 18px"
6+
target="_blank"
7+
href="https://github.com/ghiscoding/Angular-Slickgrid/blob/master/src/app/examples/grid-infinite-json.component.ts">
8+
<span class="mdi mdi-link-variant"></span> code
9+
</a>
10+
</span>
11+
</h2>
12+
13+
<h6 class="title is-6 italic content">
14+
<ul>
15+
<li>
16+
Infinite scrolling allows the grid to lazy-load rows from the server when reaching the scroll bottom (end) position.
17+
In its simplest form, the more the user scrolls down, the more rows get loaded.
18+
</li>
19+
<li>NOTES: <code>presets.pagination</code> is not supported with Infinite Scroll and will revert to the first page,
20+
simply because since we keep appending data, we always have to start from index zero (no offset).
21+
</li>
22+
</ul>
23+
</h6>
24+
25+
<div class="row">
26+
<div class="col-sm-12">
27+
<button class="btn btn-outline-secondary btn-sm" data-test="clear-filters-sorting"
28+
(click)="clearAllFiltersAndSorts()" title="Clear all Filters & Sorts">
29+
<span class="mdi mdi-close"></span>
30+
<span>Clear all Filter & Sorts</span>
31+
</button>
32+
<button class="btn btn-outline-secondary btn-sm" data-test="set-dynamic-filter" (click)="setFiltersDynamically()">
33+
Set Filters Dynamically
34+
</button>
35+
<button class="btn btn-outline-secondary btn-sm" data-test="set-dynamic-sorting" (click)="setSortingDynamically()">
36+
Set Sorting Dynamically
37+
</button>
38+
<button class="btn btn-outline-secondary btn-sm" data-test="group-by-duration" (click)="groupByDuration()">
39+
Group by Duration
40+
</button>
41+
42+
<label class="ml-4">Reset Dataset <code>onSort</code>:</label>
43+
<button class="btn btn-outline-secondary btn-sm" data-test="onsort-on" (click)="onSortReset(true)">
44+
ON
45+
</button>
46+
<button class="btn btn-outline-secondary btn-sm" data-test="onsort-off" (click)="onSortReset(false)">
47+
OFF
48+
</button>
49+
</div>
50+
</div>
51+
52+
<div *ngIf="metrics" class="mt-2" style="margin: 10px 0px">
53+
<b>Metrics:</b>
54+
<span>
55+
<span>{{metrics.endTime | date: 'dd MMM, h:mm:ssa'}}</span>
56+
<span data-test="totalItemCount">{{metrics.totalItemCount}}</span>
57+
items
58+
</span>
59+
</div>
60+
61+
<angular-slickgrid gridId="grid40"
62+
[columnDefinitions]="columnDefinitions"
63+
[gridOptions]="gridOptions"
64+
[dataset]="dataset"
65+
(onAngularGridCreated)="angularGridReady($event.detail)"
66+
(onSort)="handleOnSort()"
67+
(onScroll)="handleOnScroll($event.detail.args)"
68+
(onRowCountChanged)="refreshMetrics($event.detail.args)">
69+
</angular-slickgrid>
70+
</div>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import {
3+
type AngularGridInstance,
4+
Aggregators,
5+
type Column,
6+
FieldType,
7+
Formatters,
8+
type GridOption,
9+
type Grouping,
10+
type Metrics,
11+
type OnRowCountChangedEventArgs,
12+
SortComparers,
13+
SortDirectionNumber
14+
} from '../modules/angular-slickgrid';
15+
16+
const FETCH_SIZE = 50;
17+
18+
@Component({
19+
templateUrl: './grid-infinite-json.component.html'
20+
})
21+
export class GridInfiniteJsonComponent implements OnInit {
22+
angularGrid!: AngularGridInstance;
23+
columnDefinitions!: Column[];
24+
dataset: any[] = [];
25+
gridOptions!: GridOption;
26+
metrics!: Partial<Metrics>;
27+
scrollEndCalled = false;
28+
shouldResetOnSort = false;
29+
30+
ngOnInit(): void {
31+
this.defineGrid();
32+
this.dataset = this.loadData(0, FETCH_SIZE);
33+
this.metrics = {
34+
itemCount: FETCH_SIZE,
35+
totalItemCount: FETCH_SIZE,
36+
};
37+
}
38+
39+
angularGridReady(angularGrid: AngularGridInstance) {
40+
this.angularGrid = angularGrid;
41+
}
42+
43+
defineGrid() {
44+
this.columnDefinitions = [
45+
{ id: 'title', name: 'Title', field: 'title', sortable: true, minWidth: 100, filterable: true },
46+
{ id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
47+
{ id: 'percentComplete', name: '% Complete', field: 'percentComplete', sortable: true, minWidth: 100, filterable: true, type: FieldType.number },
48+
{ id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, exportWithFormatter: true, filterable: true },
49+
{ id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, exportWithFormatter: true, filterable: true },
50+
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', sortable: true, minWidth: 100, filterable: true, formatter: Formatters.checkmarkMaterial }
51+
];
52+
53+
this.gridOptions = {
54+
autoResize: {
55+
container: '#demo-container',
56+
rightPadding: 10
57+
},
58+
enableAutoResize: true,
59+
enableFiltering: true,
60+
enableGrouping: true,
61+
editable: false,
62+
rowHeight: 33,
63+
};
64+
}
65+
66+
// add onScroll listener which will detect when we reach the scroll end
67+
// if so, then append items to the dataset
68+
handleOnScroll(args: any) {
69+
const viewportElm = args.grid.getViewportNode();
70+
if (
71+
['mousewheel', 'scroll'].includes(args.triggeredBy || '')
72+
&& !this.scrollEndCalled
73+
&& viewportElm.scrollTop > 0
74+
&& Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight
75+
) {
76+
console.log('onScroll end reached, add more items');
77+
const startIdx = this.angularGrid.dataView?.getItemCount() || 0;
78+
const newItems = this.loadData(startIdx, FETCH_SIZE);
79+
this.angularGrid.dataView?.addItems(newItems);
80+
this.scrollEndCalled = false;
81+
}
82+
}
83+
84+
// do we want to reset the dataset when Sorting?
85+
// if answering Yes then use the code below
86+
handleOnSort() {
87+
if (this.shouldResetOnSort) {
88+
const newData = this.loadData(0, FETCH_SIZE);
89+
this.angularGrid.slickGrid?.scrollTo(0); // scroll back to top to avoid unwanted onScroll end triggered
90+
this.angularGrid.dataView?.setItems(newData);
91+
this.angularGrid.dataView?.reSort();
92+
}
93+
}
94+
95+
groupByDuration() {
96+
this.angularGrid?.dataView?.setGrouping({
97+
getter: 'duration',
98+
formatter: (g) => `Duration: ${g.value} <span class="text-green">(${g.count} items)</span>`,
99+
comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.asc),
100+
aggregators: [
101+
new Aggregators.Avg('percentComplete'),
102+
new Aggregators.Sum('cost')
103+
],
104+
aggregateCollapsed: false,
105+
lazyTotalsCalculation: true
106+
} as Grouping);
107+
108+
// you need to manually add the sort icon(s) in UI
109+
this.angularGrid?.slickGrid?.setSortColumns([{ columnId: 'duration', sortAsc: true }]);
110+
this.angularGrid?.slickGrid?.invalidate(); // invalidate all rows and re-render
111+
}
112+
113+
loadData(startIdx: number, count: number) {
114+
const tmpData: any[] = [];
115+
for (let i = startIdx; i < startIdx + count; i++) {
116+
tmpData.push(this.newItem(i));
117+
}
118+
119+
return tmpData;
120+
}
121+
122+
newItem(idx: number) {
123+
const randomYear = 2000 + Math.floor(Math.random() * 10);
124+
const randomMonth = Math.floor(Math.random() * 11);
125+
const randomDay = Math.floor((Math.random() * 29));
126+
const randomPercent = Math.round(Math.random() * 100);
127+
128+
return {
129+
id: idx,
130+
title: 'Task ' + idx,
131+
duration: Math.round(Math.random() * 100) + '',
132+
percentComplete: randomPercent,
133+
start: new Date(randomYear, randomMonth + 1, randomDay),
134+
finish: new Date(randomYear + 1, randomMonth + 1, randomDay),
135+
effortDriven: (idx % 5 === 0)
136+
};
137+
}
138+
139+
onSortReset(shouldReset: boolean) {
140+
this.shouldResetOnSort = shouldReset;
141+
}
142+
143+
clearAllFiltersAndSorts() {
144+
if (this.angularGrid?.gridService) {
145+
this.angularGrid.gridService.clearAllFiltersAndSorts();
146+
}
147+
}
148+
149+
setFiltersDynamically() {
150+
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
151+
this.angularGrid?.filterService.updateFilters([
152+
{ columnId: 'percentComplete', searchTerms: ['50'], operator: '>=' },
153+
]);
154+
}
155+
156+
refreshMetrics(args: OnRowCountChangedEventArgs) {
157+
if (this.angularGrid && args?.current >= 0) {
158+
this.metrics.itemCount = this.angularGrid.dataView?.getFilteredItemCount() || 0;
159+
this.metrics.totalItemCount = args.itemCount || 0;
160+
}
161+
}
162+
163+
setSortingDynamically() {
164+
this.angularGrid?.sortService.updateSorting([
165+
{ columnId: 'title', direction: 'DESC' },
166+
]);
167+
}
168+
}

test/cypress/e2e/example40.cy.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
describe('Example 40 - Infinite Scroll from JSON data', () => {
2+
const GRID_ROW_HEIGHT = 33;
3+
const titles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
4+
5+
it('should display Example title', () => {
6+
cy.visit(`${Cypress.config('baseUrl')}/infinite-json`);
7+
cy.get('h2').should('contain', 'Example 40: Infinite Scroll from JSON data');
8+
});
9+
10+
it('should have exact Column Titles in the grid', () => {
11+
cy.get('#grid40')
12+
.find('.slick-header-columns')
13+
.children()
14+
.each(($child, index) => expect($child.text()).to.eq(titles[index]));
15+
});
16+
17+
it('should expect first row to include "Task 0" and other specific properties', () => {
18+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
19+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains(/[0-9]/);
20+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains(/[0-9]/);
21+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/);
22+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/);
23+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).find('.mdi.mdi-check').should('have.length', 1);
24+
});
25+
26+
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', () => {
27+
cy.get('[data-test="totalItemCount"]')
28+
.should('have.text', '50');
29+
30+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
31+
.scrollTo('bottom');
32+
33+
cy.get('[data-test="totalItemCount"]')
34+
.should('have.text', '100');
35+
});
36+
37+
it('should scroll to bottom of the grid again and expect 50 more items for a total of now 150 items', () => {
38+
cy.get('[data-test="totalItemCount"]')
39+
.should('have.text', '100');
40+
41+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
42+
.scrollTo('bottom');
43+
44+
cy.get('[data-test="totalItemCount"]')
45+
.should('have.text', '150');
46+
});
47+
48+
it('should disable onSort for data reset and expect same dataset length of 150 items after sorting by Title', () => {
49+
cy.get('[data-test="onsort-off"]').click();
50+
51+
cy.get('[data-id="title"]')
52+
.click();
53+
54+
cy.get('[data-test="totalItemCount"]')
55+
.should('have.text', '150');
56+
57+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
58+
.scrollTo('top');
59+
60+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
61+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1');
62+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 10');
63+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 100');
64+
});
65+
66+
it('should enable onSort for data reset and expect dataset to be reset to 50 items after sorting by Title', () => {
67+
cy.get('[data-test="onsort-on"]').click();
68+
69+
cy.get('[data-id="title"]')
70+
.click();
71+
72+
cy.get('[data-test="totalItemCount"]')
73+
.should('have.text', '50');
74+
75+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
76+
.scrollTo('top');
77+
78+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 9');
79+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 8');
80+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 7');
81+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 6');
82+
});
83+
84+
it('should "Group by Duration" and expect 50 items grouped', () => {
85+
cy.get('[data-test="group-by-duration"]').click();
86+
87+
cy.get('[data-test="totalItemCount"]')
88+
.should('have.text', '50');
89+
90+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
91+
.scrollTo('top');
92+
93+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
94+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/);
95+
});
96+
97+
it('should scroll to the bottom "Group by Duration" and expect 50 more items for a total of 100 items grouped', () => {
98+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
99+
.scrollTo('bottom');
100+
101+
cy.get('[data-test="totalItemCount"]')
102+
.should('have.text', '100');
103+
104+
cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left')
105+
.scrollTo('top');
106+
107+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1);
108+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/);
109+
});
110+
});

0 commit comments

Comments
 (0)