Ag-Grid - Saving columns for future use - angularjs

I am using the ag-grid for angular1, (and loving it), and I want my users to be able to reorgenize columns, change sortings, and everything, and that it will stay after a refresh.
It should not be very hard, except that the columns are circular (contains pointers to themselves), and thus I cannot parse them.
Code:
var columnDefsKey = "columnDefs["+$rootScope.page+"]";
var savedColumns = localStorage.getItem(columnDefsKey);
function saveColumnsState() {
var currentCol = vm.gridOptions.columnApi.getAllColumns();
if (!angular.equals(currentCol, savedColumns))
try {
localStorage.setItem(columnDefsKey, JSON.stringify(currentCol));
} catch (ex) {
log(ex);
log(currentCol);
}
}
And:
onColumnEverythingChanged: saveColumnsState,
onColumnVisible: saveColumnsState,
onColumnPinned: saveColumnsState,
onColumnResized: saveColumnsState,
onColumnRowGroupChanged: saveColumnsState,
onColumnValueChanged: saveColumnsState,
onColumnMoved: saveColumnsState,
onColumnGroupOpened: saveColumnsState,
It fails on the "try" every time:
TypeError: Converting circular structure to JSON(…) [Column, Column, Column, Column, Column, Column, Column, Column, Column, Column]
How can I do that? (save columns for later use)
If I manage to do that, I will be able to create several views without coding.

you can get the better understanding of the issue from below link
Chrome sendrequest error: TypeError: Converting circular structure to JSON
Also check below reference
https://github.com/isaacs/json-stringify-safe

The way to achieve this was to build my own column model, that I can save and parse again, and in which to save only necessary properties.
This method is XSS vulnerable, as I am evaluating functions, but it is a working solution.
columnsApi: {
key: null,
grid: null,
newColumnModel: {
headerName: "",
width: 200,
valueGetter: "",
filter: 'text',
aggFunc: 'none',
filterParams: {apply: true}
},
setKey: function (key) {
this.key = key;
},
setGrid: function (grid) {
this.grid = grid;
},
format: function (columns) {
var format = [];
angular.forEach(columns, function (col) {
var colDef = {
width: col.actualWidth,
pinned: col.pinned,
hide: !col.visible
};
format.push(angular.extend(col.colDef, colDef));
});
return format;
},
getIDs: function (columns) {
var ids = [];
angular.forEach(columns, function (col) {
ids.push(col.colId);
});
return ids;
},
stringify: function (columns) {
return JSON.stringify(columns, function (key, value) {
if (typeof value === "function")
return "/Function(" + value.toString() + ")/";
return value;
});
},
parse: function (string) {
return JSON.parse(string, function (key, value) {
if (typeof value === "string" &&
value.startsWith("/Function(") &&
value.endsWith(")/")) {
value = value.substring(10, value.length - 2);
return eval("(" + value + ")");
}
return value;
});
},
add: function (column) {
if (this.grid === null) {
console.error("Assertion error: grid must not be null");
return;
}
if(column.aggFunc == 'none')
column.aggFunc = undefined;
var groups = this.get().groups;
var newColumns = this.format(getGridColumns(this.grid));
newColumns.push(column);
this.grid.api.setColumnDefs(newColumns);
this.setGroups(groups);
},
save: function () {
var self = this;
if (this.key === null) {
console.error("Assertion error: key must not be null");
return;
}
if (this.grid === null) {
console.error("Assertion error: grid must not be null");
return;
}
var savedOptions = {
columns: self.format(getGridColumns(self.grid)),
groups: self.getIDs(self.grid.columnApi.getRowGroupColumns()),
sorting: self.grid.api.getSortModel(),
filter: self.grid.api.getFilterModel()
};
localStorage.setItem(this.key, this.stringify(savedOptions));
},
// Get function uses "eval" - XSS vulnerable.
get: function () {
if (this.key === null) {
console.error("Assertion error: key must not be null");
return;
}
var options = localStorage.getItem(this.key);
if (options)
options = this.parse(options);
return options;
},
remove: function (field) {
if (this.grid === null) {
console.error("Assertion error: grid must not be null");
return;
}
var newColumns = this.format(getGridColumns(this.grid));
angular.forEach(newColumns, function (col, key) {
if (col.field == field)
newColumns.splice(key, 1);
});
this.grid.api.setColumnDefs(newColumns);
},
setGroups: function (groups) {
var self = this;
angular.forEach(groups, function (id) {
angular.forEach(getGridColumns(self.grid), function (col) {
if (col.colId == id)
self.grid.columnApi.addRowGroupColumn(col);
});
});
}
}
This solution was written for Ag-Grid 5 I believe, and thus I am not sure if it still holds.

Related

