Javascript array grid object property serialized by Kendo is null on server - javascript

I have a Kendo grid that I have an editable detail template. In the detail template, I have a multiselect.
When I save, I am manually updating the dataItem to grab the most up to date options in the detail template's multiselect. There is no problem with grabbing the data from the multiselect. Everything actually looks pretty good until I trigger the grid to save.
I add the multiselect items to the grid's dataItem with this code:
function SaveRefItemSubCategories() { //Called on Save button click
var grid = $("#RefItemSubCategoryGrid").data("kendoGrid");
for (var i = 0; i < grid.dataSource._data.length; i++) {
var dataItem = grid.dataSource._data[i];
dataItem.RefItemCategoryServiceIDs = [];
var ms = $("#MultiSelect_" + dataItem.uid).data("kendoMultiSelect");
var values = ms.value();
for(var x = 0; x < values.length; x++) {
dataItem.RefItemCategoryServiceIDs.push(values[i]);
}
}
grid.dataSource.sync();
}
At that point, when I look at what is posted to the server, the grid's dataSource looks like this for the serialized multiselect items:
models[0].RefItemCategoryServiceIDs[] 15
&models%5B0%5D.RefItemCategoryServiceIDs%5B%5D=15
It should look like this:
models[0].RefItemCategoryServiceIDs[0] 15
&models%5B0%5D.RefItemCategoryServiceIDs%5B 0%5D=15
Sorry for extra space in there, I couldn't get it to bold otherwise
The lack of the array's index in the serialized data results in that property being null when it hits the server. this property's type is List
I have tried a couple different approaches, but the end up creating new properties on the dataItem that stick around and cause problems after I save for the first time.
Some examples I've used instead of the push() method are:
This one does send the data back, but it ignores the existing list and creates a new dataSource property for each array index. This would work if I was only saving once, but if I save the grid multiple times, then the properties added stick around and mess with data integrity. I would entertain this option if a way to 'reset' the dataSource was possible after saving (without reloading the grid).
dataItem["RefItemCategoryServiceIDs[" + i + "]"] = values[i];
This one had the same effect as the .push(), which is the property on my object on the server being null
dataItem.RefItemCategoryServiceIDs[i] = values[i];
Please let me know if I can provide any more information and thanks in advance!

I'm not confident that this is the best way to handle it, but I have puzzled out a way that works.
I changed adding to the data item's properties from this:
for(var x = 0; x < values.length; x++) { //values is array from multiselect
dataItem.RefItemCategoryServiceIDs.push(values[i]);
}
to this:
for (var i = 0; i < values.length; i++) { //values is array from multiselect
dataItem["RefItemCategoryServiceIDs[" + i + "]"] = values[i];
}
Just this change works for every scenario EXCEPT this:
Save row with 2 options chosen from multiselect, resulting in 2 properties being added to the dataItem. We'll say the ID's of those items are 1 and 21. Posted data looks like this:
models[0].RefItemCategoryServiceIDs[0] = 1
models[0].RefItemCategoryServiceIDs[1] = 21
After saving, if we open that multiselect again and delete the first option (the one with ID 1), we get the following data posted:
models[0].RefItemCategoryServiceIDs[0] = 21
models[0].RefItemCategoryServiceIDs[1] = 21
Which is obviously not right (should just be 1 entry with the ID set to 21). This is happening because the RefItemCategoryServiceIDs isn't really an array, just properties whose names look like array entries so that they serialize into a list on the server (as I understand it). To mitigate this issue, I preceded the code that maps the current multiselect values to delete the existing properties associate with this array:
function CleanDataItemLists(dataItem) {
if (dataItem.RefItemCategoryCarrierIDs != undefined && dataItem.RefItemCategoryCarrierIDs.length > 0) {
var i = 0;
//Continues to remove properties by index until none are left
while (dataItem.RefItemCategoryCarrierIDs[i] != undefined) {
delete dataItem["RefItemCategoryCarrierIDs[" + i + "]"];
i++;
}
//Cleans up empty array property
delete dataItem.RefItemCategoryCarrierIDs;
}
return dataItem;
}
After this function is called, you can execute the for loop code referenced above and the data serializes with an index present (See initial post for example of missing index in serialized data), allowing the server to map it to the list object. Like I said, I'm not convinced that this is the best way to do this, but it does work for me. Hopefully, this can help someone else or lead to an even better answer.

