-
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Infinite Scroll for JSON data (#1245)
* feat: Infinite Scroll for JSON data
- Loading branch information
1 parent
c19ddbb
commit a982443
Showing
4 changed files
with
346 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<div class="demo40"> | ||
<h2> | ||
Example 40: Infinite Scroll from JSON data | ||
<span class="float-end"> | ||
<a style="font-size: 18px" target="_blank" | ||
href="https://github.com/ghiscoding/aurelia-slickgrid/blob/master/packages/demo/src/examples/slickgrid/example40.ts"> | ||
<span class="mdi mdi-link-variant"></span> code | ||
</a> | ||
</span> | ||
</h2> | ||
|
||
<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"> | ||
<div class="col-sm-12"> | ||
<button class="btn btn-outline-secondary btn-sm" data-test="clear-filters-sorting" | ||
click.trigger="clearAllFiltersAndSorts()" title="Clear all Filters & Sorts"> | ||
<span class="mdi mdi-close"></span> | ||
<span>Clear all Filter & Sorts</span> | ||
</button> | ||
<button class="btn btn-outline-secondary btn-sm" data-test="set-dynamic-filter" click.trigger="setFiltersDynamically()"> | ||
Set Filters Dynamically | ||
</button> | ||
<button class="btn btn-outline-secondary btn-sm" data-test="set-dynamic-sorting" click.trigger="setSortingDynamically()"> | ||
Set Sorting Dynamically | ||
</button> | ||
<button class="btn btn-outline-secondary btn-sm" data-test="group-by-duration" click.trigger="groupByDuration()"> | ||
Group by Duration | ||
</button> | ||
|
||
<label class="ml-4">Reset Dataset <code>onSort</code>:</label> | ||
<button class="btn btn-outline-secondary btn-sm" data-test="onsort-on" click.trigger="onSortReset(true)"> | ||
ON | ||
</button> | ||
<button class="btn btn-outline-secondary btn-sm" data-test="onsort-off" click.trigger="onSortReset(false)"> | ||
OFF | ||
</button> | ||
</div> | ||
</div> | ||
|
||
<div show.bind="metrics" class="mt-2" style="margin: 10px 0px"> | ||
<b>Metrics:</b> | ||
<span> | ||
<span>${metrics.endTime | dateFormat: 'DD MMM, h:mm:ss a'}</span> — | ||
<span data-test="totalItemCount">${metrics.totalItemCount}</span> | ||
items | ||
</span> | ||
</div> | ||
|
||
<aurelia-slickgrid | ||
grid-id="grid40" | ||
column-definitions.bind="columnDefinitions" | ||
grid-options.bind="gridOptions" | ||
dataset.bind="dataset" | ||
instances.bind="aureliaGrid" | ||
on-aurelia-grid-created.trigger="aureliaGridReady($event.detail)" | ||
on-row-count-changed.trigger="refreshMetrics($event.detail.args)" | ||
on-sort.trigger="handleOnSort()" | ||
on-scroll.trigger="handleOnScroll($event.detail.args)"> | ||
</aurelia-slickgrid> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Metrics>; | ||
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} <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.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' }, | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]/); | ||
}); | ||
}); |