Protractor promises causing headaches when retrieving elements

Our web application has various html forms that each contain a list of form fields. Using Protractor, I'm looking for a way to retrieve the list of form fields: the field label, input type (textbox, select, radio, etc...), and input control (for setting the value later on). I then want to populate certain values for the fields in the form dynamically.
Here is the definition of the form field labels and the values I want to set:
this.fields = {
'Listing Agent': 1,
'Property Class': 1,
'Property Type': 2,
'Transaction Type': 'Sale',
'Ownership Terms': 'Sole Ownership',
'Listing Agreement': 'Yes',
'Display Listing on Internet': 'Yes',
'Display Address on Internet': 'Yes',
'Allow Automated Valuation on Internet': 'Yes',
'Allow Public Comment/Reviews on Internet': 'Yes'
};
I then retrieve the elements that match those field names by label text:
this.elements = form.find.allElements(this.fields);
When calling that method it retrieves the correct elements, but then I'm having trouble with setting the input type for each field. Checking the input type of a field returns a promise, not the actual value, so I can't figure out how to retrieve the input type for each element and then return an array of all of the elements.
this.find = {
allElements: function (fields) {
var items = [];
for (var key in fields) {
var el = element(by.cssContainingText('.sheet-grid-row', key));
this.getElementType(el).then(function (type) {
var item = {
type: type,
label: key,
isRequired: false,// TODO: el.getAttribute('class').indexOf('is-required-field') > -1
input: this.getElementInput(el, type)
};
items.push(item);
});
}
return items;// TODO: Doesn't work, of course...
},
getElementType: function (el) {
var deferred = protractor.promise.defer();
el.element(by.css('select')).isPresent().then(function (exists) {
if (exists)
deferred.fulfill(self.inputTypes.select);
else {
el.element(by.css('input[type="text"]')).isPresent().then(function (exists) {
if (exists)
deferred.fulfill(self.inputTypes.textbox);
else {
el.element(by.css('input[type="radio"]')).isPresent().then(function (exists) {
if (exists)
deferred.fulfill(self.inputTypes.textbox);
else
deferred.fulfill(self.inputTypes.unknown);
});
}
});
}
});
return deferred.promise;
},
getElementInput: function (el, type) {
switch (type) {
case self.inputTypes.select:
return new SelectWrapper(el.element(by.css('select')));
break;
case self.inputTypes.textbox:
return el.element(by.css('input[type="text"]'));
break;
case self.inputTypes.radio:
return el.element(by.css('input[type="radio"]'));
break;
}
return null;
}
};
At this point, I wish I could just get the native DOM elements and not deal with the promises at all. Is there a way to accomplish what I'm after?
You are trying to do asynchronous things element.isPresent() inside a synchronous loop for. You will want your allElements function to look more like this:
this.find = {
allElements: function (fields) {
var self = this;
var items = [];
var getItems = function(inputArray, currentIndex, outputArray) {
outputArray = outputArray || [];
currentIndex = currentIndex || 0;
var key = inputArray[currentIndex];
if (key) {
var el = element(by.cssContainingText('.sheet-grid-row', key));
return self.getElementType(el).then(function(type) {
var item = {
type: type,
label: key,
isRequired: false,// TODO: el.getAttribute('class').indexOf('is-required-field') > -1
input: self.getElementInput(el, type)
};
outputArray.push(item);
}).then(function() {
return getItems(inputArray, currentIndex + 1, outputArray);
});
} else {
return Promise.resolve(outputArray);
}
};
return getItems(Object.keys(fields));
},
getElementType: function (el) {
var self = this;
return el.element(by.css('select')).isPresent().then(function(exists) {
if (exists) {
return self.inputTypes.select;
} else {
return el.element(by.css('input[type="text"]')).isPresent().then(function(exists) {
if (exists) {
return self.inputTypes.textbox;
} else {
return el.element(by.css('input[type="radio"]')).isPresent().then(function(exists) {
if (exists) {
return self.inputTypes.radio;
} else {
return self.inputTypes.unknown;
}
});
}
});
}
});
},
getElementInput: function (el, type) {
switch (type) {
case self.inputTypes.select:
return new SelectWrapper(el.element(by.css('select')));
break;
case self.inputTypes.textbox:
return el.element(by.css('input[type="text"]'));
break;
case self.inputTypes.radio:
return el.element(by.css('input[type="radio"]'));
break;
}
return null;
}
};
Keep in mind that the output of find.allElements is now a promise, as it should be, so its usage will be something like:
this.find.allElements(this.fields).then(function(items) {
console.log('this is an array of my ' + items.length + ' items');
});

Changing the text of an element when referencing it from an array

