Skip to content

Commit

Permalink
Add dataProvider mixin (#725)
Browse files Browse the repository at this point in the history
* Add dataProvider mixin

* Add a test suite for dataProvider mixin

* fix(tests): dataProvider spy callback error

* chore: dataProvider mixin alignment

* fix(dataProvider): request first page when opened

* chore(tests): use named functions for better assertions

* fix(tests): wrong assertions for dataProvider arguments

* fix(tests): wrong assertions for filteredItems from dataProvider

* fix(dataProvider): remove initial dummy filtered item

* fix(dataProvider): ensure first page load when opened

* fix(tests): use opened comboBox in tests for lazy loading

* fix(dataProvider) set loading state while loading

* fix(dataProvider) throw if used with items

* fix(comboBoxLight): add dataProviderMixin

* fix(demos): use preserve-content in lazy loading demo template

* fix(tests): test single first load with size and async cases

* fix(lazyLoading): prevent unnecessary page requests when loading

* fix(tests): add tests for second/third page requests

* fix(filtering): fix excessive loading requests and re-enable builtin items filtering

* fix(dataProvider): prevent duplicate first page requests on certain cases

* fix(tests): relax lazyLoading requested number of pages criteria

* fix(tests): skip hiding while loading test for now

* fix(dataProvider): linter errors

* fix(lazyLoading) cleanup magic placeholder object

* fix(dataProvider): don’t stop requesting pages if already loading

* chore: use 2018 year for new files

* fix(demos): replace arrow function with regular one in lazy loading demo

* chore: cleanup style added by accident in demo index page

* fix: add JSdoc annotations when using ComboBoxDataProviderMixin

* fix(dataProvider): data provider API cleanup

* chore(dataProvider): use single quotes

* fix(dataProvider): throw when incorrect pageSize is set

* fix: use a separate dropdown-wrapper method for dispatching item requests

* fix(lazyLoading): support setting initial value / selectedItem

* fix(lazyLoading) disallow selecting placeholder ites

* fix(lazyLoading): use itemIdPath to match selectedItem with filteredItems

* fix(dataProvider): make clearCache not request pages when closed

* Fix linter errors

* Keep iron-list empty when closed

* Fix array init for IE11

* Clear filter on value or opened change

* Fix test timings for iOS

* Add a missing test
  • Loading branch information
platosha authored and limonte committed Oct 3, 2018
1 parent 13b53dd commit 7a872f2
Show file tree
Hide file tree
Showing 12 changed files with 1,007 additions and 25 deletions.
26 changes: 26 additions & 0 deletions demo/combo-box-basic-demos.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,32 @@ <h3>Allow Custom Values</h3>
</template>
</vaadin-demo-snippet>


<h3>Lazy Loading</h3>

<vaadin-demo-snippet id="combo-box-basic-demos-lazy-loading">
<template preserve-content>
<vaadin-combo-box label="Element"></vaadin-combo-box>
<script>
window.addDemoReadyListener('#combo-box-basic-demos-lazy-loading', function(document) {
const combo = document.querySelector('vaadin-combo-box');
combo.size = elements.length;
combo.dataProvider = function(params, callback) {
setTimeout(function() {
const filter = (params.filter || '').toLowerCase();
const filteredElements = elements.filter(function(element) {
return element.toLowerCase().indexOf(filter) > -1;
});
const index = params.page * params.pageSize;
const slice = filteredElements.slice(index, index + params.pageSize);
callback(slice, filteredElements.length);
}, 1000);
};
});
</script>
</template>
</vaadin-demo-snippet>

</template>
<script>
class ComboBoxBasicDemos extends DemoReadyEventEmitter(ComboBoxDemo(Polymer.Element)) {
Expand Down
208 changes: 208 additions & 0 deletions src/vaadin-combo-box-data-provider-mixin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<!--
@license
Copyright (c) 2018 Vaadin Ltd.
This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
-->
<link rel="import" href="vaadin-combo-box-placeholder.html">

<script>
window.Vaadin = window.Vaadin || {};
/**
* @polymerMixin
*/
Vaadin.ComboBoxDataProviderMixin = superClass => class DataProviderMixin extends superClass {

static get properties() {
return {

/**
* Number of items fetched at a time from the dataprovider.
*/
pageSize: {
type: Number,
value: 50,
observer: '_pageSizeChanged'
},

size: {
type: Number,
observer: '_sizeChanged'
},

dataProvider: {
type: Object,
observer: '_dataProviderChanged'
},

_pendingRequests: {
value: () => {
return {};
}
}

};
}

static get observers() {
return [
'_dataProviderFilterChanged(filter, dataProvider)',
'_dataProviderClearFilter(dataProvider, opened, value)',
'_warnDataProviderValue(dataProvider, value)',
'_ensureFirstPage(opened)',
];
}

_dataProviderClearFilter(dataProvider, opened, value) {
// Can't depend on filter in this obsever as we don't want
// to clear the filter whenever it's set
if (dataProvider && this.filter) {
this.size = undefined;
this._pendingRequests = {};
this.filter = '';
this.clearCache();
}
}

ready() {
super.ready();
this.clearCache();
this.$.overlay.addEventListener('index-requested', e => {
const index = e.detail.index;
if (index !== undefined) {
const page = this._getPageForIndex(index);
if (!this._hasPage(page)) {
this._loadPage(page);
}
}
});
}

_dataProviderFilterChanged() {
if (this.dataProvider && this.opened) {
this.size = undefined;
this._pendingRequests = {};
this.clearCache();
}
}

_ensureFirstPage(opened) {
if (opened && !this._hasPage(0)) {
this._loadPage(0);
}
}

_hasPage(page) {
if (!this.filteredItems) {
return false;
}
const loadedItem = this.filteredItems[page * this.pageSize];
return loadedItem !== undefined && !(loadedItem instanceof Vaadin.ComboBoxPlaceholder);
}

_loadPage(page) {
// make sure same page isn't requested multiple times.
if (!this._pendingRequests[page] && this.dataProvider) {
this.loading = true;

const params = {
page,
pageSize: this.pageSize,
filter: this.filter
};

const callback = (items, size) => {
if (this._pendingRequests[page] === callback) {
if (!this.filteredItems) {
const filteredItems = [];
filteredItems.splice(params.page * params.pageSize, items.length, ...items);
this.filteredItems = filteredItems;
} else {
this.splice('filteredItems', params.page * params.pageSize, items.length, ...items);
}
// Update selectedItem from filteredItems if value is set
if (this._isValidValue(this.value) && this._getItemValue(this.selectedItem) !== this.value) {
this._selectItemForValue(this.value);
}
this.size = size;

delete this._pendingRequests[page];

if (Object.keys(this._pendingRequests).length === 0) {
this.loading = false;
}
}
};
this._pendingRequests[page] = callback;
this.dataProvider(params, callback);
}
}

_getPageForIndex(index) {
return Math.floor(index / this.pageSize);
}

/**
* Clears the cached pages and reloads data from dataprovider when needed.
*/
clearCache() {
if (!this.dataProvider) {
return;
}
const filteredItems = [];
filteredItems.length = this.size || 0;
this.filteredItems = new Array(...filteredItems).map(item => {
return new Vaadin.ComboBoxPlaceholder();
});
if (this.opened) {
this._loadPage(0);
}
}

_sizeChanged(size) {
const filteredItems = this.filteredItems || [];
filteredItems.length = size || 0;
this.filteredItems = new Array(...filteredItems).map(item => {
return item !== undefined ? item : new Vaadin.ComboBoxPlaceholder();
});
}

_pageSizeChanged(pageSize, oldPageSize) {
if (Math.floor(pageSize) !== pageSize || pageSize === 0) {
this.pageSize = oldPageSize;
throw new Error('`pageSize` value must be an integer > 0');
}
this.clearCache();
}

_dataProviderChanged(dataProvider, oldDataProvider) {
this._ensureItemsOrDataProvider(() => {
this.dataProvider = oldDataProvider;
});
}

_ensureItemsOrDataProvider(restoreOldValueCallback) {
if (this.items !== undefined && this.dataProvider !== undefined) {
restoreOldValueCallback();
throw new Error('Using `items` and `dataProvider` together is not supported');
}
}

_warnDataProviderValue(dataProvider, value) {
if (dataProvider && value !== '' && (this.selectedItem === undefined || this.selectedItem === null)) {
const valueIndex = this._indexOfValue(value, this.filteredItems);
if (valueIndex < 0 || !this._getItemLabel(this.filteredItems[valueIndex])) {
/* eslint-disable no-console */
console.warn(
'Warning: unable to determine the label for the provided `value`. ' +
'Nothing to display in the text field. This usually happens when ' +
'setting an initial `value` before any items are returned from ' +
'the `dataProvider` callback. Consider setting `selectedItem` ' +
'instead of `value`'
);
/* eslint-enable no-console */
}
}
}

};
</script>
45 changes: 35 additions & 10 deletions src/vaadin-combo-box-dropdown-wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<link rel="import" href="../../iron-list/iron-list.html">
<link rel="import" href="vaadin-combo-box-item.html">
<link rel="import" href="vaadin-combo-box-dropdown.html">
<link rel="import" href="vaadin-combo-box-placeholder.html">

<dom-module id="vaadin-combo-box-dropdown-wrapper">
<template>
Expand All @@ -32,15 +33,15 @@
on-position-changed="_setOverlayHeight"
theme="[[theme]]">
<template>
<div id="scroller" on-click="_stopPropagation" hidden$="[[loading]]">
<iron-list id="selector" role="listbox" items="[[_items]]" scroll-target="[[_scroller]]">
<div id="scroller" on-click="_stopPropagation">
<iron-list id="selector" role="listbox" items="[[_getItems(opened, _items)]]" scroll-target="[[_scroller]]">
<template>
<vaadin-combo-box-item
on-click="_onItemClick"
index="[[index]]"
index="[[__requestItemByIndex(item, index)]]"
item="[[item]]"
label="[[getItemLabel(item)]]"
selected="[[_isItemSelected(item, _selectedItem)]]"
selected="[[_isItemSelected(item, _selectedItem, _itemIdPath)]]"
renderer="[[renderer]]"
role$="[[_getAriaRole(index)]]"
aria-selected$="[[_getAriaSelected(_focusedIndex,index)]]"
Expand Down Expand Up @@ -148,7 +149,9 @@
value: 'value'
},

_selector: Object
_selector: Object,

_itemIdPath: String
};
}

