package.src.vaadin-grid-data-provider-mixin.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grid Show documentation
Show all versions of grid Show documentation
A free, flexible and high-quality Web Component for showing large amounts of tabular data
/**
* @license
* Copyright (c) 2016 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { microTask, timeOut } from '@vaadin/component-base/src/async.js';
import { DataProviderController } from '@vaadin/component-base/src/data-provider-controller/data-provider-controller.js';
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
import { get } from '@vaadin/component-base/src/path-utils.js';
import { getBodyRowCells, updateCellsPart, updateState } from './vaadin-grid-helpers.js';
/**
* @polymerMixin
*/
export const DataProviderMixin = (superClass) =>
class DataProviderMixin extends superClass {
static get properties() {
return {
/**
* The number of root-level items in the grid.
* @attr {number} size
* @type {number}
*/
size: {
type: Number,
notify: true,
sync: true,
},
/**
* @type {number}
* @protected
*/
_flatSize: {
type: Number,
sync: true,
},
/**
* Number of items fetched at a time from the dataprovider.
* @attr {number} page-size
* @type {number}
*/
pageSize: {
type: Number,
value: 50,
observer: '_pageSizeChanged',
sync: true,
},
/**
* Function that provides items lazily. Receives arguments `params`, `callback`
*
* `params.page` Requested page index
*
* `params.pageSize` Current page size
*
* `params.filters` Currently applied filters
*
* `params.sortOrders` Currently applied sorting orders
*
* `params.parentItem` When tree is used, and sublevel items
* are requested, reference to parent item of the requested sublevel.
* Otherwise `undefined`.
*
* `callback(items, size)` Callback function with arguments:
* - `items` Current page of items
* - `size` Total number of items. When tree sublevel items
* are requested, total number of items in the requested sublevel.
* Optional when tree is not used, required for tree.
*
* @type {GridDataProvider | null | undefined}
*/
dataProvider: {
type: Object,
notify: true,
observer: '_dataProviderChanged',
sync: true,
},
/**
* `true` while data is being requested from the data provider.
*/
loading: {
type: Boolean,
notify: true,
readOnly: true,
reflectToAttribute: true,
},
/**
* @protected
*/
_hasData: {
type: Boolean,
value: false,
sync: true,
},
/**
* Path to an item sub-property that indicates whether the item has child items.
* @attr {string} item-has-children-path
*/
itemHasChildrenPath: {
type: String,
value: 'children',
observer: '__itemHasChildrenPathChanged',
sync: true,
},
/**
* Path to an item sub-property that identifies the item.
* @attr {string} item-id-path
*/
itemIdPath: {
type: String,
value: null,
sync: true,
},
/**
* An array that contains the expanded items.
* @type {!Array}
*/
expandedItems: {
type: Object,
notify: true,
value: () => [],
sync: true,
},
/**
* @private
*/
__expandedKeys: {
type: Object,
computed: '__computeExpandedKeys(itemIdPath, expandedItems)',
},
};
}
static get observers() {
return ['_sizeChanged(size)', '_expandedItemsChanged(expandedItems)'];
}
constructor() {
super();
/** @type {DataProviderController} */
this._dataProviderController = new DataProviderController(this, {
size: this.size,
pageSize: this.pageSize,
getItemId: this.getItemId.bind(this),
isExpanded: this._isExpanded.bind(this),
dataProvider: this.dataProvider ? this.dataProvider.bind(this) : null,
dataProviderParams: () => {
return {
sortOrders: this._mapSorters(),
filters: this._mapFilters(),
};
},
});
this._dataProviderController.addEventListener('page-requested', this._onDataProviderPageRequested.bind(this));
this._dataProviderController.addEventListener('page-received', this._onDataProviderPageReceived.bind(this));
this._dataProviderController.addEventListener('page-loaded', this._onDataProviderPageLoaded.bind(this));
}
/**
* @protected
* @deprecated since 24.3 and will be removed in Vaadin 25.
*/
get _cache() {
console.warn(' The `_cache` property is deprecated and will be removed in Vaadin 25.');
return this._dataProviderController.rootCache;
}
/**
* @protected
* @deprecated since 24.3 and will be removed in Vaadin 25.
*/
get _effectiveSize() {
console.warn(' The `_effectiveSize` property is deprecated and will be removed in Vaadin 25.');
return this._flatSize;
}
/** @private */
_sizeChanged(size) {
this._dataProviderController.rootCache.size = size;
this._dataProviderController.recalculateFlatSize();
this._flatSize = this._dataProviderController.flatSize;
}
/** @private */
__itemHasChildrenPathChanged(value, oldValue) {
if (!oldValue && value === 'children') {
// Avoid an unnecessary content update on init.
return;
}
this.requestContentUpdate();
}
/**
* @param {number} index
* @param {HTMLElement} el
* @protected
*/
_getItem(index, el) {
el.index = index;
const { item } = this._dataProviderController.getFlatIndexContext(index);
if (item) {
this.__updateLoading(el, false);
this._updateItem(el, item);
if (this._isExpanded(item)) {
this._dataProviderController.ensureFlatIndexHierarchy(index);
}
} else {
this.__updateLoading(el, true);
this._dataProviderController.ensureFlatIndexLoaded(index);
}
}
/**
* @param {!HTMLElement} row
* @param {boolean} loading
* @private
*/
__updateLoading(row, loading) {
const cells = getBodyRowCells(row);
// Row state attribute
updateState(row, 'loading', loading);
// Cells part attribute
updateCellsPart(cells, 'loading-row-cell', loading);
if (loading) {
// Run style generators for the loading row to have custom names cleared
this._generateCellClassNames(row);
this._generateCellPartNames(row);
}
}
/**
* Returns a value that identifies the item. Uses `itemIdPath` if available.
* Can be customized by overriding.
* @param {!GridItem} item
* @return {!GridItem | !unknown}
*/
getItemId(item) {
return this.itemIdPath ? get(this.itemIdPath, item) : item;
}
/**
* @param {!GridItem} item
* @return {boolean}
* @protected
*/
_isExpanded(item) {
return this.__expandedKeys && this.__expandedKeys.has(this.getItemId(item));
}
/** @private */
_expandedItemsChanged() {
this._dataProviderController.recalculateFlatSize();
this._flatSize = this._dataProviderController.flatSize;
this.__updateVisibleRows();
}
/** @private */
__computeExpandedKeys(itemIdPath, expandedItems) {
const expanded = expandedItems || [];
const expandedKeys = new Set();
expanded.forEach((item) => {
expandedKeys.add(this.getItemId(item));
});
return expandedKeys;
}
/**
* Expands the given item tree.
* @param {!GridItem} item
*/
expandItem(item) {
if (!this._isExpanded(item)) {
this.expandedItems = [...this.expandedItems, item];
}
}
/**
* Collapses the given item tree.
* @param {!GridItem} item
*/
collapseItem(item) {
if (this._isExpanded(item)) {
this.expandedItems = this.expandedItems.filter((i) => !this._itemsEqual(i, item));
}
}
/**
* @param {number} index
* @return {number}
* @protected
*/
_getIndexLevel(index = 0) {
const { level } = this._dataProviderController.getFlatIndexContext(index);
return level;
}
/**
* @param {number} page
* @param {ItemCache} cache
* @protected
* @deprecated since 24.3 and will be removed in Vaadin 25.
*/
_loadPage(page, cache) {
console.warn(' The `_loadPage` method is deprecated and will be removed in Vaadin 25.');
this._dataProviderController.__loadCachePage(cache, page);
}
/** @protected */
_onDataProviderPageRequested() {
this._setLoading(true);
}
/** @protected */
_onDataProviderPageReceived() {
// If the page response affected the flat size
if (this._flatSize !== this._dataProviderController.flatSize) {
// Schedule an update of all rendered rows by _debouncerApplyCachedData,
// to ensure that all pages associated with the rendered rows are loaded.
this._shouldUpdateAllRenderedRowsAfterPageLoad = true;
// TODO: Updating the flat size property can still result in a synchonous virtualizer update
// if the size change requires the virtualizer to increase the amount of physical elements
// to display new items e.g. the viewport fits 10 items and the size changes from 1 to 10.
// This is something to be optimized in the future.
this._flatSize = this._dataProviderController.flatSize;
}
// After updating the cache, check if some of the expanded items should have sub-caches loaded
this._getRenderedRows().forEach((row) => {
this._dataProviderController.ensureFlatIndexHierarchy(row.index);
});
this._hasData = true;
}
/** @protected */
_onDataProviderPageLoaded() {
// Schedule a debouncer to update the visible rows
this._debouncerApplyCachedData = Debouncer.debounce(this._debouncerApplyCachedData, timeOut.after(0), () => {
this._setLoading(false);
const shouldUpdateAllRenderedRowsAfterPageLoad = this._shouldUpdateAllRenderedRowsAfterPageLoad;
this._shouldUpdateAllRenderedRowsAfterPageLoad = false;
this._getRenderedRows().forEach((row) => {
const { item } = this._dataProviderController.getFlatIndexContext(row.index);
if (item || shouldUpdateAllRenderedRowsAfterPageLoad) {
this._getItem(row.index, row);
}
});
this.__scrollToPendingIndexes();
this.__dispatchPendingBodyCellFocus();
});
// If the grid is not loading anything, flush the debouncer immediately
if (!this._dataProviderController.isLoading()) {
this._debouncerApplyCachedData.flush();
}
}
/** @private */
__debounceClearCache() {
this.__clearCacheDebouncer = Debouncer.debounce(this.__clearCacheDebouncer, microTask, () => this.clearCache());
}
/**
* Clears the cached pages and reloads data from dataprovider when needed.
*/
clearCache() {
this._dataProviderController.clearCache();
this._dataProviderController.rootCache.size = this.size;
this._dataProviderController.recalculateFlatSize();
this._hasData = false;
this.__updateVisibleRows();
if (!this.__virtualizer || !this.__virtualizer.size) {
this._dataProviderController.loadFirstPage();
}
}
/** @private */
_pageSizeChanged(pageSize, oldPageSize) {
this._dataProviderController.setPageSize(pageSize);
if (oldPageSize !== undefined && pageSize !== oldPageSize) {
this.clearCache();
}
}
/** @protected */
_checkSize() {
if (this.size === undefined && this._flatSize === 0) {
console.warn(
'The needs the total number of items in' +
' order to display rows, which you can specify either by setting' +
' the `size` property, or by providing it to the second argument' +
' of the `dataProvider` function `callback` call.',
);
}
}
/** @private */
_dataProviderChanged(dataProvider, oldDataProvider) {
this._dataProviderController.setDataProvider(dataProvider ? dataProvider.bind(this) : null);
if (oldDataProvider !== undefined) {
this.clearCache();
}
this._ensureFirstPageLoaded();
this._debouncerCheckSize = Debouncer.debounce(
this._debouncerCheckSize,
timeOut.after(2000),
this._checkSize.bind(this),
);
}
/** @protected */
_ensureFirstPageLoaded() {
if (!this._hasData) {
// Load data before adding rows to make sure they have content when
// rendered for the first time.
this._dataProviderController.loadFirstPage();
}
}
/**
* @param {!GridItem} item1
* @param {!GridItem} item2
* @return {boolean}
* @protected
*/
_itemsEqual(item1, item2) {
return this.getItemId(item1) === this.getItemId(item2);
}
/**
* @param {!GridItem} item
* @param {!Array} array
* @return {number}
* @protected
*/
_getItemIndexInArray(item, array) {
let result = -1;
array.forEach((i, idx) => {
if (this._itemsEqual(i, item)) {
result = idx;
}
});
return result;
}
/**
* Scroll to a specific row index in the virtual list. Note that the row index is
* not always the same for any particular item. For example, sorting or filtering
* items can affect the row index related to an item.
*
* The `indexes` parameter can be either a single number or multiple numbers.
* The grid will first try to scroll to the item at the first index on the top level.
* In case the item at the first index is expanded, the grid will then try scroll to the
* item at the second index within the children of the expanded first item, and so on.
* Each given index points to a child of the item at the previous index.
*
* Using `Infinity` as an index will point to the last item on the level.
*
* @param indexes {...number} Row indexes to scroll to
*/
scrollToIndex(...indexes) {
// Synchronous data provider may cause changes to the cache on scroll without
// ending up in a loading state. Try scrolling to the index until the target
// index stabilizes.
let targetIndex;
while (targetIndex !== (targetIndex = this._dataProviderController.getFlatIndexByPath(indexes))) {
this._scrollToFlatIndex(targetIndex);
}
if (this._dataProviderController.isLoading() || !this.clientHeight) {
this.__pendingScrollToIndexes = indexes;
}
}
/** @private */
__scrollToPendingIndexes() {
if (this.__pendingScrollToIndexes && this.$.items.children.length) {
const indexes = this.__pendingScrollToIndexes;
delete this.__pendingScrollToIndexes;
this.scrollToIndex(...indexes);
}
}
/**
* Fired when the `expandedItems` property changes.
*
* @event expanded-items-changed
*/
/**
* Fired when the `loading` property changes.
*
* @event loading-changed
*/
};