I'm trying to change text elements in a page based on numerical values I pass back from my server. I manage to store each reference as well as the value from the server, but when trying to access to the text value of the object I get hit with the error statusText[key].text is not a function. is it possible to access the object properties when referencing from a list? Javascript and Angular are not my strong suits.
Here's the code:
var log = [];
var statusText = [];
$http.post("url", {userid: currentUserID})
.then(function(response)
{
angular.forEach(response.data.status, function(value, key){
this.push(value.complete);
}, log);
angular.forEach(document.getElementsByClassName("step-status"), function(value, key)
{
this.push(value)
}, statusText);
angular.forEach(statusText, function(value, key)
{
if (log[key] == 2)
{
statusText[key].text('Completed');
}
else if (log[key] == 1)
{
statusText[key].text('In progress');
}
else
{
statusText[key].text('Not started');
}
});
});
Try this
var log = [];
var statusText = [];
$http.post("url", {userid: currentUserID})
.then(function(response)
{
angular.forEach(response.data.status, function(value, key){
this.push(value.complete);
}, log);
angular.forEach(document.getElementsByClassName("step-status"), function(value, key)
{
this.push(value)
}, statusText);
angular.forEach(statusText, function(value, key)
{
if (log[key] === 2)
{
statusText[key] = 'Completed';
}
else if (log[key] === 1)
{
statusText[key] = 'In progress';
}
else
{
statusText[key] = 'Not started';
}
});
});
Sollution: Changed my approach for displaying the returned strings. Just set each element of the array to the string I want and used bindings to display the data.
Thanks for the help all.

Multiple optional filters in angular

I am very new to angular and, I am not sure how to control the behavior of my filters.
In the app, I have two different single-select drop down controls that filter the results of my data set and fill a table. However, even though these filters work, the results are dependent of both controls and if both are not being used , the empty set is returned. So, my question is: How can I use these filters optionally? So, the app returns every result when the filters are not used or returns the filtered results by one of the controls or both?
Thank you
Here is the code:
AngularJS
The filters for each control. They look very similar:
.filter('byField', function () {
return function (results, options) {
var items = { options: options, out: [] };
angular.forEach(results, function (value, key) {
for (var i in this.options) {
if ((options[i].value === value.fieldId &&
options[i].name === "Field" &&
options[i].ticked === true)) {
this.out.push(value);
}
}
}, items);
return items.out;
};
})
.filter('byClass', function () {
return function (results, options) {
var items = { options: options, out: [] };
angular.forEach(results, function (value, key) {
for (var i in this.options) {
if ((options[i].value === value.documentClass &&
options[i].name === "Class" &&
options[i].ticked === true)) {
this.out.push(value);
}
}
}, items);
return items.out;
};
})
HTML
This is what I am doing to populate the rows of the table:
<tr ng-repeat="result in results | byField:outputFields | byClass:outputClasses">
<td>{{result.documentId}}</td>
...
</tr>
Dorado7.1 in all event listeners provides a view implicit variable pointing to the current event host's view, the variable can completely replace the use of this scenario.
Well, as I imagined the answer was more related to set theory than to angular.
I just made an union between the empty set and every result, and it worked.
.filter('byField', function () {
return function (results, options) {
var items = { options: options, out: [] };
angular.forEach(results, function (value, key) {
if (options.length) {
for (var i in this.options) {
if ((options[i].value === value.fieldId &&
options[i].name === "Field" &&
options[i].ticked === true)) {
this.out.push(value);
}
}
} else {
this.out = results.slice();
}
}, items);
return items.out;
};
})

Nodejs async data duplication