Related

getRange() with specific criteria in Google Apps Sheets Script

I'm very new to JavaScript, and have only recently started fiddling with coding.
I had found a question that is similar to what I'm asking, so I've tried to modify the suggestions to fit my needs, but am drawing a blank pulling together other resources to understand the solution.
That question and answer is here: range.getValues() With specific Date in Specific Cell
I hope to ask my question by adding it to that thread, but I'm unable to make a comment - only an Answer, as I have zero votes.
I want to know how I can use the range.setValues() function, but specify only rows of cells that fit a certain criteria (e.g. cell in column B = 1).
Mostly I understand the answer, I think - push the full list of data into another array, then use a for loop to identify which rows to "keep".
However, I don't understand a good chunk of the solution, specifically this portion:
data.forEach(function(row) {
var colHvalue = row[7];
var colKvalue = row[10];
if( /* your desired test */) {
// Add the name of the sheet as the row's 1st column value.
row.unshift(sheetName);
// Keep this row
kept.push(row);
}
});
where I'm confused:
What's function(row) refer to? It seems to me that "row" is not a defined variable, and is also not a method.
What are the objects being specified in the line var colHvalue = row[7];? And same for the second row.
How does the pushing and unshifting work?
Hope for some answers, thanks!
Edit:
So I still don't understand the detailed logic of the Answer to that other question, and wrote something else based off that:
function importtest() {
var sheetX = SpreadsheetApp.openById("XXX-XXX-XXX"); // workbook containing orginal data
var tabX = sheetX.getSheetByName("EXPORTER"); // tab containing original data
var rangeX = tabX.getRange(6,2,sheetX.getLastRow(),sheetX.getLastColumn()).getValues(); //range of full original data
var kept = [] ;
var data = rangeX ; //storing full data in "data"
// define function "latestrows", which fills up the "kept" array with selected rows
function latestrows() {
var datesent = data[i][2] ; // indicating dynamic cell position (using variable i) that determines if should be kept or not
var datarow = data[i] ; // specify the entire row of data
// loop statement; starting with i=0 (or row 1), and increasing until the last row of "data"
for ( i = 0; i < data.length ; i++) {
if (datesent == 1) { // if cell in the col indicating if it should be kept, is "1"
kept.push(datarow); //move the chosen rows of data into "kept"
}
}
}
var sheetX3 = SpreadsheetApp.openById("XXX-XX-XX"); // workbook import destination
var tabX3 = sheetX3.getSheetByName("IMPORTED"); // import destination tab
var rangeX3 = tabX3.getRange(7,3,kept.length,kept[0].length) // imported array range
rangeX3.setValues(kept); // set values
}
I get the error message TypeError: Cannot read property "length" from undefined. (line 31, file "Code"), which together with testing other modifications indicates to me that kept is empty. What am I missing?

Posting model back to controller containing complex collection

