【问题标题】:AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrollingAngularJS + ag-grid:带有虚拟分页/无限滚动的粘性/记忆选择
【发布时间】:2016-02-20 00:46:39
【问题描述】:

在 AngularJS 应用程序中,我有一个 ag-grid,它使用 virtual paging/infinite scrolling 延迟加载数据集中的行,该数据集太大而无法立即显示。我在第一列中打开了check-box selection,这样用户应该能够为任意特定于应用程序的操作选择单独的行。

AngularJS 应用程序使用ui-router 来控制多个视图。因此,在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>

我想要完成的事情:

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

我怎样才能做到这一点?

【问题讨论】:

    标签: javascript angularjs datagrid angular-ui-router ag-grid


    【解决方案1】:

    我已经创建了一个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 在用户的整个选项卡/浏览器会话期间保留选择。这可能有点矫枉过正。我们本可以依靠服务来跟踪选择,因为它只会被实例化一次。这当然可以根据您的需要进行修改。

    然后必须对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 网格选项 onAfterFilterChangedonAfterSortChanged 无法用于重新应用选择,因为它们在(远程)数据之前触发加载完毕。

    【讨论】:

      猜你喜欢
      • 2020-11-05
      • 1970-01-01
      • 2020-04-07
      • 2019-03-30
      • 2018-11-17
      • 2018-08-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多