AngularJS + ag-grid: sticky/remembered 选择与虚拟 paging/infinite 滚动
AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrolling
在 AngularJS 应用程序中,我在第一列中有一个 ag-grid that uses virtual paging/infinite scrolling to lazy-load rows from a dataset that is too large to show at once. I have turned on check-box selection,因此用户应该能够 select 单独的行来执行任意特定于应用程序的操作。
AngularJS 应用程序使用 ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering",从 ag-grid
文档中构建有关奥运会获胜者的数据,我进一步扩展了代码。来自 index.html
:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
和以下 ui-router
状态:
myapp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("example.page1")
$stateProvider
.state('example', {
abstract: true,
views: {
contents: {
template: '<div ui-view="example"></div>'
}
}
})
.state('example.page1', {
url: '/page1',
views: {
example: {
templateUrl: 'page1.html'
}
}
})
.state('example.page2', {
url: '/page2',
views: {
example: {
template: 'Go back to the <a ui-sref="example.page1">example grid</a>.'
}
}
});
});
其中 page1.html
如下所示:
<div ng-controller="GridController">
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div>
</div>
<div>
<h3>Selected rows:</h3>
<ul class="list-inline">
<li ng-repeat="row in currentSelection track by row.id">
<a ng-click="remove(row)">
<div class="badge">#{{ row.id }}, {{ row.athlete }}</div>
</a>
</li>
</ul>
</div>
<p>Go to <a ui-sref="example.page2">the other page</a>.</p>
我想完成的事情:
- 当将(虚拟)页面滚动出视图并再次返回时,
ag-grid
中创建的 select 离子会被记住(粘性),以便用户可以 select 多个单独页面上的行。
- 记住的 select 离子在网格外可用,并支持添加和删除 select 离子(如上图所示
page1.html
中的 ng-click="remove(row)"
).
- 从使用
ag-grid
的视图切换到另一个视图并再次返回时,应记住 selections。
- (可选)为用户会话记住 select 离子。
我怎样才能做到这一点?
我创建了一个working example这个可以实现。
首先,我们将编写一个 AngularJS 服务,selectionService
来跟踪选择:
function _emptyArray(array) {
while (array.length) {
array.pop();
}
}
function _updateSharedArray(target, source) {
_emptyArray(target);
_.each(source, function _addActivity(activity) {
target.push(activity);
});
}
myapp.factory('selectionService', function ($rootScope, $window) {
var _collections = {},
_storage = $window.sessionStorage,
_prefix = 'selectionService';
angular.element($window).on('storage', _updateOnStorageChange);
function _persistCollection(collection, data) {
_storage.setItem(_prefix + ':' + collection, angular.toJson(data));
}
function _loadCollection(collection) {
var item = _storage.getItem(_prefix + ':' + collection);
return item !== null ? angular.fromJson(item) : item;
}
function _updateOnStorageChange(event) {
var item = event.originalEvent.newValue;
var keyParts = event.originalEvent.key.split(':');
if (keyParts.length < 2 || keyParts[0] !== _prefix) {
return;
}
var collection = keyParts[1];
_updateSharedArray(_getCollection(collection), angular.fromJson(item));
_broadcastUpdate(collection);
}
function _broadcastUpdate(collection) {
$rootScope.$emit(_service.getUpdatedSignal(collection));
}
function _afterUpdate(collection, selected) {
_persistCollection(collection, selected);
_broadcastUpdate(collection);
}
function _getCollection(collection) {
if (!_.has(_collections, collection)) {
var data = _loadCollection(collection);
// Holds reference to a shared array. Only mutate, don't replace it.
_collections[collection] = data !== null ? data : [];
}
return _collections[collection];
}
function _add(item, path, collection) {
// Add `item` to `collection` where item will be identified by `path`.
// For example, path could be 'id', 'row_id', 'data.athlete_id',
// whatever fits the row data being added.
var selected = _getCollection(collection);
if (!_.any(selected, path, _.get(item, path))) {
selected.push(item);
}
_afterUpdate(collection, selected);
}
function _remove(item, path, collection) {
// Remove `item` from `collection`, where item is identified by `path`,
// just like in _add().
var selected = _getCollection(collection);
_.remove(selected, path, _.get(item, path));
_afterUpdate(collection, selected);
}
function _getUpdatedSignal(collection) {
return 'selectionService:updated:' + collection;
}
function _updateInGridSelections(gridApi, path, collection) {
var selectedInGrid = gridApi.getSelectedNodes(),
currentlySelected = _getCollection(collection),
gridPath = 'data.' + path;
_.each(selectedInGrid, function (node) {
if (!_.any(currentlySelected, path, _.get(node, gridPath))) {
// The following suppressEvents=true flag is ignored for now, but a
// fixing pull request is waiting at ag-grid GitHub.
gridApi.deselectNode(node, true);
}
});
var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),
currentlySelectedIds = _.pluck(currentlySelected, path),
missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);
if (missingIdsInGrid.length > 0) {
// We're trying to avoid the following loop, since it seems horrible to
// have to loop through all the nodes only to select some. I wish there
// was a way to select nodes/rows based on an id.
var i;
gridApi.forEachNode(function (node) {
i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));
if (i >= 0) {
// multi=true, suppressEvents=true:
gridApi.selectNode(node, true, true);
missingIdsInGrid.splice(i, 1); // Reduce haystack.
if (!missingIdsInGrid.length) {
// I'd love for `forEachNode` to support breaking the loop here.
}
}
});
}
}
var _service = {
getCollection: _getCollection,
add: _add,
remove: _remove,
getUpdatedSignal: _getUpdatedSignal,
updateInGridSelections: _updateInGridSelections
};
return _service;
});
selectionService
服务允许添加和删除任意对象到单独的集合,由 collection
标识,您认为合适的名称。这样,同一服务可用于在多个 ag-grid
实例中记住选择。每个对象都将使用 path
参数进行标识。 path
用于使用 lodash's get 函数检索唯一标识符。
此外,该服务使用 sessionStorage 在用户的整个 tab/browser 会话期间保持选择。这可能有点矫枉过正;我们可以只依赖服务来跟踪选择,因为它只会被实例化一次。这当然可以根据您的需要进行修改。
然后是必须对 GridController
进行的更改。首先,第一列的 columnDefs
条目必须稍微更改
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
一旦从远程服务器检索到数据,就会生成新生成的行 ID
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(包含 ID 中的 'm'
只是为了确保我没有将该 ID 与 ag-grid
使用的其他 ID 混淆。)
接下来,对 gridOptions
的必要更改是添加
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
不同的处理程序是否非常直接,与 selectionService
进行通信
function rowSelected(event) {
selectionService.add(event.node.data, 'id', 'page-1');
}
function rowDeselected(event) {
selectionService.remove(event.node.data, 'id', 'page-1');
}
function clearSelections(event) {
$scope.gridOptions.api.deselectAll();
}
现在,GridController
也需要处理 selectionService
发出的更新信号
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
和
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
调用 selectionService.updateInGridSelections
将更新相关网格的网格内选择。那是写起来最麻烦的函数。例如,如果在外部(网格外)添加了一个选择,那么我们将不得不执行 forEachNode
运行,即使我们知道所有必要的节点都已在网格内选择;无法提前退出该循环。
最后,另一个难点是在更改过滤器或排序顺序时,或从服务器检索新数据时(仅在演示中模拟),分别清除并重新应用之前和之后的选择。解决方案是在 getRows
处理程序
中的 params.successCallback
之后包含对 updateSelections
的调用
params.successCallback(rowsThisPage, lastRow);
updateSelections();
现在,在这个解决方案的实施过程中最令人费解的发现是 ag-grid
API 网格选项 onAfterFilterChanged
和 onAfterSortChanged
不能用于重新应用选择是因为它们在(远程)数据完成加载之前触发。
在 AngularJS 应用程序中,我在第一列中有一个 ag-grid that uses virtual paging/infinite scrolling to lazy-load rows from a dataset that is too large to show at once. I have turned on check-box selection,因此用户应该能够 select 单独的行来执行任意特定于应用程序的操作。
AngularJS 应用程序使用 ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering",从 ag-grid
文档中构建有关奥运会获胜者的数据,我进一步扩展了代码。来自 index.html
:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
和以下 ui-router
状态:
myapp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("example.page1")
$stateProvider
.state('example', {
abstract: true,
views: {
contents: {
template: '<div ui-view="example"></div>'
}
}
})
.state('example.page1', {
url: '/page1',
views: {
example: {
templateUrl: 'page1.html'
}
}
})
.state('example.page2', {
url: '/page2',
views: {
example: {
template: 'Go back to the <a ui-sref="example.page1">example grid</a>.'
}
}
});
});
其中 page1.html
如下所示:
<div ng-controller="GridController">
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div>
</div>
<div>
<h3>Selected rows:</h3>
<ul class="list-inline">
<li ng-repeat="row in currentSelection track by row.id">
<a ng-click="remove(row)">
<div class="badge">#{{ row.id }}, {{ row.athlete }}</div>
</a>
</li>
</ul>
</div>
<p>Go to <a ui-sref="example.page2">the other page</a>.</p>
我想完成的事情:
- 当将(虚拟)页面滚动出视图并再次返回时,
ag-grid
中创建的 select 离子会被记住(粘性),以便用户可以 select 多个单独页面上的行。 - 记住的 select 离子在网格外可用,并支持添加和删除 select 离子(如上图所示
page1.html
中的ng-click="remove(row)"
). - 从使用
ag-grid
的视图切换到另一个视图并再次返回时,应记住 selections。 - (可选)为用户会话记住 select 离子。
我怎样才能做到这一点?
我创建了一个working example这个可以实现。
首先,我们将编写一个 AngularJS 服务,selectionService
来跟踪选择:
function _emptyArray(array) {
while (array.length) {
array.pop();
}
}
function _updateSharedArray(target, source) {
_emptyArray(target);
_.each(source, function _addActivity(activity) {
target.push(activity);
});
}
myapp.factory('selectionService', function ($rootScope, $window) {
var _collections = {},
_storage = $window.sessionStorage,
_prefix = 'selectionService';
angular.element($window).on('storage', _updateOnStorageChange);
function _persistCollection(collection, data) {
_storage.setItem(_prefix + ':' + collection, angular.toJson(data));
}
function _loadCollection(collection) {
var item = _storage.getItem(_prefix + ':' + collection);
return item !== null ? angular.fromJson(item) : item;
}
function _updateOnStorageChange(event) {
var item = event.originalEvent.newValue;
var keyParts = event.originalEvent.key.split(':');
if (keyParts.length < 2 || keyParts[0] !== _prefix) {
return;
}
var collection = keyParts[1];
_updateSharedArray(_getCollection(collection), angular.fromJson(item));
_broadcastUpdate(collection);
}
function _broadcastUpdate(collection) {
$rootScope.$emit(_service.getUpdatedSignal(collection));
}
function _afterUpdate(collection, selected) {
_persistCollection(collection, selected);
_broadcastUpdate(collection);
}
function _getCollection(collection) {
if (!_.has(_collections, collection)) {
var data = _loadCollection(collection);
// Holds reference to a shared array. Only mutate, don't replace it.
_collections[collection] = data !== null ? data : [];
}
return _collections[collection];
}
function _add(item, path, collection) {
// Add `item` to `collection` where item will be identified by `path`.
// For example, path could be 'id', 'row_id', 'data.athlete_id',
// whatever fits the row data being added.
var selected = _getCollection(collection);
if (!_.any(selected, path, _.get(item, path))) {
selected.push(item);
}
_afterUpdate(collection, selected);
}
function _remove(item, path, collection) {
// Remove `item` from `collection`, where item is identified by `path`,
// just like in _add().
var selected = _getCollection(collection);
_.remove(selected, path, _.get(item, path));
_afterUpdate(collection, selected);
}
function _getUpdatedSignal(collection) {
return 'selectionService:updated:' + collection;
}
function _updateInGridSelections(gridApi, path, collection) {
var selectedInGrid = gridApi.getSelectedNodes(),
currentlySelected = _getCollection(collection),
gridPath = 'data.' + path;
_.each(selectedInGrid, function (node) {
if (!_.any(currentlySelected, path, _.get(node, gridPath))) {
// The following suppressEvents=true flag is ignored for now, but a
// fixing pull request is waiting at ag-grid GitHub.
gridApi.deselectNode(node, true);
}
});
var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),
currentlySelectedIds = _.pluck(currentlySelected, path),
missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);
if (missingIdsInGrid.length > 0) {
// We're trying to avoid the following loop, since it seems horrible to
// have to loop through all the nodes only to select some. I wish there
// was a way to select nodes/rows based on an id.
var i;
gridApi.forEachNode(function (node) {
i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));
if (i >= 0) {
// multi=true, suppressEvents=true:
gridApi.selectNode(node, true, true);
missingIdsInGrid.splice(i, 1); // Reduce haystack.
if (!missingIdsInGrid.length) {
// I'd love for `forEachNode` to support breaking the loop here.
}
}
});
}
}
var _service = {
getCollection: _getCollection,
add: _add,
remove: _remove,
getUpdatedSignal: _getUpdatedSignal,
updateInGridSelections: _updateInGridSelections
};
return _service;
});
selectionService
服务允许添加和删除任意对象到单独的集合,由 collection
标识,您认为合适的名称。这样,同一服务可用于在多个 ag-grid
实例中记住选择。每个对象都将使用 path
参数进行标识。 path
用于使用 lodash's get 函数检索唯一标识符。
此外,该服务使用 sessionStorage 在用户的整个 tab/browser 会话期间保持选择。这可能有点矫枉过正;我们可以只依赖服务来跟踪选择,因为它只会被实例化一次。这当然可以根据您的需要进行修改。
然后是必须对 GridController
进行的更改。首先,第一列的 columnDefs
条目必须稍微更改
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
一旦从远程服务器检索到数据,就会生成新生成的行 ID
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(包含 ID 中的 'm'
只是为了确保我没有将该 ID 与 ag-grid
使用的其他 ID 混淆。)
接下来,对 gridOptions
的必要更改是添加
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
不同的处理程序是否非常直接,与 selectionService
function rowSelected(event) {
selectionService.add(event.node.data, 'id', 'page-1');
}
function rowDeselected(event) {
selectionService.remove(event.node.data, 'id', 'page-1');
}
function clearSelections(event) {
$scope.gridOptions.api.deselectAll();
}
现在,GridController
也需要处理 selectionService
发出的更新信号
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
和
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
调用 selectionService.updateInGridSelections
将更新相关网格的网格内选择。那是写起来最麻烦的函数。例如,如果在外部(网格外)添加了一个选择,那么我们将不得不执行 forEachNode
运行,即使我们知道所有必要的节点都已在网格内选择;无法提前退出该循环。
最后,另一个难点是在更改过滤器或排序顺序时,或从服务器检索新数据时(仅在演示中模拟),分别清除并重新应用之前和之后的选择。解决方案是在 getRows
处理程序
params.successCallback
之后包含对 updateSelections
的调用
params.successCallback(rowsThisPage, lastRow);
updateSelections();
现在,在这个解决方案的实施过程中最令人费解的发现是 ag-grid
API 网格选项 onAfterFilterChanged
和 onAfterSortChanged
不能用于重新应用选择是因为它们在(远程)数据完成加载之前触发。