Expand All @@ -162,6 +165,10 @@
}));
}

_getItems(opened, items) {
return opened ? items : [];
}

_openedChanged(opened, items) {
// Do not attach if no items
// Do not dettach if opened but user types an invalid search
Expand Down Expand Up @@ -241,8 +248,12 @@
}
}

_isItemSelected(item, selectedItem) {
return item === selectedItem;
_isItemSelected(item, selectedItem, itemIdPath) {
if (itemIdPath && item !== undefined && selectedItem !== undefined) {
return this.get(itemIdPath, item) === this.get(itemIdPath, selectedItem);
} else {
return item === selectedItem;
}
}

_onItemClick(e) {
Expand Down Expand Up @@ -270,6 +281,20 @@
return -1;
}

/**
* If dataProvider is used, dispatch a request for the item’s index if
* the item is a placeholder object.
*
* @return {Number}
*/
__requestItemByIndex(item, index) {
if ((item instanceof Vaadin.ComboBoxPlaceholder) && index !== undefined) {
this.dispatchEvent(new CustomEvent('index-requested', {detail: {index}}));
}

return index;
}

/**
* Gets the label string for the item based on the `_itemLabelPath`.
* @return {String}
Expand Down Expand Up @@ -338,7 +363,7 @@
}

adjustScrollPosition() {
if (this._items) {
if (this.opened && this._items) {
this._scrollIntoView(this._focusedIndex);
}
}
Expand Down Expand Up @@ -415,8 +440,8 @@
e.stopPropagation();
}

_hidden(itemsChange, loading) {
return !loading && (!this._items || !this._items.length);
_hidden(itemsChange) {
return !this.loading && (!this._items || !this._items.length);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/vaadin-combo-box-light.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<link rel="import" href="../../vaadin-themable-mixin/vaadin-themable-mixin.html">
<link rel="import" href="../../vaadin-themable-mixin/vaadin-theme-property-mixin.html">
<link rel="import" href="vaadin-combo-box-mixin.html">
<link rel="import" href="vaadin-combo-box-data-provider-mixin.html">
<link rel="import" href="vaadin-combo-box-dropdown-wrapper.html">

<dom-module id="vaadin-combo-box-light">
Expand All @@ -20,6 +21,7 @@
position-target="[[inputElement]]"
renderer=[[renderer]]
_focused-index="[[_focusedIndex]]"
_item-id-path="[[itemIdPath]]"
_item-label-path="[[itemLabelPath]]"
loading="[[loading]]"
theme="[[theme]]">
Expand Down Expand Up @@ -72,14 +74,16 @@
* </vaadin-combo-box-light>
* ```
* @memberof Vaadin
* @mixes Vaadin.ComboBoxDataProviderMixin
* @mixes Vaadin.ComboBoxMixin
* @mixes Vaadin.ThemableMixin
* @mixes Vaadin.ThemePropertyMixin
*/
class ComboBoxLightElement extends
Vaadin.ThemePropertyMixin(
Vaadin.ThemableMixin(
Vaadin.ComboBoxMixin(Polymer.Element))) {
Vaadin.ComboBoxDataProviderMixin(
Vaadin.ComboBoxMixin(Polymer.Element)))) {

static get is() {
return 'vaadin-combo-box-light';
Expand Down

0 comments on commit 7a872f2

Please sign in to comment.