I've posted a question similar to this, but I don't believe it clearly stated my question correctly. So here goes a better explanation!
I'm working on a C# MVC 5 application. I have a page that dynamically lists the model's collection property A. Each item in this collection uses a bit of bootstrap to show/hide it's own collection property B. So just to clarify that: This model has a collection of objects each of which has a collection of objects associated with it. These objects each have a checkbox associated with them bound to an isChecked boolean too that can be used to select/deselect items. Checking a box of an item from collection A, automatically selects all checkboxes of its collection B. So, as you can perhaps see, the sole purpose of having a checkbox on collection A items, is to act as a select all for its associated collection B items.
I'm trying to pass the entire model back to the controller on POST and then do stuff with it, e.g. grab all items that have their , but am running into a few problems.
E.g.
#foreach (var category in Model.Categories)
{
var headerId = category.Name;
var childCounter = 0;
#*some divs and headers here*#
#if (category.Items.Any())
{
#*lets give it the ability to check all of its associated items*#
#Html.CheckBoxFor(d => category.IsChecked, new {id = #headerId, onclick = String.Format("CheckUncheckChildren({0}, {1})", #headerId, category.MenuItems.Count)})
}
else
{
#Html.CheckBoxFor(d => category.IsChecked)
}
#*some other divs and headings*#
#if (category.MenuItems.Any())
{
#foreach (var item in dealItemCategory.MenuItems)
{
var childId = childCounter++ + "_" + headerId;
var serverId = item.Id;
#*some other divs and headings*#
#Html.CheckBoxFor(d => item.IsChecked, new {id = #childId})
}
}
}
Lower down on the page I have:
for (var i = 0; i < Model.Categories.Count(); i++)
{
var category = Model.Categories.ElementAt(i);
var headerId = category.Name;
#Html.HiddenFor(d => category, new {id = #headerId})
for (var j = 0; j < Model.Categories.ElementAt(i).Items.Count(); j++)
{
var item = Model.Categories.ElementAt(i).Items.ElementAt(j);
#Html.HiddenFor(d => item, new {id = j + "_" + #headerId})
}
}
In my mind, I'm retaining these properties on the page for this complex collection... (not that my understanding is 100%) I've forced ids for these hidden fields so that they correlate with the ids of the Checkboxes. Maybe not the best idea, but it was just the last thing I tried to get this to work...
Now I have two options here:
1) Believe in the above to mean that whenever a checkbox is selected/deselected, the object in the collection it is associated with, will get its isChecked boolean changed. Then, POST back the entire model and hope I'll be able to cycle through all checked items and do awesome stuff.
or
2) Create a string property in the model to hold a JSON representation of this complex collection of objects. When the page is POSTED, update this JSON string to the state of the complex collection that has been hopefully updated to have some of its items checked/unchecked. Then, POST back the entire model and server-side I would deserialize this single object into a logically equivalent complex collection and work from there.
I.e.
My model change would be adding a property:
public string JsonCategories{ get; set; }
And then in the view I'd have to initiate a post back:
#Html.HiddenFor(d => d.JsonCategories);
function accept() {
var categories= #Html.Raw(Json.Encode(Model.Categories));
var jsonCategories = JSON.stringify(categories);
var a = $('#JsonCategories').val();
$('#JsonCategories').val(jsonCategories );
$("form").submit();
};
Attempting solution A, I either get a null for the Categories property, or if I add #Html.HiddenFor(d => d.Categories); I get an empty object, count of 0.
Attempting solution B, I do get a lovely JSON representation for the complex Categories back in server land, but I see checkboxes I checked aren't changing the isChecked bools leading me to believe that, while the JSON object is able to be set to the Categories object on POST client-side, everything done by the user isn't being kept so ultimately the Categories collection of the model hasn't changed since it was passed to the View initially.
Sorry if this all seems complicated, it is and for a junior like me, I thought it best I ask around as I've never posted back lists and stuff to the controller before.
I'd be happy to provide more information if it will help!
Warmest regards,
If you change your foreach loops to for loops you can make use of the loop index in the lambda expression. This should make the engine output the correct syntax for posting the values. For example:
#for (int i = 0; i < Model.Categories.Count(); i++)
{
...
#Html.CheckBoxFor(d => d.Categories[i].IsChecked)
...
}
This should correctly bind the values to the POST request. The resulting html will be similar to the following snippet (0 being the first item, 1 would be the second etc.):
<input name="Categories[0].IsChecked" ... ></input>
Alternatively you can create editor templates for your child collections, which will result in you writing out this snippet to replace where your for loop is currently.
#Html.EditorFor(m => m.Categories)
More on how to do this here.
This should help you go with option 1.

how can i update an item in a collection, without breaking angular databinding?

I have a collection of objects, I think this is called an associative array. Regardless, I would like to locate an item in that list and update it. As a side note this collection is the ng-model for a select. I know lodash has some of this type of functionality. I can find the item but I am not sure what is the best way to update the item so the data binding still works on my select.
Here is an example that doesn't work:
for (x = 0; x < $scope.RulesTemplates.length; x++)
{
if($scope.RulesTemplates[x].Name == $scope.TempRules.Name)
{
$scope.RulesTemplates[x] = $scope.TempRules;
}
}
Assuming a sample dataset, if you want to extend an existing object in an array that is bound to ng-options, you first need to make sure the angular has the array item being tracked by a specific unique property. Then you could just update the new item in the array at the respective place, angular will automatically perform the update in the bound select options (without re-rendering other option values). But that approach might not refresh already bound ng-model.
Another simple approach using traditional loop:-
<select ng-options="item.display for item in items" ng-model="selected"></select>
and your update logic can just be:-
for (x = 0, l = $scope.items.length; x < l; x++){
var item = $scope.items[x];
if(item.name == itemsUpdate.name) {
angular.extend(item,itemsUpdate); //Extend it
break;
}
}
Demo1
or since you are using lodash, probably less lines of code:-
var item = _.first($scope.items, {name:itemsUpdate.name}).pop(); //Get the item add necessary null checks
angular.extend(item, itemsUpdate);
Demo2
angular.extend will make sure underlying source object reference remains the same with updating the properties, you could also use lodash merge.
Try something like this:
angular.forEach($scope.RulesTemplates, function(value, key){
if(value.Name == $scope.TempRules.Name){
value = $scope.TempRules;
}
})
angular.forEach

