I have a model containing a list and I'm using Razor to display the list items onscreen in a standard html table. A user can edit some parts of these items and save them, or they can add extra lines to the table.
For example, a master record may have 2 child records, the user can edit the 2nd record, then add a 3rd, then save and all the data is bound to the model as expected and is saved back to the database. This all works fine
The issue occurs when the user tries to delete a child record which is not the last record in the list. I'm using the following code to delete a child record, where ctrl is a delete icon which appears in each row.
deleteExtraLine: function (ctrl) {
//get the table to which the ctrl belongs
var table = $(ctrl).closest("table");
//remove the row from the table
$(ctrl).closest('tr').remove();
}
The issue I'm having is all rows after the one which was removed from the table are no longer bound to the model on the postback when saving.
ie. there are 5 rows, row 3 is deleted, but 4 and 5 are also deleted as they are missing from the model.
Is there some way of removing a row from the table which isn't going to affect the model in this way?
Ok, so I worked it out not long after I posted this.
The numbering of the name attribute needed to be contiguous so as soon as there was a gap it lost everything subsequent to that. So I had to write a function to make sure the number on the name attribute was in sequence
updateRowIndex: function(rows, index) {
//index will start at the first row to be updated
$(rows).each(function() {
//get all the inputs and set the appropriate attribute values (this doesn't select the hidden ones but they don't matter anyway)
var inputs = $(this).find("input");
$(inputs).each(function () {
//change the value for id
var oldId = $(this).attr("id");
var newId = oldId.replace(/\[[0-9]?[0-9]\]/ig, "[" + index + "]"); //eg replace all occurrences of [2] with [1];
$(this).attr("id", newId);
//change the value for the name attribute
var oldName = $(this).attr("name");
var newName = oldName.replace(/\[[0-9]?[0-9]\]/ig, "[" + index + "]"); //eg replace all occurrences of [2] with [1];
$(this).attr("name", newName);
});
index++;
});
}
Related
I'm trying to get data from an input sheet and use it to update a database in another sheet without hardcoding column letters and row numbers. Here is a picture of my input sheet:
CONTEXT:
Users input data into E. Columns G and H contain the field names (G) and the values input by the user (H). I want to get the values from Column H and then update the right record in the database (a different sheet) based on the ContractorID field (which is in Col A of the database sheet) and the field names in Column G (which are the column headers in my database sheet).
MY ASK
I'd like code that dynamically gets the values in Column H (without referencing "H") based on the field names in Column G (without referencing "G").
I don't know Google App script well enough, but I think the logic would look something like this:
Iterating for each field name in Column G:
Set variable for the field name in column G
Get the column with "FIELDNAMES" in row 1
Get the row # of the field name variable in the FIELDNAMES column
Get the column with "FIELDVALUES" in row 1
Get the cell value in the row # in the FIELDVALUES column
Then I can use that value to populate the right field in the database sheet.
This script accomplishes what you're looking to do: it finds the data without explicitly calling out columns, then matches the contractor ID in the database sheet, finds the corresponding columns for each item and inserts the data.
EDIT
Added functions at the end to compare the input fields with the columns and use a toast notification to alert the user around what columns are missing.
A couple of notes:
Check the sheet names of course. this should work even if the database is in a separate worksheet. Just need to update the variable accordingly.
I borrowed from this script to merge the input and database arrays.
See it in action (I added a button to run the script) here
const ss = SpreadsheetApp.getActiveSpreadsheet()
const input = ss.getSheetByName("input")
const db = ss.getSheetByName("database")
function addRecord() {
// Find the columns you want, then get all the data in the columns to an array called inputData
var fieldRangeStart = input.createTextFinder("FIELDNAMES").findNext().getColumn()
var inputData = input.getRange(1,fieldRangeStart,input.getLastRow(),2).getDataRegion().getValues()
// In the database sheet, get the column headers and create an array called dataColumns that gives us those positions in the database sheet.
var headers = db.getRange(1,1,1,db.getLastColumn()).getValues().toString().split(",")
var dataColumns = []
for (var i=0; i<headers.length; i++){
dataColumns.push([headers[i],i+1])
}
// Merge the arrays, matching the fieldName values in each.
const data = inputData.map(x => dataColumns.filter(y => y[0] === x[0]).map(y => [x[0], y[1], x[1]])).flatMap(x => x)
// Filter out the contractorID into its own variable
const contractorID = data.filter(x => x[0] === "ContractorID").map(x => x[2])
console.log(contractorID)
//Now we have the contractorID to search for, and a data array that contains the field names, database column index, and input values for the data. Just need to find the row with the matching Contractor ID and then write the values to their respective columns.
//Start by getting all the values in the first column, except the header (start the range at Row 2).
var contractors = db.getRange(2,1,db.getLastRow(),1).getValues()
//Then find the contractor ID in the list. Add 2 to the row index because we need to account for the header (1 row) and the fact that the index is 0-based but rows start at 1 when doing A1 Notation.
var contractorRow = contractors.flat(1).indexOf(contractorID.toString())+2
//Finally, confirm the contractor is in the database, and then loop through the data array and write each item to its cell, using the column reference we already have and the new contractorRow reference we just acquired, or setting a new row if the contractor doesn't exist in the database. Issue toast notifications if input fields are missing from the db
if(contractorRow === 1){
var newRow = db.getLastRow()+1
for (var i=0; i<data.length; i++){
db.getRange(newRow,data[i][1]).setValue(data[i][2])
}
} else {
for (var i=0; i<data.length; i++){
db.getRange(contractorRow,data[i][1]).setValue(data[i][2])
}
}
// Gather just the FIELDNAMES data from the input sheet into a new variable called inputFields, ignoring the first row and flattening the result so it's not a 2-dimensional array.
var inputFields = input.getRange(2,fieldRangeStart,input.getLastRow(),1).getValues().flat(1)
// We already have the column headers in a variable called headers. Filter inputFields for NOT being included in the headers variable and also remove blank entries.
let missing = inputFields.filter(x => !headers.includes(x) && x != "")
// Now trigger the toast notification
SpreadsheetApp.getActiveSpreadsheet().toast('Oops! These fields are missing from the database: ' + missing, 'Missing Columns')
For example, I have this data,
I want to filter the data which is "USA" and copy it to another sheet but I have to creates a blank row if it met a condition. For example like this
Is it possible?
I have also tried
IF(AND(F2=FALSE, NOT(ISBLANK(F2))), "", INDEX(QUERY('Sheet 1'!A2:E, "Select A where A contains '"&"USA"&"'"),COUNTIF($F$2:F2,TRUE),1))
But it didn't work as I expected
Here is a formula version:
=ArrayFormula(
array_constrain(
sortn(
filter(
{if(A:A="USA",A:A,),if(A:A="USA",B:B,),A:A="USA",
if(A:A="USA",row(A:A),iferror(vlookup(row(A:A),if(A:A="USA",row(A:A),),1,true),0)+1)},
A:A<>""),
1000,2,4,1),
1000,3)
)
The reason for the long formula is mainly finding a way to get just one row to replace one or more rows that don't start with USA. The basis of the formula is to do a lookup for non-USA rows to get the row number of the most recent USA row. All of the non-USA rows in the same block then have the same row number and can be discarded (apart from the first) using Sortn.
I have added an extra non-USA row at the beginning to check that this edge case works and falls through to the Iferror clause.
Since you provided Google Apps Script as tag, I assume you are open to script answers? If so, then I'll provide this code below. Is this what you are looking for?
function copyUSA() {
var sheetSrc = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var sheetDst = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var rowSrc = sheetSrc.getLastRow();
var values = sheetSrc.getRange("A2:A" + rowSrc).getValues().flat();
values.forEach(function (value, index){
var rowDst = sheetDst.getLastRow() + 1;
if(value == "USA"){
// If USA, copy then replace third column with TRUE
var row = index + 2;
sheetSrc.getRange(row + ":" + row).copyTo(sheetDst.getRange(rowDst + ":" + rowDst));
sheetDst.getRange("C" + rowDst).setValue("TRUE");
}
else{
// If not, set third column to FALSE
sheetDst.getRange("C" + rowDst).setValue("FALSE");
}
});
}
Sheet1:
Sheet2:
My assumptions were based from your formula. These are:
I assumed that you add blank when it is not USA. Thus having 2 blanks on the output
If USA, 3rd column is TRUE, else, FALSE with blank data
Based on your formula, it seems your data starts at A2, thus I adjust the code too.
You are also getting the 4th and 5th column on your formula but since you didn't show it in your post, I can't assume any values for it.
If the answer isn't the one you are looking for, I apologize.
I am trying to debug a specific row in a Google Spreadsheet and use the row[0] in a filtered table but I want to see the actual row in the sheet.
What I am getting is the email address, which is correct, but what do I have to do to see the actual row number? Is there a way to expose to actual row number my information?
I want to get access might skip a couple of rows then want to work on that row then skip a couple more rows and work on another row.
Is there a way to expose to row like row 6 and row 9 lets say, I have looked in the log and when I use row[0] I get the actual data, but I want instead row 6 or row 9?
I am using this:
Logger.log('ROW row is :' + row[0]); // The email is what i do not want !
Please be more specific with the question and provide some example data. Also, use comma and periods and new-lines, as they help to group thoughts.
To my understanding, you want to iterate (go) through each row in a particular Spreadsheet. You want to filter out certain row, but it is not stated what the criteria is, therefore I will make a assumption.
Assumptions: You want to iterate through certain rows.
// enter here the rows you want to filter in, ie use
const useTheseRowsOnly = [1, 3, 4];
// lets get all the data in the active Sheet (or whichever Sheet)
const sheetDataAsArr = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
// now lets go through each row
sheetDataAsArr.forEach( (rowArr, index) => {
const rowNumber = index + 1; // index starts at 0, but rows start at 1
// skip unwanted rows
if( useTheseRowsOnly.indexOf(rowNumber) == -1 ){
return
}
// here we work with the rows like this
const columnAValue = rowArr[0];
const columnBValue = rowArr[1] ;
Logger.log("Row: " + rowNumber + " has in Column A: " + columnAValue)
})
I have a Google form where some of the fields are "static" content, and some are populated by app script from a Google sheet which will be run on a daily basis. I nearly have it how I want it, but not quite.
I have a couple of dropdown lists that need to contain choices that navigate to different sections of the form, e.g:
Phill / Beacon Hill - Go to section called "Beacon Hill
Jane / Harbord - Go to section called "Harbord"
Fred / Beacon Hill - Go to section called "Beacon Hill"
etc...
What's happening is that instead of appending choices to the list, I'm overwriting which means I only end up with ONE choice in my dropdown, which is the last one added, i.e. Fred in the example above.
I've looked at lots of examples on the web but can't get this to work. I feel I'm very close but not quite there yet. Here's my code with a couple of lines that I believe are the problem. Can someone please tell me where I'm going wrong.
function populatePlayersClubsListV2() {
// ------------------------------------------------------------------------------------------------
// This gets each male player and the junior club they've been assigned to from the
// Junior_Clubs_Training_Sessions s/sheet (which is this s/sheet). It creates a list that the player
// chooses their name from and sets up a branch to the appropriate Club training sessions section
// in the form depending on which club they're assigned to. .
// ------------------------------------------------------------------------------------------------
// Open the "Find Junior Club Training Sessions" form
var form = FormApp.openById("1fo33ncgJY.................iRnMsERRTen8WjTH_xrx0");
// Identify the sheet in this spreadsheet holding the data needed to populate the drop-down. Here
// it's the "PlayersClubs" tab which says which club each player is assigned to.
var ss = SpreadsheetApp.getActive();
var playersClubsSheet = ss.getSheetByName("PlayersClubs");
// Grab the values from the rows & columns of the sheet - use 2 to skip header row. Use getMaxRows
// and getMaxColumns to avoid hard-coding the number of rows and columns.
var playersClubsSsValues = playersClubsSheet.getRange(2, 1, playersClubsSheet.getMaxRows() - 1, playersClubsSheet.getMaxColumns() - 1).getValues();
// We need to loop thro twice - once to populate the male players, and again for the female players.
// Males/females populate different fields and we hold the data-item-IDs of those fields in an array.
var formFieldsArray = [
["Male", 1397567108],
["Female", 1441402031]
];
for(var h = 0; h < formFieldsArray.length; h++) {
// Open the form field you want to populate - it must be a dropdown or multiple choice.
// Right-click field, inspect and look for data-item-ID followed by a number.
var playersClubsFormList = form.getItemById(formFieldsArray[h][1]).asListItem();
// Define array to hold values coming from the s/sheet and used to populate form fields.
var playersClubsArray = [];
var sectionMalePlayers = form.getItemById(309334479).asPageBreakItem();
var sectionFemalePlayers = form.getItemById(856495273).asPageBreakItem();
// Create the array of players and their clubs ignoring empty cells. Check if the s/sheet row
// matches male/female against formFieldsArray[h][0].
for(var i = 0, j = 0; i < playersClubsSsValues.length; i++) {
if(playersClubsSsValues[i][0] != "" && playersClubsSsValues[i][1] == formFieldsArray[h][0]) {
playersClubsArray[j] = playersClubsSsValues[i][0] + " - " + playersClubsSsValues[i][2];
if (formFieldsArray[h][0] = "Male") {
// ** THIS IS THE LINE THAT OVERWRITES BUT I NEED IT TO APPEND *** //
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
}
else {
// ** THIS IS THE LINE THAT OVERWRITES BUT I NEED IT TO APPEND *** //
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionFemalePlayers)]);
}
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
j = j + 1;
} // end if
} // end for loop
} // end for loop for Males/Females
}
Issue:
When setChoices is used, all choices that were previously stored in the item get removed. Only the ones that are specified when using setChoices get added to the item.
Right now, you are only specifying one choice:
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
Solution:
You have to specify all choices you want the item to have. So in this case you would have to do the following:
Retrieve all choices that were previously stored via getChoices. This will retrieve an array with all current choices in the item.
Use Array.prototype.push() to add the choice you want to add to the list of choices.
When using setChoices, the array retrieved in step 1 and modified in step 2 should be provided as the argument.
Code sample:
Change this:
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers)]);
For this:
var choices = playersClubsFromList.getChoices();
choices.push(playersClubsFormList.createChoice(playersClubsArray[j], sectionMalePlayers));
playersClubsFormList.setChoices(choices);
Note:
The same change would have to be made for all the lines where this problem is happening, like this one:
playersClubsFormList.setChoices([playersClubsFormList.createChoice(playersClubsArray[j], sectionFemalePlayers)]);
Reference:
getChoices()
setChoices(choices)
I've built a spreadsheet with multiple tabs.
My 'Source' Tab 1 contains columns 'Id' and 'Name'
My 'Target' Tab 2 contains 'Id'
I'm attempting to iterate through the rows in my Target Tab and read the value of the Id and then search my Source Tab for the Id. When I find the Id, I want to grab the value in the 'Name' field and add it to a cell on the same line of my Target Tab's Id.
I'm having a hard time working through the logic of iterating through one array and ensuring I find the value in either the Target tab or an array of the Target Tabs contents. In the Target tab, there may be more than one occurrence of an Id, and I would need to update them all.
Any suggestions would be welcome!
here is a suggestion :
/* let us say source array with name(columnA) & ID(columnB) is array 'source'
and target array with only IDs is array 'target', you get these with something like*/
var source = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getDataRange().getValues();
// and
var target = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1].getDataRange().getValues();// if other columns, change index values in the arrays : 0=A, 1=B ...
// then let's create a 3 rd array that will be the new target with ID + names, and call it 'newtarget'
var newtarget=new Array()
// the iteration could be like this :
for(i=0;i<target.length;++i){ // don't miss any ID
for(j=0;j<source.length;++j){ // iterate through source to find the name
if(target[i][0].toString().match(source[j][1].toString()) == source[j][1].toString()){
var newtargetrow=[source[j][0],target[i][0]] // if match found, store it with name (idx0) and ID (idx 1)
}else{
var newtargetrow=['no name found',target[i][0]] // if no match, show it in name column
}
newtarget.push(newtargetrow);// store result in new array with 2 columns
} //loop source
} // loop target
/* now you have a newtarget array that can directly overwrite the old target
using setValues() */
var sh = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];// assuming the target sheet is sheet nr2
sh.getRange(1,1,newtarget.length,newtarget[0].length).setValues(newtarget);
//
note I didn't test this code but it should give you a starting point. I made comparison on strings with a .match but you could use other comparisons (direct equality between array elements)... you could also remove spaces in IDs if there is a risk of undesired spaces in the sheet data... I don't know your data so it's up to you.