Export data in ag-grid for cell renderer - javascript

I'm using ag-grid and I have a column definition as following :
{
headerName: "Color",
valueGetter: function (params) {
return JSON.parse(params.data.color).name;
},
field: 'color',
cellRenderer: function (params) {
if (angular.isDefined(params.data) && angular.isDefined(params.data.color)) {
var color = JSON.parse(params.data.color);
return '<div style="width: 50px; height: 18px; background-color:' + color.htmlValue + ';"></div>';
}
},
suppressMenu: true,
suppressSorting: true
}
When I export the grid in CSV format, I get undefined for the color column, which is a cell renderer, I searched for a solution for this and I found this in the official documentation :
The raw values, and not the result of cell renderer, will get used,
meaning:
Cell Renderers will NOT be used.
Value Getters will be used.
Cell Formatters will NOT be used (use processCellCallback instead).
As you can see I'm already using a valueGetter but I always get undefined in the exported data for the color column.
How can I solve this ?

You could solve it by using processCellCallback when exporting to CSV. This way you can see and control exactly what is going to be exported.
Besides the column definition, you can pass other parameters to your grid options.
From ag-grid docs: What gets exported
The raw values, and not the result of cell renderer, will get used,
meaning:
...
Cell Formatters will NOT be used (use processCellCallback instead).
So, let's say that you have your column definition in a variable called columnDefs. Now you pass that to your gridOptions.
const gridOptions = {
columnDefs: columnDefs,
}
The latter code should work. So, now you want to handle the clicking on CSV Export on the context menu (You can also do it with a custom button if you may).
Export to CSV:
Now you have to add the provided getContextMenuItems function to you gridOptions object. For more info: Configuring the Context Menu
const gridOptions = {
columnDefs: columnDefs,
getContextMenuItems() {
return [
{
name: 'CSV Export',
action: function(params) {
gridOptions.api.exportDataAsCsv({
processCellCallback: function(cell) {
// Manipulate the value however you need.
return cell.value;
},
});
},
},
];
},
};
The idea is to get the CSV Export and programmatically add what you need to happen in the action. On the action, what you need is to call the exportDataAsCsv function from the gridOptions.api. Now (I know this is a ton of information) one of the options you have is to include your processCellCallback function where you can make sure you pass the cell value. Doing so is very useful because you can manipulate the value however you may need (e.g. Adding a $ sign to a number that is supposed to be money).
Custom button:
There is not much to say in the case you need a custom button. The only thing you would need to do is make sure you call the exportDataAsCsv on the gridOptions.api when onclick event is fired.
Something like:
onClick() {
gridOptions.api.exportDataAsCsv({
processCellCallback: ...
});
}

As another answer mentions, the ag-grid docs specifically state "Cell Renderers will NOT be used":
https://www.ag-grid.com/javascript-grid-excel/#what-gets-exported
I did a workaround that calls the cellRenderer from the processCellCallback function, like this:
processCellCallback: function (cell) {
var cellVal = cell.value;
if(_.get(cell, 'column.colDef.cellRenderer')) {
cellVal = cell.column.colDef.cellRenderer({value: cell.value});
}
return cellVal;
}

A lil late to the party but I recently had the same inconvenience, missing the raw values because four columns needed the cellRenderer and as you know per the docs those values will not be used. In my example I needed to plug in the raw values for in_progress, complete, unanswered, & needs_review
gridApi.exportDataAsExcel({
...
processCellCallback: (params) => processCells(params)
});
const processCells = (cell) => {
let cellVal = cell.value;
const {
in_progress,
complete,
unanswered,
needs_review
} = cell.node.data.percentageStatus; // raw data source
if(!cell.value){ // for the 4 cols this was always undefined
switch(cell.column.colId){
case "in_progress":
cellVal = in_progress;
break;
case "completed":
cellVal = complete;
break;
case "unanswered":
cellVal = unanswered;
break;
case "needs_review":
cellVal = needs_review;
break;
default:
cellVal = "n/a";
}
}
return cellVal;
}