indexOf returning -1 despite object being in the array - Javascript in Google Spreadsheet Scripts

I am writing a script for a Google Docs Spreadsheet to read a list of directors and add them to an array if they do not already appear within it.
However, I cannot seem to get indexOf to return anything other than -1 for elements that are contained within the array.
Can anyone tell me what I am doing wrong? Or point me to an easier way of doing this?
This is my script:
function readRows() {
var column = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("Director");
var values = column.getValues();
var numRows = column.getNumRows();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var directors = new Array();
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
if (directors.indexOf(row) == -1) {
directors.push(row);
} else {
directors.splice(directors.indexOf(row), 1, row);
}
}
for (var i = 2; i < directors.length; i++) {
var cell = sheet.getRange("F" + [i]);
cell.setValue(directors[i]);
}
};
When you retrieve values in Google Apps Script with getValues(), you will always be dealing with a 2D Javascript array (indexed by row then column), even if the range in question is one column wide. So in your particular case, and extending +RobG's example, your values array will actually look something like this:
[['fred'], ['sam'], ['sam'], ['fred']]
So you would need to change
var row = values[i];
to
var row = values[i][0];
As an aside, it might be worth noting that you can use a spreadsheet function native to Sheets to achieve this (typed directly into a spreadsheet cell):
=UNIQUE(Director)
This will update dynamically as the contents of the range named Director changes. That being said, there may well be a good reason that you wanted to use Google Apps Script for this.
It sounds like an issue with GAS and not the JS. I have always had trouble with getValues(). Even though the documentation says that it is a two dimensional array, you can't compare with it like you would expect to. Although if you use an indexing statement like values[0][1] you will get a basic data type. The solution (I hope there is a better way) is to force that object into a String() and then split() it back into an array that you can use.
Here is the code that I would use:
var column = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("Director");
var values = column.getValues();
values = String(values).split(",");
var myIndex = values.indexOf(myDirector);
If myDirector is in values you will get a number != -1. However, commas in your data will cause problems. And this will only work with 1D arrays.
In your case: var row = values[i]; row is an object and not the string that you want to compare. Convert all of your values to an array like I have above and your comparison operators should work. (try printing row to the console to see what it says: Logger.log(row))
I ran into a similar problem with a spreadsheet function that took a range as an object. In my case, I was wanting to do a simple search for a fixed set of values (in another array).
The problem is, your "column" variable doesn't contain a column -- it contains a 2D array. Therefore, each value is it's own row (itself an array).
I know I could accomplish the following example using the existing function in the spreadsheet, but this is a decent demo of dealing with the 2D array to search for a value:
function flatten(range) {
var results = [];
var row, column;
for(row = 0; row < range.length; row++) {
for(column = 0; column < range[row].length; column++) {
results.push(range[row][column]);
}
}
return results;
}
function getIndex(range, value) {
return flatten(range).indexOf(value);
}
So, since I wanted to simply search the entire range for the existance of a value, I just flattened it into a single array. If you really are dealing with 2D ranges, then this type of flattening and grabbing the index may not be very useful. In my case, I was looking through a column to find the intersection of two sets.
Because we are working with a 2D array, 2dArray.indexOf("Search Term") must have a whole 1D array as the search term. If we want to search for a single cell value within that array, we must specify which row we want to look in.
This means we use 2dArray[0].indexOf("Search Term") if our search term is not an array. Doing this specifies that we want to look in the first "row" in the array.
If we were looking at a 3x3 cell range and we wanted to search the third row we would use 2dArray[2].indexOf("Search Term")
The script below gets the current row in the spreadsheet and turns it into an array. It then uses the indexOf() method to search that row for "Search Term"
//This function puts the specified row into an array.
//var getRowAsArray = function(theRow)
function getRowAsArray()
{
var ss = SpreadsheetApp.getActiveSpreadsheet(); // Get the current spreadsheet
var theSheet = ss.getActiveSheet(); // Get the current working sheet
var theRow = getCurrentRow(); // Get the row to be exported
var theLastColumn = theSheet.getLastColumn(); //Find the last column in the sheet.
var dataRange = theSheet.getRange(theRow, 1, 1, theLastColumn); //Select the range
var data = dataRange.getValues(); //Put the whole range into an array
Logger.log(data); //Put the data into the log for checking
Logger.log(data[0].indexOf("Search Term")); //2D array so it's necessary to specify which 1D array you want to search in.
//We are only working with one row so we specify the first array value,
//which contains all the data from our row
}
If someone comes across this post you may want to consider using the library below. It looks like it will work for me. I was getting '-1' return even when trying the examples provide (thanks for the suggestions!).
After adding the Array Lib (version 13), and using the find() function, I got the correct row!
This is the project key I used: MOHgh9lncF2UxY-NXF58v3eVJ5jnXUK_T
And the references:
https://sites.google.com/site/scriptsexamples/custom-methods/2d-arrays-library#TOC-Using
https://script.google.com/macros/library/d/MOHgh9lncF2UxY-NXF58v3eVJ5jnXUK_T/13
Hopefully this will help someone else also.
I had a similar issue. getValues() seems to be the issue. All other methods were giving me an indexOf = -1
I used the split method, and performed the indexOf on the new array created. It works!
var col_index = 1;
var indents_column = main_db.getRange(1,col_index,main_db.getLastRow(),1).getValues();
var values = String(indents_column).split(","); // flattening the getValues() result
var indent_row_in_main_db = values.indexOf(indent_to_edit) + 1; // this worked
I ran into the same thing when I was using
let foo = Sheet.getRange(firstRow, dataCol, maxRow).getValues();
as I was expecting foo to be a one dimensional array. On research for the cause of the apparently weird behavior of GAS I found this question and the explanation for the always two dimensional result. But I came up with a more simple solution to that, which works fine for me:
let foo = Sheet.getRange(firstRow, dataCol, maxRow).getValues().flat();

