AngularJS + ag-grid:带有虚拟分页/无限滚动的粘性/记住选择 [英] AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrolling
问题描述
在 AngularJS 应用程序中,我有一个 ag-grid 使用 虚拟分页/无限滚动从太大而无法显示的数据集中延迟加载行立刻.我在第一列中打开了 复选框选择,这样用户应该能够为任意特定于应用程序的操作选择单独的行.
AngularJS 应用程序使用 ui-router 来控制多个视图.因此,在 虚拟分页示例的基础上,使用排序和过滤"",使用来自 ag-grid
文档的有关奥运会获胜者的构建数据,我进一步扩展了代码.来自 index.html
:
<div ui-view="内容"></div>
以及以下 ui-router
声明:
myapp.config(function($stateProvider, $urlRouterProvider) {$urlRouterProvider.otherwise("example.page1")$stateProvider.state('例子', {摘要:真实,意见:{内容: {模板:'<div ui-view="example"></div>'}}}).state('example.page1', {网址:'/page1',意见:{例子: {templateUrl: 'page1.html'}}}).state('example.page2', {网址:'/page2',意见:{例子: {模板:'回到<a ui-sref="example.page1">示例网格</a>.'}}});});
其中 page1.html
如下所示:
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div><div><h3>选定的行:</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>
<p>转到<a ui-sref="example.page2">另一个页面</a>.</p>
我想要完成的:
- 在将(虚拟)页面滚出视图并再次滚回时,会记住(粘性)在
ag-grid
中所做的选择,以便用户可以在不同的页面上选择多行. - 记住的选择在网格之外可用,并支持添加和删除选择(如
page1.html
中的ng-click="remove(row)"
预期的那样)代码>,如上所示). - 当从带有
ag-grid
的视图切换到另一个视图并再次返回时,应该记住这些选择. - (可选)记住用户会话的选择.
我怎样才能做到这一点?
我创建了一个 工作示例可以实现.
首先,我们将编写一个 AngularJS 服务,selectionService
来跟踪选择:
function _emptyArray(array) {而(数组.长度){数组.pop();}}函数_updateSharedArray(目标,源){_emptyArray(目标);_.each(source, function _addActivity(activity) {目标.推(活动);});}myapp.factory('selectionService', function ($rootScope, $window) {var _collections = {},_storage = $window.sessionStorage,_prefix = 'selectionService';angular.element($window).on('storage', _updateOnStorageChange);功能_persistCollection(集合,数据){_storage.setItem(_prefix + ':' + collection, angular.toJson(data));}功能_loadCollection(集合){var item = _storage.getItem(_prefix + ':' + collection);返回项目 !== null ?angular.fromJson(item) : item;}功能_updateOnStorageChange(事件){var item = event.originalEvent.newValue;var keyParts = event.originalEvent.key.split(':');if (keyParts.length <2 || keyParts[0] !== _prefix) {返回;}var collection = keyParts[1];_updateSharedArray(_getCollection(collection), angular.fromJson(item));_broadcastUpdate(集合);}功能_broadcastUpdate(集合){$rootScope.$emit(_service.getUpdatedSignal(collection));}功能_afterUpdate(集合,选择){_persistCollection(集合,选择);_broadcastUpdate(集合);}函数_getCollection(集合){如果 (!_.has(_collections, collection)) {var data = _loadCollection(collection);//保存对共享数组的引用.只变异,不取代._collections[collection] = 数据 !== null ?数据 : [];}返回_collections[集合];}功能_添加(项目,路径,集合){//将`item` 添加到`collection`,其中item 将由`path` 标识.//例如,路径可以是 'id', 'row_id', 'data.athlete_id',//任何适合添加的行数据.var 选择 = _getCollection(collection);if (!_.any(selected, path, _.get(item, path))) {selected.push(item);}_afterUpdate(集合,选择);}功能_删除(项目,路径,集合){//从`collection` 中移除`item`,其中item 由`path` 标识,//就像在 _add() 中一样.var 选择 = _getCollection(collection);_.remove(selected, path, _.get(item, path));_afterUpdate(集合,选择);}函数_getUpdatedSignal(集合){返回选择服务:更新:"+集合;}函数_updateInGridSelections(gridApi,路径,集合){var selectedInGrid = gridApi.getSelectedNodes(),currentSelected = _getCollection(collection),gridPath = '数据.'+ 路径;_.each(selectedInGrid,函数(节点){if (!_.any(currentlySelected, path, _.get(node, gridPath))) {//下面的suppressEvents=true 标志暂时被忽略,但是一个//修复拉取请求正在 ag-grid GitHub 上等待.gridApi.deselectNode(node, true);}});var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),currentSelectedIds = _.pluck(currentlySelected, path),missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);如果(missingIdsInGrid.length > 0){//我们试图避免以下循环,因为它看起来很可怕//必须遍历所有节点才能选择一些.我希望在那里//是一种基于 id 选择节点/行的方法.变量 i;gridApi.forEachNode(函数(节点){i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));如果 (i >= 0) {//multi=true,suppressEvents=true:gridApi.selectNode(node, true, true);missingIdsInGrid.splice(i, 1);//减少干草堆.如果(!missingIdsInGrid.length){//我希望 `forEachNode` 支持在这里打破循环.}}});}}var_service = {getCollection: _getCollection,添加:_添加,删除:_remove,getUpdatedSignal: _getUpdatedSignal,updateInGridSelections: _updateInGridSelections};返回_服务;});
selectionService
服务允许添加和删除任意对象到单独的集合,由 collection
标识,一个您认为合适的名称.这样可以使用相同的服务来记住多个 ag-grid
实例中的选择.每个对象都将使用 path
参数进行标识.path
用于使用 lodash 的 get 函数检索唯一标识符.>
此外,该服务使用 sessionStorage 来持久化在用户的整个选项卡/浏览器会话期间的选择.这可能有点矫枉过正;我们本可以依靠服务来跟踪选择,因为它只会被实例化一次.这当然可以根据您的需要进行修改.
然后必须对GridController
进行更改.首先,必须稍微更改第一列的 columnDefs
条目
var columnDefs = [{标题名称:#",宽度:60,field: 'id',//<-- 现在我们使用生成的行 ID.复选框选择:真,抑制排序:真,抑制菜单:真}, ...
从远程服务器检索数据后生成新生成的行 ID
//添加行 ID.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');}函数rowDeselected(事件){selectionService.remove(event.node.data, 'id', 'page-1');}功能清除选择(事件){$scope.gridOptions.api.deselectAll();}
现在,GridController
也需要处理由 selectionService
发出的更新信号
$scope.$on('$destroy',$rootScope.$on(selectionService.getUpdatedSignal('page-1'),更新选择));
和
函数 updateSelections() {selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');}
调用 selectionService.updateInGridSelections
这将更新相关网格的网格内选择.那是编写最麻烦的函数.例如,如果在外部(网格之外)添加了一个选择,那么我们将不得不执行 forEachNode
运行,即使我们知道已经在网格中选择了所有必要的节点;没有办法提前退出那个循环.
最后,另一个关键是分别在更改过滤器或排序顺序或从服务器检索新数据时(仅在演示中模拟)之前和之后清除和重新应用选择.解决方案是在 getRows
处理程序
params.successCallback
之后包括对 updateSelections
的调用 params.successCallback(rowsThisPage, lastRow);更新选择();
现在,在实施此解决方案期间最令人费解的发现是 ag-grid
API 网格选项 onAfterFilterChanged
和 onAfterSortChanged
不能't 用于重新应用选择,因为它们在(远程)数据加载完成之前触发.
In an AngularJS application, I have an 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 in the first column, so that the user should be able to select individual rows for arbitrary application-specific actions.
The AngularJS application uses ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering", with constructed data about Olympic winners, from the ag-grid
documentation, I've further extended the code a bit. From index.html
:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
and the following ui-router
states:
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>.'
}
}
});
});
where page1.html
looks like the following:
<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>
What I want to accomplish:
- That selections made in the
ag-grid
is remembered (sticky) when scrolling a (virtual) page out of view and back again, so that a user can select multiple rows on separate pages. - That the remembered selections are available outside the grid, and support adding and removing selections (as intended by the
ng-click="remove(row)"
inpage1.html
, shown above). - That the selections should be remembered when switching from the view with the
ag-grid
to another one, and back again. - (Optional) That the selections are remembered for the user's session.
How can I accomplish this?
I've created a working example of this can be implemented.
First of all, we'll write a AngularJS service, selectionService
to keep track of the selections:
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;
});
The selectionService
service allows adding and removing arbitrary objects to separate collections, identified by collection
, a name you find suitable. This way the same service can be used for remembering selections in multiple ag-grid
instances. Each object will be identified using a path
parameter. The path
is used to retrieve the unique identifier using lodash's get function.
Furthermore, the service uses sessionStorage to persist the selections during the user's whole tab/browser session. This might be overkill; we could have just relied on the service to keep track of the selections since it will only get instantiated once. This can of course be modified to your needs.
Then there were the changes that had to be done to the GridController
. First of all the columnDefs
entry for the first column had to be changed slightly
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
where the new, generated row ID is generated once the data has been retrieved from the remote server
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(The 'm'
in the ID was included just to make sure I didn't confused that ID with other IDs used by ag-grid
.)
Next, the necessary changes to gridOptions
were to add
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
Were the different handlers are quite straight forward, communicating with the 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();
}
Now, the GridController
needs to handle updates signalled by the selectionService
too
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
and
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
calls selectionService.updateInGridSelections
which will update the in-grid selections of the grid in question. That was the most cumbersome function to write. For example, if a selection has been added externally (outside the grid), then we'll have to perform a forEachNode
run, even if we know all the necessary nodes have already been selected in-grid; there's no way to exit that loop early.
Finally, another crux was to clear and reapply the selections before and after, respectively, when the filters or sort orders are changed, or when new data is retrieved from the server (which is only simulated in the demo). The solution was to include a call to updateSelections
after the params.successCallback
inside the getRows
handler
params.successCallback(rowsThisPage, lastRow);
updateSelections();
Now, the most puzzling findings during the implementation of this solution was that the ag-grid
API grid options onAfterFilterChanged
and onAfterSortChanged
couldn't be used for reapplying the selections because they trigger before the (remote) data has finished loading.
这篇关于AngularJS + ag-grid:带有虚拟分页/无限滚动的粘性/记住选择的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!