I've found these answers a little confusing. Hoping to help the next dev looking into this. Vue JS with Vux store.
const actions = {
exportPurchaseOrderLines({ state: stateObject }) {
//set gridApi in state
stateObject.gridApi.exportDataAsCsv({
// nice file name
fileName: `${stateObject.order.number}-export`,
// select only selected rows
onlySelectedAllPages: true,
processCellCallback: (params) => {
// use the header name to determine which column to process
if (params.column.colDef.headerName === 'Linked bills') {
// if there are linked bills, return the bill numbers
if (params.node.data.bills.length > 0) {
return params.node.data.bills.map((bill) => bill.number).join(', ');
}
}
// return all the other cells without issues
return params.value;
},
});
},
// more actions .....
};

This is how I solved this issue :
When I call the export function the params arg in the cellRenderer function is not the same when the grid is loaded, the params arg looks like this when we export :
{export:true, value: "{...}"}
the value is the value in the field key, so in the cellRenderer function I had to do like this :
if(angular.isDefined(params.export)){
return JSON.parse(params.value.slice(1, -1)).name;
}
Notice that double quotes are always added which looks weird, so when we have a string value in the field, params.value will look like this : ""theStringValue"", that's why I use params.value.slice(1, -1).

Related

Why doesn't custom hook return updated data?

There is a custom useMarkers hook for converting markers into clusters on the map.
It takes needed props,but the essential one are options prop.
const { markersArray, supercluster } = useMarkers({
points: transformedMarkers,
bounds,
zoom,
options: defaultOptions2, // <=== HERE IS WHAT WE NEED
});
And here is it from the inside:
const initialMarkersData = {
markersArray: [],
supercluster: []
};
const useMarkers = (initialData = {}) => {
const [markersData, setMarkersData] = useState(initialMarkersData);
const { clusters, supercluster } = useSupercluster(initialData);
useEffect(() => {
if (initialData.zoom <= 17) {
// console.log("SET NON-CLUSTERS");
setMarkersData({
...markersData,
markersArray: initialData.points,
});
} else {
// console.log("SET CLUSTERS");
setMarkersData({
markersArray: clusters,
supercluster,
});
}
}, [initialData.zoom, clusters, supercluster]);
return markersData;
};
Inside useMarkers I`m passing props to another hook, useSupercluster (comes from separate npm library), and depending on options prop it should return an updated clusters array, and depending on the zoom level an additional useEffect should change the markersData, which is then returns as a result.
And the problem is that when I add a condition for passing different options, the new array is not returned.
For example, if I pass options like this, it works:
options: defaultOptions2,
But if I add condition instead of just single defaultOptions2
options: zoom < 17 ? defaultOptions : defaultOptions2
It doesn't work. If I console.log initialData inside useMarkers hook, I will see new options, so the condition works correct, but although this hook is re-renders, it looks like useSupercluster() doesn`t reinitialize again because it doesn't return modified array according to new options. What could be the reason?
UPDATE*: I tried to investigate this behavior with debugger and everything looks correct. The supercluster property, which is destructured from useSupercluster hook, has to have the this options in it, and although new options successfully passed to useSupercluster within initialData, supercluster property got the empty object at the end;

How can I improve this function so that it overwrites the Excel cell?

I'm using this code to try and overwrite a cell in Excel:
function readExcelFile(workbook, row, cell, newData) {
workbook.xlsx.readFile('H://filename.xlsm')
.then(function () {
cell.value = newData;
row.commit();
return workbook.xlsx.writeFile('H://newFile.xlsx');
});
}
I can get the data logged to the console, but the Excel file stays the same.
Any help would be great, thanks.
Presumably, the function parameters row and cell are objects related to workbook? However, they'll lose this connection within the scope of the anonymous function where you're writing the file. (Technically speaking, JavaScript passes variables to functions by value and not by reference.)
Instead pass the info needed to find the cell, and get it locally within the function:
function readExcelFile(workbook, worksheetName, rowIndex, columnIndex, newData) {
workbook.xlsx.readFile('H://filename.xlsm')
.then(function () {
worksheet = workbook.getWorksheet(worksheetName);
row = worksheet.getRow(rowIndex);
cell = row.getCell(columnIndex);
cell.value = newData;
row.commit();
return workbook.xlsx.writeFile('H://newFile.xlsx');
});
}
readExcelFile(my_workbook, "My Worksheet", 3, 3, 34232);

How do I get and set ag-grid state?

