2

在 AngularJS 应用程序中,我有一个ag-grid,它使用虚拟分页/无限滚动来延迟加载数据集中的行,该数据集太大而无法一次显示。我在第一列中打开了复选框选择,以便用户应该能够为任意特定于应用程序的操作选择单独的行。

AngularJS 应用程序使用ui-router来控制多个视图。因此,在使用“排序和过滤”的虚拟分页示例的基础上,使用来自文档的关于奥运会获胜者的构建数据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. 记住的选择在网格之外可用,并支持添加和删除选择(如上ng-click="remove(row)"所示page1.html)。
  3. ag-grid当从视图切换到另一个视图时,应记住选择,然后再返回。
  4. (可选)记住用户会话的选择。

我怎样才能做到这一点?

4

1 回答 1

3

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

首先,我们将编写一个 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 的 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;
       }

'm'包含在 ID 中只是为了确保我没有将该 ID 与 使用的其他 ID 混淆ag-grid。)

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运行,即使我们知道所有必要的节点都已在网格内被选中;没有办法提前退出那个循环。

最后,另一个关键是分别在更改过滤器或排序顺序或从服务器检索新数据(仅在演示中模拟)时清除并重新应用之前和之后的选择。解决方案是在处理程序内部updateSelections之后调用params.successCallbackgetRows

             params.successCallback(rowsThisPage, lastRow);
             updateSelections();

现在,在实施此解决方案期间最令人费解的发现是ag-gridAPI 网格选项不能用于重新应用选择,因为它们在(远程)数据完成加载之前触发onAfterFilterChangedonAfterSortChanged

于 2015-11-18T10:21:06.643 回答