I'm having some problems with one async process on nodejs.
I'm getting some data from a remote JSON and adding it in my array, this JSON have some duplicated values, and I need check if it already exists on my array before add it to avoid data duplication.
My problem is when I start the loop between the JSON values, the loop call the next value before the latest one be process be finished, so, my array is filled with duplicated data instead of maintain only one item per type.
Look my current code:
BookRegistration.prototype.process_new_books_list = function(data, callback) {
var i = 0,
self = this;
_.each(data, function(book) {
i++;
console.log('\n\n ------------------------------------------------------------ \n\n');
console.log('BOOK: ' + book.volumeInfo.title);
self.process_author(book, function() { console.log('in author'); });
console.log('\n\n ------------------------------------------------------------');
if(i == data.length) callback();
})
}
BookRegistration.prototype.process_author = function(book, callback) {
if(book.volumeInfo.authors) {
var author = { name: book.volumeInfo.authors[0].toLowerCase() };
if(!this.in_array(this.authors, author)) {
this.authors.push(author);
callback();
}
}
}
BookRegistration.prototype.in_array = function(list, obj) {
for(i in list) { if(list[i] === obj) return true; }
return false;
}
The result is:
[{name: author1 }, {name: author2}, {name: author1}]
And I need:
[{name: author1 }, {name: author2}]
UPDATED:
The solution suggested by #Zub works fine with arrays, but not with sequelize and mysql database.
When I try to save my authors list on the database, the data is duplicated, because the system started to save another array element before finish to save the last one.
What is the correct pattern on this case?
My code using database is:
BookRegistration.prototype.process_author = function(book, callback) {
if(book.volumeInfo.authors) {
var author = { name: book.volumeInfo.authors[0].toLowerCase() };
var self = this;
models.Author.count({ where: { name: book.volumeInfo.authors[0].toLowerCase() }}).success(function(count) {
if(count < 1) {
models.Author.create(author).success(function(author) {
console.log('SALVANDO AUTHOR');
self.process_publisher({ book:book, author:author }, callback);
});
} else {
models.Author.find({where: { name: book.volumeInfo.authors[0].toLowerCase() }}).success(function(author) {
console.log('FIND AUTHOR');
self.process_publisher({ book:book, author:author }, callback);
});
}
});
// if(!this.in_array(this.authors, 'name', author)) {
// this.authors.push(author);
// console.log('AQUI NO AUTHOR');
// this.process_publisher(book, callback);
// }
}
}
How can I avoid data duplication in an async process?
This is because you are comparing different objects and result is always false.
Just for experiment type in the console:
var obj1 = {a:1};
var obj2 = {a:1};
obj1 == obj2; //false
When comparing objects (as well as arrays) it only results true when obj1 links to obj2:
var obj1 = {a:1};
var obj2 = obj1;
obj1 == obj2; //true
Since you create new author objects in each process_author call you always get false when comparing.
In your case the solution would be to compare name property for each book:
BookRegistration.prototype.in_array = function(list, obj) {
for(i in list) { if(list[i].name === obj.name) return true; }
return false;
}
EDIT (related to your comment question):
I would rewrite process_new_books_list method as follows:
BookRegistration.prototype.process_new_books_list = function(data, callback) {
var i = 0,
self = this;
(function nextBook() {
var book = data[i];
if (!book) {
callback();
return;
}
self.process_author(book, function() {
i++;
nextBook();
});
})();
}
In this case next process_author is being called not immediately (like with _.each), but after callback is executed, so you have consequence in your program.
Not sure is this works though.
Sorry for my English, I'm not a native English speaker

Loading store dynamically based on visible page data for Ext.grid.Panel column

Below is a renderer for an Ext.grid.Panel column. Suppose contactStore has 2,000 values in it and all I care about is the name of the record based on the id (value parameter in this case), and my grid only has 25 rows/records in it for the page I'm on. How can I dynamically get the store so that I grab the relevant associated records (based on the foreign key) of the id of my grid column, rather than loading all 2,000 records? Is there a way to load the store and then in the callback, somehow have this "renderer" function display the values after the callback succeeded?
columns: [{
...
}, {
header: 'Contact Name',
flex: 1,
sortable: true,
dataIndex: 'contact_id',
renderer: function(value) {
var contactStore = Ext.StoreManager.lookup('Contacts');
return contactStore.getById(value).get('full_name');
}
}, {
You can adjust the collectData(records, startIndex) in the viewConfig for that:
Ext.create('Ext.grid.Panel', {
(...)
viewConfig: {
//this method needs to be adjusted
collectData: function(records, startIndex) {
var me = this;
//we can use a custom function for this
if (me.onBeforeCollectData) {
me.onBeforeCollectData(records);
}
var data = me.superclass.collectData.call(me, records, startIndex);
return data;
},
onBeforeCollectData: function(records) {
var newExtraParams = [];
var oldExtraParams;
var needToLoadStore = false;
var contactStore = Ext.StoreManager.lookup('Contacts');
if (contactStore) {
oldExtraParams = contactStore.oldExtraParams;
} else {
//don't use autLoad: true, this will be a local store
contactStore = Ext.create('Ext.data.Store', {
storeId:'Contacts',
(...)
});
needToLoadStore = true;
}
for (var x in records) {
//contact_id is read out here
var param = records[x].get('contact_id');
if (param) {
if (needToLoadStore == false && Ext.Array.contains(oldExtraParams, param) == false) {
needToLoadStore = true;
}
newExtraParams.push(param);
}
}
if (needToLoadStore == true) {
//we use this to load the store data => because of async: false property
Ext.Ajax.request({
scope: this,
//this is for synchronous calls
async: false,
url: (...),
method: (...),
params: newExtraParams,
success: function (res, opt) {
var responseObj = Ext.decode(res.responseText, false);
contactStore.loadData(responseObj); //or deeper in the responseObj if needed
contactStore.oldExtraParams = newExtraParams;
}
});
}
}
}
});

Resources