How can I obtain and re-set the state of an ag-grid table? I mean, which columns are being show, in what order, with what sorting and filtering, etc.
Update: getColumnState and setColumnState seem to be close to what I want, but I cannot figure out the callbacks in which I should save and restore the state. I tried restoring it in onGridReady but when the actual rows are loaded, I lose the state.
There is a very specific example on their website providing details for what you are trying to do: javascript-grid-column-definitions
function saveState() {
window.colState = gridOptions.columnApi.getColumnState();
window.groupState = gridOptions.columnApi.getColumnGroupState();
window.sortState = gridOptions.api.getSortModel();
window.filterState = gridOptions.api.getFilterModel();
console.log('column state saved');
}
and for restoring:
function restoreState() {
gridOptions.columnApi.setColumnState(window.colState);
gridOptions.columnApi.setColumnGroupState(window.groupState);
gridOptions.api.setSortModel(window.sortState);
gridOptions.api.setFilterModel(window.filterState);
console.log('column state restored');
}
I believe you are looking for this page of the Docs. This describes the column api and what functions are available to you. The functions that are of most relevance would be:
getAllDisplayedColumns() - used to show what columns are able to be rendered into the display. Due to virtualization there may be some columns that aren't rendered to the DOM quite yet, iff you want only the columns rendered to the DOM then use getAllDisplayedVirtualColumns() - both functions show the order as they will be displayed on the webpage
The Column object that is returned from these functions contains sort and filter attributes for each of the columns.
I believe that all you need would be available to you from that function which would be called like this gridOptions.columnApi.getAllDisplayedColumns()
Other functions which might be useful:
Available from gridOptions.columnApi:
getColumnState() - returns objects with less detail than the above funciton - only has: aggFunc, colId, hide, pinned, pivotIndex, rowGroupIndex and width
setColumnState(columnState) - this allows you to set columns to hidden, visible or pinned, columnState should be what is returned from getColumnState()
Available from gridOptions.api:
getSortModel() - gets the current sort model
setSortModel(model) - set the sort model of the grid, model should be the same format as is returned from getSortModel()
getFilterModel() - gets the current filter model
setFilterModel(model) - set the filter model of the grid, model should be the same format as is returned from getFilterModel()
There are other functions that are more specific, if you only want to mess with pinning a column you can use setColumnPinned or for multiple columns at once use setColumnsPinned and more functions are available from the linked pages of the AG-Grid docs
The gridReady event should do what you need it to do. What I suspect is happening is your filter state is being reset when you update the grid with data - you can alter this behaviour by setting filterParams: {newRowsAction: 'keep'}
This can either by set by column, or set globally with defaultColDef.
Here is a sample configuration that should work for you:
var gridOptions = {
columnDefs: columnDefs,
enableSorting: true,
enableFilter: true,
onGridReady: function () {
gridOptions.api.setFilterModel(filterState);
gridOptions.columnApi.setColumnState(colState);
gridOptions.api.setSortModel(sortState);
},
defaultColDef: {
filterParams: {newRowsAction: 'keep'}
}
};
I've created a plunker here that illustrates how this would work (note I'm restoring state from hard code strings, but the same principle applies): https://plnkr.co/edit/YRH8uQapFU1l37NAjJ9B . The rowData is set on a timeout 1 second after loading
To keep the filter values you may use
filterParams: {newRowsAction: 'keep'}
Here's my approach. It simply involves creating a wrapper function with the same API as instantiating the agGrid.
A few things of interest in this function
saves/loads to local storage
makes use of the addEventListener that becomes available to the options object once it is passed in to the Grid function.
attaches to the onGridReady callback of the options object without erasing what may already be defined.
usage:
newAgGrid(document.getElementById('my-grid'), options);
static newAgGrid(element, options) {
const ls = window.localStorage;
const key = `${location.pathname}/${element.id}`;
const colStateKey = `${key}colstate`;
const sortStateKey = `${key}sortState`;
function saveState(params) {
if (ls) {
ls.setItem(colStateKey, JSON.stringify(params.columnApi.getColumnState()));
ls.setItem(sortStateKey, JSON.stringify(params.api.getSortModel()));
}
}
function restoreState(params) {
if (ls) {
const colState = ls.getItem(colStateKey);
if (colState) {
params.columnApi.setColumnState(JSON.parse(colState));
}
const sortState = ls.getItem(sortStateKey)
if (sortState) {
params.api.setSortModel(JSON.parse(sortState));
}
}
}
// monitor the onGridReady event. do not blow away an existing handler
let existingGridReady = undefined;
if (options.onGridReady) {
existingGridReady = options.onGridReady;
}
options.onGridReady = function (params) {
if (existingGridReady) {
existingGridReady(params);
}
restoreState(params);
}
// construct grid
const grid = new agGrid.Grid(element, options);
// now that grid is created, add in additional event listeners
options.api.addEventListener("sortChanged", function (params) {
saveState(params);
});
options.api.addEventListener("columnResized", function (params) {
saveState(params);
});
options.api.addEventListener("columnMoved", function (params) {
saveState(params);
});
return grid;
}
we can use useRef() to refer <AgGridReact> element to access getColumn and setColumn state in following way.
const GridRef = useRef()
GridRef.current.columnApi.getColumnState() // get Column state value
GridRef.current.columnApi.setColumnState() // set Column state value
<AgGridReact
enableFilter={true}
rowStyle={rowStyle}
ref={GridRef}
></AgGridReact>
The following needs to be done.
Include a div for the grid in your html
<div id="myGrid" style="width: 500px; height: 200px;"></div>
On the js side
//initialize your grid object
var gridDiv = document.querySelector('#myGrid');
//Define the columns options and the data that needs to be shown
var gridOptions = {
columnDefs: [
{headerName: 'Name', field: 'name'},
{headerName: 'Role', field: 'role'}
],
rowData: [
{name: 'Niall', role: 'Developer'},
{name: 'Eamon', role: 'Manager'},
{name: 'Brian', role: 'Musician'},
{name: 'Kevin', role: 'Manager'}
]
};
//Set these up with your grid
new agGrid.Grid(gridDiv, gridOptions);
Check this pen out for more features. https://codepen.io/mrtony/pen/PPyNaB . Its done with angular

