Trying to auto expand tree grid to the 3rd level with jqGrid - javascript

We are currently having a difficult time trying to auto expand a jqGrid treegrid to the 3rd level of the tree so that all children are visible. The current data set is thousands or rows deep and we were forced to dynamically load each node when requested to be expanded, which requires reloading the grid. Expanded node ids are saved in an array as a saved tree state so that the nodes can be re-expanded when the tree is redisplayed. This goes through the process of loading each node from the database as the expansion happens. AS each node is dynamically loaded and expended gridComplete and loadComplete events are handled as expected.
We are trying to trigger the 3rd level expansion by utilizing the save tree state and the existing logic to break out the tree appropriately within the existing logic. The problem we are experiencing is that the tree cannot expand out fast enough in order to be processed, and we can never break the tree apart completely.
Here is the function to iterate through the parents to capture the appropriate ids to expand:
function ExpandGridLevel(level) {
if (ExpandGridLevels == false) {
ExpandTotalLevels = level;
ExpandLevelCurrent = 0;
ExpandGridLevels = true;
}
if (!TreeExpandState) {
TreeExpandState = new Array();
}
$(".treeclick", "#Grid").each(function () {
if ($(this).hasClass("tree-plus")) {
var id = $(this).parents("tr").attr("id");
var rowLevel = $("#MaGrid").getLocalRow(id);
if (rowLevel.level == ExpandLevelCurrent) {
TreeExpandState.push(id);
$(this).removeClass("tree-plus");
$(this).addClass("tree-minus");
}
}
});
$(this).trigger("reloadGrid");
$("#Grid").jqGrid("setGridParam", { datatype: "local" });
ExpandLevelCurrent++;
RefreshGrid();
}
Our gridComplete and loadComplete sections of code:
loadComplete: function (data) {
$("#Grid").jqGrid("setGridParam", { datatype: "local" });
if (TreeExpandState.length == 0) {
setTimeout(function () { ExpandGridLevel (ExpandTotalLevels); }, 3000);
}
}
gridComplete: function () {
if (TreeExpandState) {
var rowId = TreeExpandState.shift();
ExpandNode(rowId);
}
}
Is what we are trying to do possible with jqGrid? If so, how do we know when the tree expansion is truly complete and we can reiterate over the expended grid to begin the expansion of the next level of parents?

Related

Retain the selection in previous page when moved to next in ExtJS Combo box