Javascript database rows in array - how to change value?

I have a database query that returns rows into a local array:
for (var i=0; i < results.rows.length; i++)
{
localResultsArray[i] = results.rows.item(i);
}
Later on, I want to update a value in the local array that corresponds to the 'answered_correctly' column in my database, but this value is not being updated. My code is:
localResultsArray[currentQuestionNumber].answered_correctly = 1;
but this doesn't change the value in the array to 1 for some reason - what am I doing wrong?
(Incidentally, if I do a comparison, eg. in an if statement then it works, so I must be using the wrong syntax above??)
if (localResultsArray[currentQuestionNumber].answered_correctly == 2)
{
incrementMe++;
}
Thanks in advance! Nick.
for (var i=0; i < results.rows.length; i++) {
localResultsArray[i] = results.rows.item(i);
}
As already pointed out, item() is unlikely to be a method, you probably meant item[i].
localResultsArray[currentQuestionNumber].answered_correctly = 1;
If localResultsArray[currentQuestionNumber] references an array, then the above line sets the answered_correctly property to 1. Is that what you want to do? It will not change the value in any array. You may want to do:
localResultsArray[answered_correctly] = 1;
or
localResultsArray[currentQuestionNumber] = 1;
depending on which of those variables references the column number.
Javascript arrays are just objects with a special length property (and some handy methods), the members of the array are just properties with numeric names (indexes or keys). So if you want to access the members of the array, use numeric property names. Using alphabetic names adds a new property that is not a member of the array.
From a quick review of your code, I'm wondering if the following line is corret:
localResultsArray[i] = results.rows.item(i);
I don't know what library you use that gives you the results object, but I highly doubt rows.item is a function and not an array...
Try
localResultsArray[i] = results.rows.item[i];
If it doesn't work, update your post with the library you used or some more data on the results object...

Categories

Resources