AngularJS + ag-grid:带有虚拟分页/无限滚动的粘性/记住选择 [英] AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrolling

查看:115
本文介绍了AngularJS + ag-grid:带有虚拟分页/无限滚动的粘性/记住选择的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 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>

我想要完成的:

  1. 在将(虚拟)页面滚出视图并再次滚回时,会记住(粘性)在 ag-grid 中所做的选择,以便用户可以在不同的页面上选择多行.
  2. 记住的选择在网格之外可用,并支持添加和删除选择(如 page1.html 中的 ng-click="remove(row)" 预期的那样)代码>,如上所示).
  3. 当从带有 ag-grid 的视图切换到另一个视图并再次返回时,应该记住这些选择.
  4. (可选)记住用户会话的选择.

我怎样才能做到这一点?

解决方案

我创建了一个 工作示例可以实现.

首先,我们将编写一个 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 网格选项 onAfterFilterChangedonAfterSortChanged 不能'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:

  1. 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.
  2. That the remembered selections are available outside the grid, and support adding and removing selections (as intended by the ng-click="remove(row)" in page1.html, shown above).
  3. That the selections should be remembered when switching from the view with the ag-grid to another one, and back again.
  4. (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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