Ag-grid - having expandable height cells causes rendering issues - javascript

I have an Ionic/Angular project where I wish to use the Ag-grid as a (virtual) container for my potentially very large list of items. I am very new the the ag-grid, I have played a little in the past, but still relatively new to it
Ag-grid is the has come the closest by far to being able to do what I am trying to achieve.
Basically I have a list of items, where I wish to use an Angular component in each cell, and this component has an "expander", with a varying number of sub items.
To get a component in the grid, I followed this excellent tutorial, which seemed to work perfectly.
It almost all works, but I find when I expand items down the list a bit (eg item position say 50), the UI starts to jump around, and not render properly. If you expand the first few, it is ok, it is only when you get down the list a bit I have problems.
I have a sample project here on Github.
It looks like the following...
My setup of the grid is in home.page.ts, where I have the following gridOptions
gridOptions: GridOptions = {
// Makes col take up full grid
onGridSizeChanged: () => {
this.gridOptions.api.sizeColumnsToFit();
} ,
getRowHeight(params) {
return params.data.getHeight();
},
getRowNodeId: function (data) {
return data.equipment;
},
}
The getRowHeight above will call ListItem.getHeight, and you can see this returns the height based on whether or not is it expanded...
public getHeight(): number {
let height = 72;
if (this.isOpen)
height += (this.jobs.length - 1) * 40;
return height;
}
Also, when the expander is clicked the refresh function passed into the constructor is called.
public setIsOpen(val: boolean) {
this.isOpen = val;
this.refresh();
}
And this call the following in the home.page.ts to try and refresh the grid cells..
private refreshCells(): void {
this.gridOptions.api.refreshCells({ force: true });
this.gridOptions.api.resetRowHeights();
}
And that's basically it.
It is close to working, ie click the initial items works, but when I scroll down, sometimes the view just does not refresh properly (sometimes it does, other times not).
Is this achievable, and if so, what can I do get get it to work better?

Related

Split table data into multiple pages for printing

This is more of an architectural issue, but basically, I have a server-side React app that renders a bunch of charts and tables with page breaks in-between, so that a puppeteer instance can open the page then print, and send that printed report back to the user in another app.
I need to be able to take some data that is normally rendered into a table format on this app, but make it printable so that the data extends as far as possible before a page break is required, then renders a new table past the page break (so it appears on a new page when printing), and continues until all of the data is rendered into the report. Essentially, I need pagination on a table, without the user interaction that pagination usually comes with.
The thing I'm struggling with is that the length of the data is dynamic, and so are the widths and heights of the rows.
Any suggestions on how to tackle this? The only thing I can think of so far is to basically hide the table, and measure the height of it after every row is attached, and compare that to the max height (the height of a standard letter size in pixels), and if it exceeds it, remove the row, add a page break, then start a new table.
Thanks in advance.
EDIT:
FYI, The solution mentioned here doesn't apply: How to apply CSS page-break to print a table with lots of rows?
This needs to be an entirely new table because I have custom headers and footers that are going above and below it (showing metadata like the name of the chart, how many rows are shown out of how many total, etc.), so it can't just be one continuous table that's split.
Here's a codepen with a shell of what I'm trying to do. If you open in debug view, and print it, you'll see in the print preview that the table is split up across two pages, but the footer I created will only be on the second page (where it needs to be on both pages, after the table). Additionally, the footer needs to display the dynamic count of rows that were able to fit on the page, so it can't be a static part of the table as a tfoot element. https://codepen.io/nicholaswilson/pen/GRWNzMa
So I'm trying to figure out now if I can mount the table to the DOM, but hide it, and calculate the height as I add rows to it so I can try my original method above. But I'm also open to other suggestions.
Alright, I think I got it. Still needs some tweaking (and there are probably more performant ways to do it) but this is my concept: https://codepen.io/nicholaswilson/pen/abJpLYE. Currently I'm splitting the tables after they've exceeded the height, but I'll be fixing that later. The concept is here.
Basically, the idea is to build a 4D array to represent the instances of tables that need to be rendered. Then in componentDidMount() and componentDidUpdate(), I can add new tables to the state as needed:
componentDidMount() {
const { tableData } = this.props;
if (this.state.currentRowIndex === 0) {
// just starting out
this.setState((state, props) => {
const tables = state.tables;
tables.push([tableData.data[state.currentRowIndex]]); // push first new table and first row
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: 0
};
});
}
}
componentDidUpdate() {
const { tableData } = this.props;
if (this.state.currentRowIndex < tableData.data.length) {
this.setState((state, props) => {
const tables = state.tables;
const currentTableHeight = this.tableRefs[this.state.currentTableIndex]
.clientHeight;
console.log(
`Table ${this.state.currentTableIndex} height: ${currentTableHeight}`
);
if (currentTableHeight > MAX_TABLE_HEIGHT_IN_PIXELS) {
// push a new table instead based on this condition
tables.push([tableData.data[state.currentRowIndex]]);
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: state.currentTableIndex + 1
};
} else {
tables[state.currentTableIndex].push(
tableData.data[state.currentRowIndex]
); // push new row to existing table
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: state.currentTableIndex
};
}
});
}
}
See the codepen for the rest of the implementation.

