FormApp.GridItem.setRows() 在链接的 Sheet 中生成重复的列。如何预防?
FormApp.GridItem.setRows() producing duplicated columns in the linked Sheet. How to prevent it?
当 setRows()
运行 在现有 GridItem
上时,生成的 Form GridItem
元素没问题,但 [=72= 中的列]ed Sheet 在下一栏中转载。新列是重复的,但是有一个隐藏的 属性 表明它们属于 Form(所以我们不能删除新的列)。这是什么属性?旧列不再属于 Form。旧列可能具有现有值或以前的表单响应。
如何预防?
FormApp
应正确处理 sheet 范围,方法是查找现有列并仅添加具有新字符串数组的真正新列。
Google 表格 UI 是如何处理的:
当我们使用Forms UI时,我们可以很容易地在GridItem中添加新的行,并且link Sheet会更新而不会出现重复的列。
这是要测试的表单,请在运行输入代码之前创建一个新的linked response-Sheet:
Copy Sample Form with GAS code
这是 GAS 代码:
function reGenerateRows1() {
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
item.asGridItem()
// repopulate from existing rows or from new string array
.setRows(item.asGridItem().getRows())
// .setRows(['A','B','C','D','E'])
// .setRows(['A','B','C','D','E','F'])
// result: Duplicates in destination sheet
break;
}
}
}
function reGenerateRows2() {
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
let grid = item.asGridItem()
let rows = grid.getRows()
let columns = grid.getColumns()
let title = grid.getTitle()
form.deleteItem(item)
// recreate GridItem and repopulate
form.addGridItem().setTitle(title).setRows(rows).setColumns(columns)
// result: Duplicates in destination sheet
break;
}
}
}
function reGenerateRows3() {
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
let grid = item.asGridItem()
let rows = grid.getRows()
let columns = grid.getColumns()
let id = item.getId()
// repopulate from id
form.getItemById(id).asGridItem().setRows(rows).setColumns(columns)
// result: Duplicates in destination sheet
break;
}
}
}
编辑了截图(我是新来的。无法嵌入图片。只有 imgur-linked 图片)
这是.setRows(['A','B','C','D','E','F'])
的结果
原始行带有 'A','B','C'
Screenshot of resulting .setRows()
这是我希望 .setRows()
做的事情:
Screenshot of expected result
在带有链接的表格中编辑网格 sheet。
手动编辑带链接sheet表格中的网格的过程如下:
- 您单击项目的输入框。
- 您更新文本。
- sheet会自动替换相应栏目的标题。
但是,使用 Apps 脚本。唯一可用于更改标题的过程是使用方法 .setRows
或 .setColumns
。这与 UI 的行为非常不同。本质上,它 替换 行,因此,链接的 sheet 将生成新列以保留以前的答案。
它会创建新列,因为它无法知道哪个新标题对应于旧标题。
例如,如果您有一些行:
- 一个
- B
- C
当您从 Apps 脚本获取此内容时,您必须先 getRows()
const rows = item.getRows()
这是作为 字符串列表返回的 :
['A', 'B', 'C']
所以在这里您已经丢失了一些重要信息。如果行返回为自己的 object 类型,带有内部 ID 号或其他内容,这样您就可以在该行上调用假设的 setTitle
,那么这会很好,但是,这不是案.
如果您随后将行更改为:
const newRows = ['A', 'C', 'B']
然后将其传递给如下形式:
item.setRows(newRows)
它无法知道哪个项目对应于另一个项目,所以它只是重新生成整个项目。
同样,要使它的行为与在 UI 中的行为相同,行和列需要以不同方式实现。也许不如自己class。也许 FormGridColumns
和 FormGridRows
或类似的东西。理想情况下,这将有自己的方法,例如:
const rows = item.getRows()
rows[0].setTitle('B')
这是一个可以实施的示例,但由于尚未实施,您唯一的办法就是使用此模板提交功能请求:
File an Apps Script Feature Request
可能的解决方法
可以编写自己的函数来更新和合并 sheet 中的列,但它很快就会变得复杂。它还需要 Utilities.sleep
和 SpreadsheetApp.flush()
的剂量,但在我的测试中似乎是可靠的。
例如,我编写了这个函数来更新和合并行,但前提是新行的元素数与现有行的元素数相同。
/**
* Updates a gridItem's column names and merges previous answers
* into the new columns that are created automatically.
* @param {GridItem}
* @param {string}
* @param {array} - must have same number of rows as gridItem currently has
*/
function updateRows(gridItem, ssID, newRows){
// Getting basic data from sheet
const file = SpreadsheetApp.openById(ssID);
const sheet = file.getSheetByName("Form Responses 1");
const dataRange = sheet.getDataRange();
const values = dataRange.getValues();
const headers = values.shift()
// Getting existing title and rows from form
const itemTitle = gridItem.getTitle()
const oldRows = gridItem.getRows()
// Check to make sure newRows is the same length as the current rows
if (oldRows.length !== newRows.length) {
throw "new rows and old rows must be same length"
}
// Finding which column the old rows are in the spreadsheet
const oldRowHeaderIndices = getIndicesOfRows(itemTitle, headers, oldRows)
// Setting the new rows which will autogenerate new columns in the spreadsheet
gridItem.setRows(newRows)
// Need to sleep and flush so that the changes have time to propagate
console.log("sleeping")
Utilities.sleep(6000) // I found anything less than 6 seconds was unreliable
console.log("woken up")
SpreadsheetApp.flush()
// Refreshing the data from the sheet
const updatedDataRange = sheet.getDataRange();
const updatedValues = updatedDataRange.getValues();
const updatedHeaders = updatedValues.shift()
// Finding the columns that the new rows are in
const newRowHeaderIndices = getIndicesOfRows(itemTitle, updatedHeaders, newRows)
// Copying all the data from old columns to new columns
for (let i = 0; i != oldRows.length; i++) {
const sourceRange = sheet.getRange(
2,
oldRowHeaderIndices[i] + 1,
updatedValues.length,
1
)
const destinationRange = sheet.getRange(
2,
newRowHeaderIndices[i] + 1,
updatedValues.length,
1
)
destinationRange.setValues(sourceRange.getValues())
}
// Deleting old columns (assuming that they are all adjacent to each other)
sheet.deleteColumns(oldRowHeaderIndices[0] + 1, oldRowHeaderIndices.length)
}
function getIndicesOfRows(itemTitle, headers, rows){
return rows.map((rowTitle, i) => {
return headers.findIndex(header => {
return header === `${itemTitle} [${rowTitle}]`
})
})
}
用法示例
起点:
然后你可以像这样调用函数(改编自你的代码):
function test() {
// Get your form as usual
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
const originalRows = item.asGridItem().getRows()
// ['A', 'B', 'C', 'D', 'E', 'F']
// Here just appending "NEW" to the name to create a new row
const newRows = originalRows.map(row => "NEW " + row)
// ['NEW A', 'NEW B', 'NEW C', 'NEW D', 'NEW E', 'NEW F']
const ssID = form.getDestinationId()
// Call custom function
updateRows(item.asGridItem(), ssID, newRows)
break;
}
}
}
该脚本需要大约 10 秒才能 运行 并将导致:
结论
建议的功能在使用上可能非常有限。不过,如果有足够的时间,它可能会适应大多数用例。
显然,如果 Google 可以将网格行和网格列实现为 classes,那将是最好的,但这将需要一个功能请求并且 Google 接受它,如果他们觉得很多人都想要它。
参考文献
当 setRows()
运行 在现有 GridItem
上时,生成的 Form GridItem
元素没问题,但 [=72= 中的列]ed Sheet 在下一栏中转载。新列是重复的,但是有一个隐藏的 属性 表明它们属于 Form(所以我们不能删除新的列)。这是什么属性?旧列不再属于 Form。旧列可能具有现有值或以前的表单响应。
如何预防?
FormApp
应正确处理 sheet 范围,方法是查找现有列并仅添加具有新字符串数组的真正新列。
Google 表格 UI 是如何处理的: 当我们使用Forms UI时,我们可以很容易地在GridItem中添加新的行,并且link Sheet会更新而不会出现重复的列。
这是要测试的表单,请在运行输入代码之前创建一个新的linked response-Sheet:
Copy Sample Form with GAS code
这是 GAS 代码:
function reGenerateRows1() {
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
item.asGridItem()
// repopulate from existing rows or from new string array
.setRows(item.asGridItem().getRows())
// .setRows(['A','B','C','D','E'])
// .setRows(['A','B','C','D','E','F'])
// result: Duplicates in destination sheet
break;
}
}
}
function reGenerateRows2() {
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
let grid = item.asGridItem()
let rows = grid.getRows()
let columns = grid.getColumns()
let title = grid.getTitle()
form.deleteItem(item)
// recreate GridItem and repopulate
form.addGridItem().setTitle(title).setRows(rows).setColumns(columns)
// result: Duplicates in destination sheet
break;
}
}
}
function reGenerateRows3() {
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
let grid = item.asGridItem()
let rows = grid.getRows()
let columns = grid.getColumns()
let id = item.getId()
// repopulate from id
form.getItemById(id).asGridItem().setRows(rows).setColumns(columns)
// result: Duplicates in destination sheet
break;
}
}
}
编辑了截图(我是新来的。无法嵌入图片。只有 imgur-linked 图片)
这是.setRows(['A','B','C','D','E','F'])
的结果
原始行带有 'A','B','C'
Screenshot of resulting .setRows()
这是我希望 .setRows()
做的事情:
Screenshot of expected result
在带有链接的表格中编辑网格 sheet。
手动编辑带链接sheet表格中的网格的过程如下:
- 您单击项目的输入框。
- 您更新文本。
- sheet会自动替换相应栏目的标题。
但是,使用 Apps 脚本。唯一可用于更改标题的过程是使用方法 .setRows
或 .setColumns
。这与 UI 的行为非常不同。本质上,它 替换 行,因此,链接的 sheet 将生成新列以保留以前的答案。
它会创建新列,因为它无法知道哪个新标题对应于旧标题。
例如,如果您有一些行:
- 一个
- B
- C
当您从 Apps 脚本获取此内容时,您必须先 getRows()
const rows = item.getRows()
这是作为 字符串列表返回的 :
['A', 'B', 'C']
所以在这里您已经丢失了一些重要信息。如果行返回为自己的 object 类型,带有内部 ID 号或其他内容,这样您就可以在该行上调用假设的 setTitle
,那么这会很好,但是,这不是案.
如果您随后将行更改为:
const newRows = ['A', 'C', 'B']
然后将其传递给如下形式:
item.setRows(newRows)
它无法知道哪个项目对应于另一个项目,所以它只是重新生成整个项目。
同样,要使它的行为与在 UI 中的行为相同,行和列需要以不同方式实现。也许不如自己class。也许 FormGridColumns
和 FormGridRows
或类似的东西。理想情况下,这将有自己的方法,例如:
const rows = item.getRows()
rows[0].setTitle('B')
这是一个可以实施的示例,但由于尚未实施,您唯一的办法就是使用此模板提交功能请求:
File an Apps Script Feature Request
可能的解决方法
可以编写自己的函数来更新和合并 sheet 中的列,但它很快就会变得复杂。它还需要 Utilities.sleep
和 SpreadsheetApp.flush()
的剂量,但在我的测试中似乎是可靠的。
例如,我编写了这个函数来更新和合并行,但前提是新行的元素数与现有行的元素数相同。
/**
* Updates a gridItem's column names and merges previous answers
* into the new columns that are created automatically.
* @param {GridItem}
* @param {string}
* @param {array} - must have same number of rows as gridItem currently has
*/
function updateRows(gridItem, ssID, newRows){
// Getting basic data from sheet
const file = SpreadsheetApp.openById(ssID);
const sheet = file.getSheetByName("Form Responses 1");
const dataRange = sheet.getDataRange();
const values = dataRange.getValues();
const headers = values.shift()
// Getting existing title and rows from form
const itemTitle = gridItem.getTitle()
const oldRows = gridItem.getRows()
// Check to make sure newRows is the same length as the current rows
if (oldRows.length !== newRows.length) {
throw "new rows and old rows must be same length"
}
// Finding which column the old rows are in the spreadsheet
const oldRowHeaderIndices = getIndicesOfRows(itemTitle, headers, oldRows)
// Setting the new rows which will autogenerate new columns in the spreadsheet
gridItem.setRows(newRows)
// Need to sleep and flush so that the changes have time to propagate
console.log("sleeping")
Utilities.sleep(6000) // I found anything less than 6 seconds was unreliable
console.log("woken up")
SpreadsheetApp.flush()
// Refreshing the data from the sheet
const updatedDataRange = sheet.getDataRange();
const updatedValues = updatedDataRange.getValues();
const updatedHeaders = updatedValues.shift()
// Finding the columns that the new rows are in
const newRowHeaderIndices = getIndicesOfRows(itemTitle, updatedHeaders, newRows)
// Copying all the data from old columns to new columns
for (let i = 0; i != oldRows.length; i++) {
const sourceRange = sheet.getRange(
2,
oldRowHeaderIndices[i] + 1,
updatedValues.length,
1
)
const destinationRange = sheet.getRange(
2,
newRowHeaderIndices[i] + 1,
updatedValues.length,
1
)
destinationRange.setValues(sourceRange.getValues())
}
// Deleting old columns (assuming that they are all adjacent to each other)
sheet.deleteColumns(oldRowHeaderIndices[0] + 1, oldRowHeaderIndices.length)
}
function getIndicesOfRows(itemTitle, headers, rows){
return rows.map((rowTitle, i) => {
return headers.findIndex(header => {
return header === `${itemTitle} [${rowTitle}]`
})
})
}
用法示例
起点:
然后你可以像这样调用函数(改编自你的代码):
function test() {
// Get your form as usual
const form = FormApp.getActiveForm()
for (const item of form.getItems(FormApp.ItemType.GRID)) {
if (item.getTitle() === 'First Question') {
const originalRows = item.asGridItem().getRows()
// ['A', 'B', 'C', 'D', 'E', 'F']
// Here just appending "NEW" to the name to create a new row
const newRows = originalRows.map(row => "NEW " + row)
// ['NEW A', 'NEW B', 'NEW C', 'NEW D', 'NEW E', 'NEW F']
const ssID = form.getDestinationId()
// Call custom function
updateRows(item.asGridItem(), ssID, newRows)
break;
}
}
}
该脚本需要大约 10 秒才能 运行 并将导致:
结论
建议的功能在使用上可能非常有限。不过,如果有足够的时间,它可能会适应大多数用例。
显然,如果 Google 可以将网格行和网格列实现为 classes,那将是最好的,但这将需要一个功能请求并且 Google 接受它,如果他们觉得很多人都想要它。