Ember - Custom Computed Property to check if all dependent fields exists

I am creating a form and I am trying to find a simple, elegant way of handling to see if all inputs exist.
Form = Ember.Object.extend({
// section 1
name: null,
age: null,
isABoolean: null,
// section 2
job: null,
numberOfSiblings: null,
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('name')) && isPresent(this.get('age')) && isPresent(this.get('isABoolean'));
}.property('name', 'age', 'isABoolean'),
_isSection2Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('job')) && isPresent(this.get('numberOfSiblings'));
}.property('job', 'numberOfSiblings')
});
However, this doesn't seem to scale. My actual application will have many sections (over 20 sections).
I am looking into trying to create a re-usable computed property that fits my needs. Take for example the code of what I am going for:
Form = Ember.Object.extend({
// properties...
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: Ember.computed.allPresent('name', 'age', 'isABoolean'),
_isSection2Complete: Ember.computed.allPresent('job', 'numberOfSiblings')
});
I feel that this is a common case, but I'm failing to find the correct computed properties on how to execute this, so I would like to make my own.
Two questions:
Where's the best place to define the custom computed property? Can I just attach a function to Ember.computed?
Is there an easier way to solve this? I feel like I'm overlooking something simple.
As for Question #1,
You can define a custom computed helper in the App namespace. In this example, I created a new computed helper called allPresent that checks each property passed in against Ember.isPresent.
App.computed = {
allPresent: function (propertyNames) {
// copy the array
var computedArgs = propertyNames.slice(0);
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
It can be used like this, per your example code:
_isSection2Complete: App.computed.allPresent(['job', 'numberOfSiblings'])
I adapted this from the approach here: http://robots.thoughtbot.com/custom-ember-computed-properties
As for Question #2, I can't think of a simpler solution.
I had to make a minor adjustment to Evan's solution, but this works perfectly for anyone else that needs it:
App.computed = {
allPresent: function () {
var propertyNames = Array.prototype.slice.call(arguments, 0);
var computedArgs = propertyNames.slice(0); // copy the array
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
This can now be used as such:
_isSection2Complete: App.computed.allPresent('job', 'numberOfSiblings')

XUL/Thunderbird: startEditing return

I'm playing with the thunderbird codebase, the aim being to implement inline contact editing. The current code catches the Click event on a XUL tree, and if it's a double click (events.detail == 2), it open the profile editor. I modified it so as to start editing the current treeCell, and I did add editable=true to the corresponding XUL document. The updated code reads
var orow = {}, ocolumn = {}, opart = {};
gAbResultsTree.treeBoxObject.getCellAt(event.clientX, event.clientY,
orow, ocolumn, opart);
var row = orow.value, column = ocolumn.value.index;
if (row == -1)
return;
if (event.detail == 2)
gAbResultsTree.startEditing(row, column);
Unfortunately, when the code reaches the startEditing part, it returns
Error: uncaught exception: [Exception... "Component returned failure code: 0x80004001 (NS_ERROR_NOT_IMPLEMENTED) [nsITreeView.isEditable]" nsresult: "0x80004001 (NS_ERROR_NOT_IMPLEMENTED)" location: "JS frame :: chrome://global/content/bindings/tree.xml :: startEditing :: line 337" data: no]
I'm pretty much lost here. Could someone with more XUL experience help?
Thanks!
I was trying to do something similar and I have same problem.
Wrapper with original abview set as __proto__ with functions overriden works fine until it is set as abResultsTree's view.
I've finally found (I hope) an elegant solution.
function MyAbView() {
this.originalAbViewInstance = this.originalAbViewFactory.createInstance(null, Ci.nsIAbView);
if (!this.proxiesGenerated) {
// find out which interfaces are implemented by original instance, their property proxies will be generated later
for (var ifName in Ci) {
if (Ci[ifName] instanceof Ci.nsIJSID && this.originalAbViewInstance instanceof Ci[ifName]) {
MyAbView.prototype.supportedInterfaces.push(Ci[ifName]);
}
}
function generatePropertyProxy(name) {
Object.defineProperty(MyAbView.prototype, name, {
get: function() {
return this.originalAbViewInstance[name];
},
set: function(val) {
this.originalAbViewInstance[name] = val;
},
enumerable: true
});
}
for (var prop in this.originalAbViewInstance) {
if (this[prop] == undefined) {
generatePropertyProxy(prop);
}
}
MyAbView.prototype.proxiesGenerated = true;
} else {
for each (var interface in this.supportedInterfaces) {
this.originalAbViewInstance.QueryInterface(interface);
}
}
}
MyAbView.prototype = {
classID: null,
_xpcom_factory: {
createInstance: function(outer, iid) {
return new MyAbView().QueryInterface(iid);
}
},
QueryInterface: function(aIID) {
for each (var interface in this.supportedInterfaces) {
if (interface.equals(aIID)) {
return this;
}
}
throw Components.results.NS_ERROR_NO_INTERFACE;
},
originalAbViewFactory: null,
originalAbViewInstance: null,
proxiesGenerated: false,
supportedInterfaces: [],
// any overriden functions come here
};
It's implemented as a component to replace the original abview, but it might be modified to just create a wrapper.
The <tree> widget uses an nsITreeView object to retrieve or manipulate data that needs to be displayed. There are predefined nsITreeView implementations reading data from the DOM or RDF datasources but one can choose to use his own tree view. Thunderbird's address book chooses the latter:
gAbView = Components.classes["#mozilla.org/addressbook/abview;1"]
.createInstance(Components.interfaces.nsIAbView);
...
gAbResultsTree.treeBoxObject.view =
gAbView.QueryInterface(Components.interfaces.nsITreeView);
Unfortunately for you, the component in question is implemented in C++, in the file nsAbView.cpp. This means that changing it without recompiling Thunderbird isn't possible. And the existing component doesn't implement isEditable() and setCellText() methods that would be required to edit tree cells.
If you don't want to mess with C++ yet, you could wrap that component in your own object. Something like this:
gAbView = Components.classes["#mozilla.org/addressbook/abview;1"]
.createInstance(Components.interfaces.nsIAbView);
gAbViewWrapper = {
__proto__: gAbView,
QueryInterface: function(iid)
{
gAbView.QueryInterface(iid);
return this;
},
isEditable: function(row, col)
{
// Do something here
},
setCellText: function(row, col, value)
{
// Do something here
}
};
...
gAbResultsTree.treeBoxObject.view =
gAbViewWrapper.QueryInterface(Components.interfaces.nsITreeView);
Method isEditable() should check again whether this particular cell is editable - even if the column is editable, individual cells don't have to be. And setCellText() should store the new value for the cell.

Categories

Resources