Skip to content

Commit

Permalink
feat: add new Infinite Scroll example (#1040)
Browse files Browse the repository at this point in the history
* feat: add new Infinite Scroll example
  • Loading branch information
ghiscoding authored Jul 27, 2024
1 parent 856286d commit 060c8a0
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 6 deletions.
87 changes: 87 additions & 0 deletions cypress/e2e/example-infinite-scroll-esm.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
describe('Example - Infinite Scroll', () => {
const GRID_ROW_HEIGHT = 25;
const titles = ['#', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];

it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/examples/example-infinite-scroll-esm.html`);
cy.get('h2').contains('Demonstrates');
cy.get('h2 + p').contains('This page demonstrates Infinite Scroll using DataView.');

cy.get('.grid-header > label')
.should('contain', 'SlickGrid - Infinite Scroll');
});

it('should have exact Column Titles in the grid', () => {
cy.get('#myGrid')
.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(1)`).should('contain', 'Task 0');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '5 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) .percent-complete-bar`).should('exist');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '01/01/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', '01/05/2009');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).find('.sgi.sgi-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="itemCount"]')
.should('have.text', '50');

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

cy.get('[data-test="itemCount"]')
.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="itemCount"]')
.should('have.text', '100');

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

cy.get('[data-test="itemCount"]')
.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="itemCount"]')
.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(1)`).should('contain', 'Task 0');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 10');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).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="itemCount"]')
.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(1)`).should('contain', 'Task 9');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 8');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 7');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 6');
});
});
230 changes: 230 additions & 0 deletions examples/example-infinite-scroll-esm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link rel="shortcut icon" type="image/ico" href="favicon.ico" />
<title>SlickGrid example: Infinite Scroll</title>
<link rel="stylesheet" href="../dist/styles/css/slick-icons.css" type="text/css"/>
<link rel="stylesheet" href="../dist/styles/css/example-demo.css" type="text/css"/>
<link rel="stylesheet" href="../dist/styles/css/slick-alpine-theme.css" type="text/css"/>
<style>
.cell-title {
font-weight: bold;
}

.cell-effort-driven {
justify-content: center;
}

.cell-selection {
border-right-color: silver;
border-right-style: solid;
background: #f5f5f5;
color: gray;
text-align: right;
font-size: 10px;
}
</style>
</head>
<body>
<div style="position:relative">
<div style="width:650px;">
<div class="grid-header" style="width:100%">
<label>SlickGrid - Infinite Scroll</label>
</div>
<div id="myGrid" class="slick-container" style="width:100%;height:500px;"></div>
</div>

<div class="options-panel">
<h2>
<a href="/examples/index.html" style="text-decoration: none; font-size: 22px">&#x2302;</a>
Demonstrates:
</h2>

<p>
This page demonstrates Infinite Scroll using DataView.
<ul>
<li>Infinite scrolling allows the grid to lazy-load rows from the server (or locally) when reaching the scroll bottom (end) position.</li>
<li>In its simplest form, the more the user scrolls down, the more rows will get loaded and appended to the in-memory dataset.</li>
<li>This demo will keep loading and adding data indifinitely, however in most cases you will eventually reach the end of your dataset and have everything loaded in memory.</li>
<li>You can choose to reset (or not) the dataset after Sorting by any column.</li>
<li>Note that instead of the DataView, we could also use just plain SlickGrid (without DataView) to add items</li>
</ul>
</p>
<h2>View Source:</h2>
<ul>
<li><A href="https://github.com/6pac/SlickGrid/blob/master/examples/example-infinite-scroll-esm.html" target="_sourcewindow"> View the source for this example on Github</a></li>
</ul>
<h4>Reset Dataset onSort</h4>
<button data-test="onsort-on">ON</button>
<button data-test="onsort-off">OFF</button>

<h4>Data Count</h4>
<p>
<label>Data length:</label>
<span id="dataLength" data-test="itemCount">0</span>
</p>
</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/sortablejs/Sortable.min.js"></script>
<script src="sortable-cdn-fallback.js"></script>

<script type="module">
import {
Formatters,
SlickDataView,
SlickGrid,
} from '../dist/esm/index.js';

const FETCH_SIZE = 50;
let dataView;
let grid;
let data = [];
let columns = [
{id: "sel", name: "#", field: "num", behavior: "select", cssClass: "cell-selection", width: 40, resizable: false, selectable: false },
{id: "title", name: "Title", field: "title", width: 120, cssClass: "cell-title", sortable: true },
{id: "duration", name: "Duration", field: "duration", sortable: true },
{id: "%", name: "% Complete", field: "percentComplete", width: 120, resizable: false, formatter: Formatters.PercentCompleteBar, sortable: true },
{id: "start", name: "Start", field: "start", minWidth: 60, sortable: true },
{id: "finish", name: "Finish", field: "finish", minWidth: 60, sortable: true },
{id: "effort-driven", name: "Effort Driven", width: 100, minWidth: 20, cssClass: "cell-effort-driven", field: "effortDriven", formatter: Formatters.Checkmark, sortable: true }
];
let shouldResetOnSort = false;
let scrollEndCalled = false;
let sortcol = "title";
let sortdir = 1;