I have the ExtJs form combo which shows the values in the dropdown as checkbox and the value to select. I have used the pagination to list all the values with no of pages. I need the selected value to be retained in the current page even when we move to next or previous page and comes back to the same page(without selecting any thing in prev, next pages).
Code:
Ext.create('Ext.form.ComboBox', {
id: 'ageComboPickerReport',
name: 'ageComboPickerReport',
maxHeight: 150,
margin: '0 5 0 0',
width: 150,
emptyText: "Select tags",
listConfig: {
getInnerTpl: function (displayField) {
return '<div class="x-combo-list-item"><img src="" class="chkCombo-default-icon
chkCombo" /> {' + displayField + '}</div>';
},
autoEl: { 'q-name': 'rpt-age-criteria-list'
}, labelAlign: 'top',
pageSize: 25,
displayField: 'age', valueField: 'id', forceSelection: true, store: me.ageStore,
//Disable Typing and Open Combo
onFocus: function () {
if (!this.isExpanded) {
me.ageStore.load()
this.expand()
} this.getPicker().focus();
}
}),
Could any one tell me how to retain the selected value in the page when move to other page and comes back
I faced a very similar issue, in my case I had a grid with checkbox selection model, and I wanted to retain the selected rows during moving between pages. I am quite sure (although not 100%) that there is no built-in functionality for that in ExtJS. I can tell what I did, hope it helps, although you have to adopt it because it is not for ComboBox, but for Grid.
Since this is a paged store, not all of the records are loaded, only the ones that are currently on the displayed page. (I assume your store is remote because I see you call load on it.) So to achieve what you like, you need to:
keep track of the selected record ids to be able the reset selection on page,
keep track the items (records) themselves as well, since you will likely need them,
after a page is loaded and displayed, set the selection accordingly.
(This solution can have issues, so you need to be careful. Depending on the use case, it is possible for example that the user selects something, goes to another page and come back, but the previously selected row is no more available (it was deleted by someone else). You have to consider whether it affects you.)
The complete code is complicated and not general enough to share it, but I can outline the steps I did:
Set up two data entries in viewmodel to track the selected record ids and items (records):
data: {
multiSelection: {
ids: {},
items: [],
},
}
Add listeners to the grid's select and deselect events in the view:
listeners: {
select: 'onGridSelect',
deselect: 'onGridDeselect',
},
Create onGridSelect and onGridDeselect functions in the controller, and also add a isDataChanged variable to the controller to indicate whether the store was changed (it is changed on each paging). It is important because I will programatically change the selection, and I don't want my listeners to be executed in this case, only when the user interacts. Like this:
isDataChanged: false,
onGridSelect: function(selModel, record, index, eOpts) {
if (isDataChanged) {
return;
}
const viewModel = this.getViewModel(),
multiSelection = viewModel.get('multiSelection');
multiSelection.ids[record.getId()] = true;
Ext.Array.push(multiSelection.items, record);
},
onGridDeselect: function(selModel, record, index, eOpts) {
if (isDataChanged) {
return;
}
const viewModel = this.getViewModel(),
multiSelection = viewModel.get('multiSelection');
delete multiSelection.ids[record.getId()];
Ext.Array.remove(multiSelection.items, record);
},
Finally, add a listeners to the store to detect changes, this will be called every time the user moves between pages. This is a little tricky, because I need to access the grid from the store listeners which is not very ExtJS like, but I had to (store needs to be the grid's store:
store.on('datachanged', function(store, eOpts) {
// this part you have to figure out, my solution is way too specific
// for share
const grid = ...;
// this was important for me, if the store is really changed,
// deleted, added rows etc., I deleted the selection, but
// you can consider it
if (store.getUpdatedRecords().length > 0 ||
store.getRemovedRecords().length > 0 ||
store.getNewRecords().length > 0) {
// access the `viewmodel` and reset `multiSelection` data entries to
// default, I `return` here to skip the rest
return;
}
// disable listener
grid.getController().isDataChanged = true;
const selModel = grid.getSelectionModel(),
multiSelection = grid.getViewModel().get('multiSelection');
// deselect everything
selModel.deselectAll();
// get an array of the saved selection and find out, which
// record from the current page is in the saved multiSelection
const selected = [];
Ext.Array.each(grid.getStore().getRange(), function(record) {
if (multiSelection.ids[record.getId()]) {
Ext.Array.push(selected, record);
}
});
// apply selection to current page
selModel.select(selected);
// enable listener
grid.getController().isDataChanged = false;
}

Ag-grid-Enterprise expand/collapse all row using button? Very slow crashing FF and Edge

I created a button to expand all the rows in ag-grid (Enterprise) having 150 rows in the grid. It is working fine in Chrome but it is showing an alert in the latest FF and Edge, saying the web page is making your browser slow. Any better approach to expand all the row? It is taking almost 10-15 second
HTML
<button (click)="expandAll(expand)">Expand/Collapse</button>
JavaScript
this.columnDefs = [
{
headerName: "",
field: "",
cellRenderer: "group",// for rendering cell
suppressMenu: true,
suppressSorting: true
}
]
// This is how I am creating fullrow width
this.gridOptions = <GridOptions>{
isFullWidthCell: function (rowNode) {
var rowIsNestedRow = rowNode.flower;
return rowIsNestedRow;
},
fullWidthCellRendererFramework: AgGridInventorRowComponent,
doesDataFlower: function (dataItem) {
return true;
}
public expandAll(value:boolean) {
if(value) {
this.gridOptions.api.forEachNode((node) =>{
node.setExpanded(true);
});
} else {
this.gridOptions.api.forEachNode((node) =>{
node.setExpanded(false);
});
}
}
As per the documentation:
Calling node.setExpanded() causes the grid to get redrawn. If you have many nodes you want to expand, then it is best to set node.expanded=true directly, and then call api.onGroupExpandedOrCollapsed() when finished to get the grid to redraw the grid again just once.
So i modified my code like below:
this.gridOptions.api.forEachNode(node => {
node.expanded = true;
});
this.gridOptions.api.onGroupExpandedOrCollapsed();
Ag-gridDocumentation page inside Group Api
I'm supposing that you are using the row grouping feature, and that you meant that there are 150 grouped rows that are able to be expanded.
Currently your code is getting executed for every single row of data... not just the ones that are able to be expanded. So supposing you have 50 rows or so of data in each group, your calling the setExpanded function 7500 times. You can limit this to just calling the setExpanded on the grouped rows by putting in a check before calling setExpanded:
public expandAll(value:boolean) {
this.gridOptions.api.forEachNode((node) =>{
if (node.group)
node.setExpanded(value);
});
}
testing it on this example, it took roughly 2 seconds for 110 row groups and 5 seconds for 511 row groups in firefox
The API has expandAll and collapseAll:
api.expandAll();
api.collapseAll();
Note that due to the crappy architecture of AG Grid the expansion state (along with row selection etc) is lost if the row data changes or the grid is re-mounted/re-rendered. You should use deltaRowDataMode but make sure you give your rows a unique ID to help prevent this (though this option of course can cause some hard to debug bugs which I have reported).
Also if you want to restore the user expansion in this case you have no choice but to iterate and expand/collapse individual nodes.
Also they don't seem to work on a master-detail (enterprise feature) grid...
I hope this would help, the performance seems to be fine. Took reference from -
https://github.com/ag-grid/ag-grid/issues/2179
But it is always better to check if the groups are present are not. That increases the performance and expanding is much much faster.
this.gridApi.forEachNode((node) => {
if(node?.group) {
node.setExpanded(true)
}
})
I have an update as a complete solution.
Please see this example that I created o Plnkr.
Basically you can use the following code blocks to expand and collapse the tree data on the grid:
At first, imports, definitions and assignments:
import { GridApi } from 'ag-grid-community';
then:
gridApi!: GridApi; // variable
constructor() {}
onGridReady(params: GridReadyEvent) {
this.gridApi = params.api;
// other codes...
}
expand() {
this.gridApi.expandAll();
}
collapse() {
this.gridApi.collapseAll();
}
However if you want to collapse and expand a specific node level groups, you can use the following examples:
collapse2ndLevel() {
this.gridApi.forEachNode((node) => {
if (node.level === 1) {
node.setExpanded(false);
}
});
}
expand2ndLevel() {
this.gridApi.forEachNode((node) => {
if (node.level < 2 && node.isExpandable) {
node.setExpanded(true);
}
});
}
collapse3rdLevel() {
this.gridApi.forEachNode((node) => {
if (node.level === 2 && node.isExpandable) {
node.setExpanded(false);
}
});
}
expand3rdLevel() {
this.gridApi.forEachNode((node) => {
if (node.level < 3 && node.isExpandable) {
node.setExpanded(true);
}
});
}
Please check out this example that I created o Plnkr.
I use server-side row model and single decision for me it is purgeServerSideCashe() after update data. https://www.ag-grid.com/archive/23.2.0/javascript-grid-server-side-model-grouping/#example-purging-caches. It closes all expanded rows

Ext JS 4.2.1 - grid with paging - checkbox state lost

I have an ExtJS grid that has a PagingToolbar for (remote) paging, and a checkbox column defined as:
{
dataIndex : 'selected',
xtype: 'checkcolumn',
width : 60
}
However, if I check a box and then page forwards and backwards, the checkbox state is not saved - all the checkboxes are unchecked.
I guess since the store only contains data for the current page (from the server), I need some way of storing the state for rows that are not in the current page of data, and then reinstating the checkboxes when I return to that page.
Is there a best practice, or example of storing the checkbox state in the store while paging?
Well, that's so low-level work that no one has yet thought to make a "best practice" for it. E.g.
beforeload:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
store.checkedItems[item.get("Id")] = item.get("selected");
});
},
load:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
item.set("selected",store.checkedItems[item.get("Id")]);
});
}
or
beforeload:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
store.checkedItems[item.get("Id")] = {selected: item.get("selected") }; // extensible if you want to keep more than one checkbox...
});
},
load:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
item.set(store.checkedItems[item.get("Id")]);
});
}

AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrolling

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)" in page1.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.

DOM Elements loaded from ajax and (probably) not ready in time

I'm using sorting/filtering jQuery plugin Isotope and also jQuery $.ajax() to dynamically load some new elements to the page that need to be sorted with Isotope. That library seems to set all the new sorted elements with absolute position and with fixed (left, top) position in order to perform sorting.
The problem is that when you load the first set of elements with clear cache the the element positions in that absolute grid are incorrect (they are overlapping). This is caused by Isotope initialization. My (inexperienced) guess would be that the all the new DOM elements are not fully loaded by the time Isotope starts to calculate the future positions of the elements and there's where the in-accuracy comes in. If I do the exact same ajax request again it manages to calculate the positions correctly.
EDIT #1 ajax reuqest
var $isocont = $('#page-content-result');
var isoActive = false;
$.ajax({
url: actionUrl + 'ajax',
type: 'POST',
data: searchData,
success:function (data) {
if(data.trim().length > 0) {
$('#page-content-result').hide().empty().html(data).fadeIn(600);
initIsotope();
} else {
var visible = $('#page-content-result').is(':visible');
if(visible === true)
{
$('#page-content-result').empty();
}
$('#result-notif').show();
}
}
});
var initIsotope = function() {
if(isoActive === true) {
$isocont.isotope('destroy');
console.log('iso stop');
isoActive = false;
}
if(isoActive === false) {
$isocont.isotope({
getSortData: {
name: '.iso-docname',
}
});
console.log('iso start');
isoActive = true;
}
}
Can someone explain the nature of this problem and give few hints for solution?
Thnx!
Seems that once again the "morning is smarter than the night" (it's a saying in Estonian :P).
I placed a piece of test-code after part where the new data was inserted to the DOM.
if (/complete/.test(document.readyState)) {
alert('loaded');
}
The alert kind of pauses the loading of the page and I realized that the containers where quite a lot shorter because of the not-yet-loaded images. Meaning the DOM doesn't have the information how high the containers are going to be when we initialize the Isotope and that's why the Isotope fails to calculate the correct positions.
FIX: After understanding the problem the fix is really simple just tell the DOM how high the image is going to be in CSS with min-height: 122px; and that is all. I guess if the image height varies then I believe setting an interval checker for document.readyState == 'loaded' would help.
Idea came from that blog http://callmenick.com/2014/06/04/check-everything-loaded-javascript/
So my general js file now looks something like that:
var $isocont;
$(document)(function() {
//When DOM create a var of selector.
$isocont = $('#page-content-result');
//One time aka first init of Isotope with basic options.
$isocont.isotope({
getSortData: {
name: '.iso-docname',
}
});
$.ajax({
url: actionUrl,
type: 'POST',
data: searchData,
success:function (data) {
if(data.trim().length > 0) {
//Set HTML data from ajax request
$('#page-content-result').hide().empty().html(data).fadeIn(600);
//Reload isotope items and re-arrange the items according to config.
reloadIsotope();
} else {
//DO SMTH
}
}
});
});
var reloadIsotope = function() {
$isocont.isotope('reloadItems');
//We are resetting sorting since we might not want same ordering as on last result.
$isocont.isotope({sortBy: null, sortAscending: true});
}
Thanx TimSPQR for looking into!

Categories

Resources