AG-Grid (Enterprise) Column Menu Listener

Is there a way to set a listener to column-menu, so that an event is fired when I open and close the menu?
Feature description: https://www.ag-grid.com/javascript-grid-column-menu/
I already searched in the official documentation, but didn't find an answer.
Background is:
I want to store the table state with displayed cols, sorting, position of cols, filter etc. in a database. Of course I could use the listeners like onFilterChanged, onDisplayedColumnsChanged or onSortChanged.
Problem is, that it will be fired every time when something changes and so there are produced a lot of unwanted api-calls.
Thats why I want to perform one call when the column-menu is closed.
Update
As Viqas said in his Answer, there is no official way to do it. I
tried to avoid the solution with postProcessPopup and tried to find a cleaner
solution for my problem - to store the table state.
For a workaround with a callback when ColumnMenu is closed Viqas Answer is more appropriate.
Notice that this is no workaround for the callback itself - it is just a (possible) solution to store the table state and perform ONE API Call
I used the ngOnDestory() function of Angular.
ngOnDestory(): void {
const tableState = {
columnState: this.gridOptions.columnApi.getColumnState(),
columnGroupState: this.gridOptions.columnApi.getColumnGroupState(),
sortState: this.gridOptions.api.getSortModel(),
filterState: this.gridOptions.api.getFilterModel(),
displayedColumns: this.gridOptions.columnApi.getAllDisplayedColumns()
};
// submit it to API
}
You're right, there's no official way to do it. A workaround could be to detect when the menu is closed yourself. Ag-grid does provide you the postProcessPopup callback (see here) which provides the parameter of type PostProcessPopupParams; this contains the column menu popup element that is displayed, so you could check when the menu is no longer visible.
Create a variable to store the columnMenu element in:
columnMenu: any = null;
Store the columnMenu in this variable using the ag-grid event postProcessPopup:
<ag-grid-angular [postProcessPopup]="postProcessPopup"></ag-grid-angular>
this.postProcessPopup = function(params) {
this.columnMenu = params.ePopup;
}.bind(this);
Then create a listener to detect when the column menu is no longer visible in the dom:
this.renderer.listen('window', 'click',(e:Event)=>{
console.log(this.columnMenu)
const columnMenuIsInDom = document.body.contains(this.columnMenu);
if (!columnMenuIsInDom && this.columnMenu != null)
{
this.columnMenu = null;
}
});
This is slightly hacky and a workaround, but I can't think of a better way at the moment.
Take a look at this Plunker for illustration.

Presetting kendo DataSource PageSize to "All"