let gridOptions = {
editable: false,
enableAddRow: false,
enableCellNavigation: true
};

let percentCompleteThreshold = 0;
let prevPercentCompleteThreshold = 0;
let h_runfilters = null;

function onSortReset(shouldReset) {
shouldResetOnSort = shouldReset;
}

function myFilter(item, args) {
return item["percentComplete"] >= args;
}

function DataItem(i) {
this.num = i;
this.id = "id_" + i;
this.percentComplete = Math.round(Math.random() * 100);
this.effortDriven = (i % 5 == 0);
this.start = "01/01/2009";
this.finish = "01/05/2009";
this.title = "Task " + i;
this.duration = "5 days";
}

function handleOnScrollEnd() {
console.log('onScroll end reached, add more items');
const startIdx = dataView.getItemCount();

dataView.addItems(loadData(startIdx, FETCH_SIZE));
scrollEndCalled = false;
}

function loadData(startIdx, count) {
let tmpData = [];
for (let i = startIdx; i < startIdx + count; i++) {
tmpData.push(new DataItem(i));
}
return tmpData;
}

function comparer(a, b) {
let x = a[sortcol], y = b[sortcol];
return (x == y ? 0 : (x > y ? 1 : -1));
}

document.addEventListener("DOMContentLoaded", function() {
// prepare the data
data = loadData(0, FETCH_SIZE);

dataView = new SlickDataView({ inlineFilters: true });
grid = new SlickGrid("#myGrid", dataView, columns, gridOptions);

// wire up model events to drive the grid
dataView.onRowCountChanged.subscribe((e, args) => {
grid.updateRowCount();
grid.render();

let countElm = document.querySelector('#dataLength');
countElm.textContent = args.itemCount;
});

dataView.onRowsChanged.subscribe((e, args) => {
grid.invalidateRows(args.rows);
grid.render();
});

grid.onSort.subscribe((e, args) => {
sortdir = args.sortAsc ? 1 : -1;
sortcol = args.sortCol.field;

// using native sort with comparer
dataView.sort(comparer, args.sortAsc);

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

// add onScroll listener to append items to the dataset whenever reaching the scroll bottom (scroll end)
grid.onScroll.subscribe((e, args) => {
const viewportElm = args.grid.getViewportNode();
if (
['mousewheel', 'scroll'].includes(args.triggeredBy || '')
&& !scrollEndCalled
&& viewportElm.scrollTop > 0
&& Math.ceil(viewportElm.offsetHeight + args.scrollTop) >= args.scrollHeight
) {
const vpLeft = document.querySelector('.slick-viewport-top.slick-viewport-left');
scrollEndCalled = true;
handleOnScrollEnd();
}
});

function filterAndUpdate() {
let isNarrowing = percentCompleteThreshold > prevPercentCompleteThreshold;
let isExpanding = percentCompleteThreshold < prevPercentCompleteThreshold;
let renderedRange = grid.getRenderedRange();

dataView.setFilterArgs(percentCompleteThreshold);
dataView.setRefreshHints({
ignoreDiffsBefore: renderedRange.top,
ignoreDiffsAfter: renderedRange.bottom + 1,
isFilterNarrowing: isNarrowing,
isFilterExpanding: isExpanding
});
dataView.refresh();

prevPercentCompleteThreshold = percentCompleteThreshold;
}

// initialize the model after all the events have been hooked up
dataView.beginUpdate();
dataView.setItems(data);
dataView.setFilter(myFilter);
dataView.setFilterArgs(0);
dataView.endUpdate();

// add DOM event listeners
document.querySelector('[data-test="onsort-on"]').addEventListener('click', () => onSortReset(true));
document.querySelector('[data-test="onsort-off"]').addEventListener('click', () => onSortReset(false));
});
</script>
</body>
</html>
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ <h2>ES6 / ESM</h2>
<li><a href="./example-grouping-esm.html">Interactive grouping and aggregates</a></li>
<li>(must see) <a href="./example-trading-esm.html">Realtime Trading - High Frequency Update</a></li>
<li><a href="./example-web-component-pubsub-esm.html">Web Component with PubSub Service instead of SlickEvent</a></li>
<li><a href="./example-infinite-scroll-esm.html">Infinite Scroll with DataView</a></li>
</ul>
</div>

Expand Down
Loading

0 comments on commit 060c8a0

Please sign in to comment.