I have a table that displays SQL data. I'm trying to only allow the user to edit an entry within a row if that value already exists somewhere in that column.
For example, if I tried to change the value "Strawberry" in the column "Fruit" to "Grape", when I click out of the text field, it should only keep the value "Grape", if it already exists within the column "Fruit". Otherwise, it should return to "Strawberry".
I am trying to accomplish this like so:
On ng-focus, I call recordValue(). This gets the entire row, the column, and the value, and sets it in a separate variable.
On ng-blur, I call validateValue(), and check all of my rows for
a) the original row index
b) to see if the value exists already in any row in the table
c) if it does, keep it, else revert to original value
(use the original row variable to replace edited row at original row index)
However, the row I pass is an ng-model of row[column] (value of a column in a row). This means that I can't have the original value of the row, because it will update the original data as I change it.
To solve this, I created a $watch on the data. If the value for $scope.validated was true, update the variable the table uses as a data source. Otherwise, don't. $scope.validated is set to false when recordValue() is called, and true after looping through validateValue() and finding the value, or resetting it.
The problem is, the data still updates so I can't revert the value because the original row doesn't exist; when I loop through the data source, only the updated row exists. I can't find the original row to revert to.
I'm not sure why this is happening. Am I doing something incorrectly?
HTML:
<table>
<tr>
<th ng-repeat="columnName in columnArray">{{columnName}}</th>
</tr>
<tr ng-repeat="row in filteredData">
<td ng-repeat="columnName in columnArray">
<input ng-model="row[columnName]" ng-focus="recordValue(row,row[column])" ng-blur="validateValue(row,column,row[column])">
</td>
</tr>
</table>
Angular:
$scope.recordValue = function (row, val) {
$scope.originalRow = row;
$scope.originalValue = val;
$scope.validated = false;
};
$scope.validateValue = function (row, column, val) {
// row is the row of data the text field is in
// column is column name at row
// val is value at column in row(ng-model on $scope.data)
var indexOfOriginalRow;
for (var i = 0; i < $scope.data.length; i++) {
var currentRow = $scope.data[i];
if (currentRow == $scope.originalRow) {
console.log("found it!");
indexOfOriginalRow = i;
}
else if (currentRow[column] == val) {
$scope.validated = true;
// the value was found, keep it as is
return;
}
}
$scope.validated = true;
$scope.data[index][column] = $scope.originalValue;
};
$scope.$watch("data", function () {
if ($scope.validated) {
$scope.filteredData = doStuffWithData($scope.data);
}
}, true);
Each row is an object within an array (the datasource). When the values change, I update the PARENT data source, but not the data source the table uses. Unfortunately, it seems as though the table's data source (filteredData) updates anyways.
This prevents me from resetting the value of the text field because the value is updated in the data source, so it seems like it already exists in the table. It appears to be a logical error, but it is difficult to conceptualize the table and variable data structure, so I can't find the error.
UPDATE:
Even when I only store the value, and not reference of the original row, the bigger problem is that the data still updates. The original row is useless because the new row is already in the data source, meaning it will be found, viewed as a pre-existing value, and allow anything you type. Very frustrating.
Try using angular.copy() to store the value of row in $scope.originalRow without binding them.
I think you will always set $scope.validated = true and return because you are checking the updated row with the updated value. Try adding an else:
for (var i = 0; i < $scope.data.length; i++) {
var currentRow = $scope.data[i];
if (currentRow == $scope.originalRow) {
console.log("found it!");
indexOfOriginalRow = i;
} else if (currentRow[column] == val) { // ADDED ELSE
$scope.validated = true;
// the value was found, keep it as is
return;
}
}
Related
I am having one google sheet having more than 100 rows with column of "NAME, PLACE, PHONE". I want to change /correct the phone number on specific person Ex.John in the side bar (Form.html) and the correct place & phone number to be edit in that specific row of my google sheet "Phonelist". The code.gs given below which is not working. Could you lease rectify the same?
function sidebar() {
var html = HtmlService.createHtmlOutputFromFile("Form").setTitle('Phone Details');
SpreadsheetApp.getUi().sidebar(html);
}
function result(form) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ws = ss.getSheetByName("Phonelist");
var data = ws.getDataRange().getValues();
var name = form.name;
var place = form.place;
var phone = form.phone;
for (var i = 1; i < data.length; i++) {
if(data[i][1] == "John"){
var result = [name,place,phone];
ws.getRange(dat[i]).setValue(result);
}
}
}
It is difficult to understand what you exactly need. But there are some issues which are visible.
ws.getRange(data[i])is not valid. See docs. You need a row and a column at least, and in your case also the number of columns since your are inserting a range. Currently you only have a column. The solution is `
const startColumn = 1 // start at column A
const numberOfRows = 1 // update one row at a time
const numberOfColumns = result.length // this will be 3
ws.getRange(data[i], startColumn, numberOfRows, result.length)
.setValues(result) // setValues is correct, setValue is incorrect
The second issue is that you said that NAME is in the first column, but your test is testing against the second column. Array start at 0, i.e. the first item is actual accessed by [0]. therefore your test if(data[i][1] == "John") actually checks if the second column PLACE is equal to "John". To fix this, replace the [1] with [0], so:
if(data[i][0] == "John")
The third issue is handled in the first answer. You are using setValue() which is only to be used to set one cell. But since you are setting a number of cells at one time, you should use setValues() instead.
I have a set of data in a Google spreadsheet in two columns. One column is a list of article titles and the other is the ID of a hotel that is in that article. Call it list1.
Example data
I would like returned a new list with article titles in one column, and an array of the hotel IDs in that article in the other column. Call it list2.
Example data
There are thousands of lines that this needs to be done for, and so my hope was to use Google Apps Script to help perform this task. My original thinking was to
Create column 1 of list2 which has the unique article titles (no script here, just the G-sheets =unique() formula.
Iterate through the titles in list2, looking for a match in first column of the list1
If there is a match:
retrieve its corresponding value in column 2
push it to an empty array in column two of list2
move onto next row in list1
if no longer a match, loop back to step 2.
I've written the following code. I am currently getting a type error (TypeError: Cannot read property '0' of undefined (line 13, file "Code")), however, I wanted to ask whether this is even a valid approach to the problem?
function getHotelIds() {
var outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('list2');
var lastRow = outputSheet.getLastRow();
var data = outputSheet.getRange(2,1,lastRow,2).getValues();
var workingSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('list1');
var lastActiveRow = workingSheet.getLastRow();
var itemIDS = [];
for (var i=1; i<=data.length; i++) {
var currentArticle = data[i][0];
var lookupArticle = workingSheet[i][0];
if (currentArticle === lookupArticle) {
var tempValue = [workingSheet[i][1]];
itemIDS.push(tempValue);
}
}
}
Use a simple google sheets formula:
You can use a very simple formula to achieve your goal instead of using long and complicated scripts.
Use =unique(list1!A2:A) in cell A2 of list2 sheet to get the unique hotels.
and then use this formula to all the unique hotels by dragging it down in column B.
=JOIN(",",filter(list1!B:B,list1!A:A=A2))
You got the idea right, but the logic needed some tweaking. The "undefined" error is caused by the workingSheet[i][0]. WorkingSheet is a Sheet object, not an array of data. Also, is not necessary to get the data from list2 (output), it is rather the opposite. You have to get the data from the list1 (source) sheet instead, and iterate over it.
I added a new variable, oldHotel, which will be used to compare each line with the current hotel. If it's different, it means we have reached a different Hotel and the data should be written in list2.
function getHotelIds() {
var outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('list2');
var outLastRow = outputSheet.getLastRow();
var workingSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('list1');
var lastActiveRow = workingSheet.getLastRow();
var sourceValues = workingSheet.getRange("A2:B" + lastActiveRow).getValues();
var itemIDS = [];
var oldHotel = sourceValues[0][0]; //first hotel of the list
for (var i = 0; i < sourceValues.length; i++) {
if (sourceValues[i][0] == oldHotel) {
itemIDS.push(sourceValues[i][1]);
/*When we reach the end of the list, the oldHotel variable will never be different. So the next if condition is needed. Otherwise it wouldn't write down the last Hotel.
*/
if (i == sourceValues.length - 1) {
outputSheet.getRange(outLastRow + 1, 1, 1, 2).setValues([
[sourceValues[i][0], itemIDS.toString()]
]);
}
} else {
outputSheet.getRange(outLastRow + 1, 1, 1, 2).setValues([
[sourceValues[i - 1][0], itemIDS.toString()]
]);
oldHotel = sourceValues[i][0]; //new Hotel will be compared
outLastRow = outputSheet.getLastRow(); //lastrow has updated
itemIDS = []; //clears the array to include the next codes
}
}
}
I also converted the itemIDS array to a String each time, so it's written down in a single cell without issues.
Make sure each column of the Sheet is set to "Plain text" from Format > Number > Plain Text
References
getRange
setValues
toString()
I have working code, that delete row from sheet if column corresponds to one of the conditions. It based on arrays and because of it works much more faster than standard google sheet deleteRow function. I call it like this:
deleteRowsBV('SIZ',4,'0','')
where
deleteRowsBV(listName,ColNum,FirstSearch,SecondSearch)
What I want is a call function with more or less and equal signs, like this:
deleteRowsBV('SIZ',4,<='0',=='')
But in case of my main function, it doesn't work, when I specify a variable instead of a sign and a value.
Here is main function:
function deleteRowsBV(listName,ColNum,FirstSearch,SecondSearch) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(listName);
var DataLengBefore = sheet.getLastRow();
var DataColLeng = sheet.getLastColumn();
var data = sheet.getRange(1,1,DataLengBefore,DataColLeng).getValues();
for (var i = data.length-1; i >= 0; i--) {
if (data[i][ColNum] <= FirstSearch||data[i][ColNum] == SecondSearch) {
data.splice(i, 1);
}
}
sheet.getRange(10, 1, DataLengBefore,DataColLeng).clearContent();
sheet.getRange(10, 1,data.length,5).setValues(data);
}
Based from this related post, spreadsheet rows and columns are numbered starting at 1, for all methods in the SpreadsheetApp, while javascript arrays start numbering from 0. You need to adjust between those numeric bases when working with both. When deleting rows, the size of the spreadsheet dataRange will get smaller; you did take that into account, but because you're looping up to the maximum size, the code is complicated by this requirement. You can simplify things by looping down from the maximum.
You may refer with this thread. However, this only looks at the value from a single cell edit now and not the values in the whole sheet.
function onEdit(e) {
//Logger.log(JSON.stringify(e));
//{"source":{},"range":{"rowStart":1,"rowEnd":1,"columnEnd":1,"columnStart":1},"value":"1","user":{"email":"","nickname":""},"authMode":{}}
try {
var ss = e.source; // Just pull the spreadsheet object from the one already being passed to onEdit
var s = ss.getActiveSheet();
// Conditions are by sheet and a single cell in a certain column
if (s.getName() == 'Sheet1' && // change to your own
e.range.columnStart == 3 && e.range.columnEnd == 3 && // only look at edits happening in col C which is 3
e.range.rowStart == e.range.rowEnd ) { // only look at single row edits which will equal a single cell
checkCellValue(e);
}
} catch (error) { Logger.log(error); }
};
function checkCellValue(e) {
if ( !e.value || e.value == 0) { // Delete if value is zero or empty
e.source.getActiveSheet().deleteRow(e.range.rowStart);
}
}
http://plnkr.co/edit/r9hMZk?p=preview
I have a ui-grid where I have enabled multi selection. I want to be able to update the data whilst preserving the selection. If I just update the data then the selection is not preserved.
$scope.gridOpts.data = data2;
However I have defined a rowIdentity function, so that the id column uniquely identifies a row.
"rowIdentity" : function(row) {
return row.id;
}
Now if I select rows with id=Bob and id=Lorraine, then update the data, the rows are still selected. However the other fields in those rows are not updated.
How can I both preserve the selection and update all the data?
I think you need to keep track of you IDs yourself. So, you should remove the rowIdentifier, and instead add this piece at the beginning of your swapData function.
$scope.selIds = [];
for (var selRow of $scope.gridApi.selection.getSelectedRows()) {
$scope.selIds.push(selRow.id);
}
In addition to that, add an event handler on rowsRendered to re-select the previously selected rows
gridApi.core.on.rowsRendered($scope,function() {
for (var selId of $scope.selIds) {
for (var row of $scope.gridOpts.data) {
if (selId == row.id) {
$scope.gridApi.selection.selectRow(row);
}
}
}
});
You can put this in your registerApi callback.
I am using smart-table to display tabular data. One column of the table is editable. It is displayed and edited in an input field. Each row has a reset button which is supposed to reset edits made in the input field to the initial state.
How do I reset values of a single row (input field) when using smart-table?
Specifically what needs to be in the following function:
$scope.resetItem = function(index) {
// .....
};
I don't know about smart-table but why don't you try to get a copy of the data upfront and replace the related row with the original one?
var initialData = angular.copy(data);
$scope.data = data;
$scope.resetItem = function(index) {
$scope.data[index] = angular.copy(initialData[index]);
}