Whenever a user changes the pagination on a grid, I save the setting in localStorage and retrieve it to set it back whenever the user navigates again to the page. To retrieve it I am using the pageSize property of the dataSource where I pass an IIF like so:
pageSize: function () {
var pageSize = 10;
if (window.localStorage) {
var userPreferencePageSize = localStorage.getItem("somegridPageSize");
if (userPreferencePageSize === parseInt(userPreferencePageSize)) {
userPreferencePageSize = parseInt(userPreferencePageSize);
}
if (!isNaN(userPreferencePageSize)) {
pageSize = userPreferencePageSize;
}
}
return pageSize;
}()
This worked well but a requirement appeared for the user to be able to set the pageSize to "All". Kendo handles "All" in the Grid so I thought this will let me set the dataSource pageSize to the string "All" (pageSize="All") as well. When I do it however the grid starts displaying NaN of X records and displays an empty grid. So the question is.. how do I preset the pageSize of the dataSource to just "All"?
NOTE: An alternative is to just fetch the grid maximum total count and then replace the number displayed in the dropdown with jquery text("All) but that looks like a hack and it seems to me this should already be inbuilt into the framework but I can't find anything in the doc's.
EDIT: This is getting even funnier. Due to lack of other options I implemented it like in the note and just set the dataSource pageSize directly:
$("#Grid").data("kendoGrid").dataSource.pageSize(pageSize);
But this is causing the filters on grid to malfunction and throw "string is not in correct format" (misleading error) error from the endpoint. After enough research I found out its caused by the DataSourceRequest doing some unidentifiable shuru buru in the background. Since setting the dataSource pageSize causes issues, I tried just setting the dropdown programatically and let kendo trigger the request to update pageSize itself like so:
var selectBox = $(".k-pager-sizes").find("select");
selectBox.val(pageSize);
selectBox.find("option[value='" + pageSize + "']").prop('selected', true);
But the grid doesn't let me do it and keeps reverting any changes I do inside the DOM from javascript.
So the question is, how in earth can you change the pageSize of a server-side kendo grid from javascript (triggering an extra request to endpoint).
To answer the question. This appears to be a bug in Kendo. Setting the pageSize to 0 (shortcut for "all") after the dataSource was already bound will always result in the grid having issues with any custom filters declared inside of a toolbar template.
To be exact, if you have a custom filter defined inside of the toolbar template:
#(Html.Kendo().TextBox().Name("Filter").Deferred())
You wire it up through the dataSource definition on the grid like:
.DataSource(ds =>
ds.Ajax()
.PageSize(defaultPageSize)
.Read(a => a.Action(actionName, controllerName, new
{
param = Model.param
}).Data("getFilterParameters")))
in javaScript fetching the parameters like:
getFilterParameters: function () {
return this.filterParameters;
},
populating them with a method:
filter: function () {
var grid = $("#Grid").data("kendoGrid");
this.filterParameters = {
param: $("#Filter").val()
};
grid.dataSource.page(1);
}
that has a simple event listener wired to it:
$("#Filter").on("keyup", filter);
Then after changing the pageSize programatically to 0/"all" with:
$("#Grid").data("kendoGrid").dataSource.pageSize(0);
The filter will start to always return NaN as the current page / skip of the filter object passed to the server with the query parameters (even though grid will display the numbers correctly). This will cause a "string is not in correct format" exception inside framework code on the endpoint whenever you try using the filter. A solution to the above is to slightly modify the getFilterParameters method:
getFilterParameters: function (e) {
// Workaround for Kendo NaN pager values if pageSize is set to All
if (isNaN(e.page)) e.page = 1;
if (isNaN(e.skip)) e.skip = 0;
//
return this.filterParameters;
}
This will re-initialize the page and skip values before submitting the filter request. These values will anyway be re-populated on the endpoint with the correct values. Wasn't me who noticed it but another developer working on the project, credit goes to her.

Kendo Treeview Expanding big tree issue

I have created a tree control using kendo TreeView.it has more than 10,000 nodes and i have used loadOnDemand false when creating Tree.
I am providing a feature to expand the tree by its level, for this i have created a method which takes the parameter "level" as number and expand it accordingly and user can enter 15 (max level) into the method, it works fine with 500 to 600 nodes for all the levels but when tree has more than 5000 nodes than if user is trying to expand above the 2nd level nodes then browser hangs and shows not responding error.
Method which i have created to expand the tree is :-
function ExapandByLevel(level, currentLevel) {
if (!currentLevel) {
currentLevel = 0;
}
if (level != currentLevel) {
var collapsedItems = $("#treeView").find(".k-plus:visible");
if (collapsedItems.length > 0) {
setTimeout(function () {
currentLevel++;
var $tree = $("#treeView");
var treeView = $tree.data("kendoTreeView");
var collapsedItemsLength = collapsedItems.length;
for (var i = 0; i < collapsedItemsLength; i++) {
treeView.expand($(collapsedItems[i]).closest(".k-item"));
}
ExapandByLevel(level, currentLevel);
}, 100);
}
else {
//console.timeEnd("ExapandByLevel");
hideLoading();
}
}
if (level == currentLevel) {
hideLoading();
}
}
call above given method like this:-
ExapandByLevel(15);
here 15 is level to expand in tree.
when tree has more than 5000 nodes than if user is trying to expand above the 2nd level nodes then browser hangs and shows not responding error.
please suggest any way to do this,what i want is expand the tree which can contains more than 5000 nodes.
I had a similar problem with kendo TreeView, when I wanted to load a tree with 30,000 nodes. The browser would freeze for a long time to load this number of nodes even when loadOnDemand was set to true.
So we decided to implement the server-side functionality for expanding nodes, and that's what you should do. You need to have 2 changes in your existing code.
Change your tree use server side Expand method.
When you call expand, you should make sure the node is expanded.
These two steps will be explained below. The thing you should know is, this way your browser doesn't hang at all, but it may take some time to complete the operation, because there will be so many webservice calls to the server.
Change your tree to use server side Expand method:
Please see Kendo UI's demos for Binding to Remote Data in this
link. Note that loadOnDemand should be set to true. In addition the server side Expand web service should be implemented too.
When you call expand, you should make sure the node is expanded:
In order to do this, there should be an event like Expanded defined in Kendo UI TreeView, but unfortunately there is none, except Expanding event. Using setTimeout in this case is not reliable, because the network is not reliable. So we ended up using a while statement to check that the node's children are created or not. There might be a better solution for this, however this satisfies our current requirement. Here's the change you should make when expanding nodes:
if (collapsedItems.length > 0) {
currentLevel++;
var $tree = $("#treeView");
var treeView = $tree.data("kendoTreeView");
var collapsedItemsLength = collapsedItems.length;
for (var i = 0; i < collapsedItemsLength; i++) {
var node = $(collapsedItems[i]).closest(".k-item")
if (!node.hasChildren)
continue; // do not expand if the node does not have children
treeView.expand(node);
// wait until the node is expanded
while (!node.Children || node.Children.length == 0);
}
ExapandByLevel(level, currentLevel);
}
You can also do the expand calls in a parallel way in order to decrease the loading time, but then you should change the way you check if all the nodes are expanded or not. I just wrote a sample code here that should work fine.
Hope this helps.
The solution to your problem is pretty simple: Update the version of Kendo UI that you are using since they have optimized (a loooooooooot) the code for HierarchicalDataSource and for TreeView.
Check this: http://jsfiddle.net/OnaBai/GHdwR/135/
This is your code where I've change the version of kendoui.all.min.js to v2014.1.318. I didn't even changed the CSS (despite you should). You will see that opening those 5000 nodes is pretty fast.
Nevertheless, if you go to 10000 elements you will very likely consider it slow but sorry for challenging you: do you really think that 10000 nodes tree is User Friendly? Is a Tree the correct way of presenting such a huge amount of data?

Sencha Touch 2.2 List not reusing list-items

I have a list linked to a store filled with Facebook friends. It contains around 350 records.
There is a searchfield at the top of the list which triggers the following function on keyup:
filterList: function (value) {
// console.time(value);
if (value === null) return;
var searchRegExp = new RegExp(value, 'g' + 'i'),
all = Ext.getStore('Friends'),
recent = Ext.getStore('Recent'),
myFilter;
all.clearFilter();
recent.clearFilter();
// console.log(value, searchRegExp);
myFilter = function (record) {
return searchRegExp.test(record.get('name'));
}
all.filter(myFilter);
recent.filter(myFilter);
// console.timeEnd(value);
},
Now, this used to work fine with ST2.1.1 but since I upgraded the app to ST2.2. It's really slow. It even makes Safari freeze and crash on iOS...
This is what the logs give :
t /t/gi Friends.js:147
t: 457ms Friends.js:155
ti /ti/gi Friends.js:147
ti: 6329ms Friends.js:155
tit /tit/gi Friends.js:147
tit: 7389ms Friends.js:155
tito /tito/gi Friends.js:147
tito: 7137ms
Does anyone know why it behaves like this now, or does anyone have a better filter method.
Update
Calling clearFilter with a true paramater seems to speed up things, but it's not as fast as before yet.
Update
It actually has nothing to do with filtering the store.
It has to do with rendering list-items. Sencha apparently create a list-item for every record I have in the store instead of just creating a couple of list-items and reusing them
Could there be an obvious reason it's behaving this way ?
Do you have the "infinite" config on your list set to true?
http://docs.sencha.com/touch/2.2.0/#!/api/Ext.dataview.List-cfg-infinite
You probably don't want the list rendering 300+ rows at once, so setting that flag will reduce the amount of DOM that gets generated.

Categories