Google Apps Script 寄出客製化的表單並搜集分散在 Google Sheet 中的回應?(二)大幅度客製你的 Google Form (2021 鐵人賽 D12)

目標

很多時候我們會需要搜集些不同的資料。像是 Marketing 在做大規模但針對不同組織的調查問卷。如果只是三份、五份的問卷要做客製化、統整算還好;但如果是一百份、甚至上千份時,總不能一個個複製了吧。此時就會遇到個問題——

  1. 要如何複製客製化 Google 表單?
  2. 要如何集中很多表單中的資料(回應)?

因為篇幅關係,這邊會拆成三篇來寫,第一篇與第二篇回應 Q1;第三篇回應 Q2。今天這篇是針對 Q1 的第二篇,昨天我們講了怎麼樣複製與簡單客製 Google 表單。而今天來到了第十二天,我們來到目前最進階的操作,用 GAS 完成超·客製表單。一樣先講結論,如果你很急著用,可以直接使用這份 Add-On: Form Publisher,功能非常強大。自己寫的好處是,如果你一天突然要做些高度客製化,那此篇會有幫助。這篇的定位比較像是字典、工具,在你需要用的時候可以來參照。那就讓我們開始吧!


Q1. 我要如何複製客製化 Google 表單?

複製表單有兩種方式。一種是比較簡單的「複製範本」,簡單來說就是針對一個表單複製,然後再改期中的元素。另一種是「從零製作」,這種就比較複雜,因為會是透過 GAS 完整製作表單,會需要比較熟悉 GAS。我們昨天講完方法一,今天則著重在方法二。

方式二:用 GAS 製造表單

如果是比較複雜的表單與設定,舉例來說,有些人需要第一題,有些人第五題要用填空,那要怎麼處理?這個時候可以用 GAS 來製作每份表單。雖然這樣客製化程度很高,但相對製作時間會比前一次更久。果然是充滿取捨的人生。

也給大家看一下這次預計生成的參數們,左邊綠色是我們輸入的參數,右邊橘色是我們預計輸出的參數。

補充:校稿時有朋友問說那個「勾勾」怎麼做,多錄一支影片給大家看

Step 1 開啟 Google Sheet,並串起 GAS

設定步驟跟之前一一樣,從 Google Sheet 中進入 GAS。

Step 2 用 GAS 中生成我們表單

昨天我們有示範如何操作既存的表單,今天我們來看看怎麼生出新的表單。方式極其簡單,就是用 FormApp.create() ,並在括號中輸入表單名稱即可。

function createNewForm(){
  let new_form = FormApp.create('New Form');
}

但,這樣創造有個小問題,就是創造的位置會在根目錄,也就是一開始打開 Google Drive 的位置。那要怎麼移動?目前是需要比較陽春的透過寫 DriveApp 的 moveTo(folder) 才行。也就是以下示範的程式碼,改編自 JLMosher 的回應

function moveFile(fileId, destinationFolderId) {
  let destinationFolder = DriveApp.getFolderById(destinationFolderId);
  DriveApp.getFileById(fileId).moveTo(destinationFolder);
}

所以原本的表單,使用上就變成了——

function createNewForm(){
  let new_form = FormApp.create('New Form');
  let new_form_id = new_form.getId()
  let destinationFolderId = "your_folder_id_here"
  moveFile(new_form_id, destinationFolderId)
}

那這邊是簡易生成一張表單的方式,接著我們要對每一張表單進行細節的操作,開始囉!

Step 3 用 FormApp 來生出表單的問題

我們要如何在 GAS 內生出問題們?我做了一個簡單方法對照表。

那實際上怎麼用呢,這邊先給大家看完整的程式碼,接著一個個說明。我們以一份要約 onsite interview 的表單為例。

function addNameText(form){
    let text_name = form.addTextItem().setTitle('Name').setRequired(true);
    return text_name
}

function addLunchList(form){
    let list_item = form.addListItem();
    list_item.setTitle('Option for the Lunch')
             .setChoices([
                 list_item.createChoice('Meat'),
                 list_item.createChoice('Vegetarian')
              ]);
    return list_item
}

function addTrafficChoices(form){
     let multipleChoice_item = form.addMultipleChoiceItem();
      multipleChoice_item.setTitle('How do you come to our office?')
                         .setChoices([
                           multipleChoice_item.createChoice('Train'),
                           multipleChoice_item.createChoice('Bus'),
                           multipleChoice_item.createChoice('Drive'),
                           multipleChoice_item.createChoice('Walk')
                         ])
                         .showOtherOption(true);
     return multipleChoice_item
}

function addInterestGridWithValidation(form){
      let grid_item = form.addGridItem();
      grid_item.setTitle('Rate your interests')
               .setRows(['SDE', 'Test Engineer', 'Project Manager'])
               .setColumns([5, 4, 3,2,1])
               .setHelpText("It won't affect your scores in interviewing.");

      let gridValidation = FormApp.createGridValidation()
                                  .setHelpText("Select one item per column.")
                                  .requireLimitOneResponsePerColumn()
                                  .build();

      grid_item.setValidation(gridValidation);
      return grid_item
}

function addSkillCheckbox(form){
      let checkbox_item = form.addCheckboxItem();
      checkbox_item.setTitle('What are your technical skillsets')
                   .setChoices([
                        checkbox_item.createChoice('Python'),
                        checkbox_item.createChoice('JavaScript'),
                        checkbox_item.createChoice('HTML5'),
                        checkbox_item.createChoice('CSS3')
                  ])
                  .showOtherOption(true);
      return checkbox_item
}

function addAvailableDateTime(form){
      let date_time_item = form.addDateTimeItem();
      date_time_item.setTitle('When is your availability?');
      return date_time_item
}

function addSuggestionParagraphText(form){
      let paragraph_text_item = form.addParagraphTextItem();
      paragraph_text_item.setTitle('Any question or suggestion?');
      return paragraph_text_item
}

function addRateScale(form){
      let scale_item = form.addScaleItem();
      scale_item.setTitle('Rate this form')
                .setBounds(1, 5);
      return scale_item
}

function writeForm(curr_form){
      curr_form.setTitle('D12 Form').setDescription('Description of form \nTest for new line');

       let text_name = addNameText(curr_form);
       let list_item = addLunchList(curr_form);
       let multipleChoice_item = addTrafficChoices(curr_form);
       let grid_item = addInterestGridWithValidation(curr_form);
       let checkbox_item = addSkillCheckbox(curr_form);
       let date_time_item = addAvailableDateTime(curr_form);
       let paragraph_text_item = addSuggestionParagraphText(curr_form);
       let scale_item = addRateScale(curr_form);
            
       return curr_form
}

好,那我們一個個來講。順序依照最上面圖的順序。

設定表單標題與敘述(Title / Description)

這邊應該算好理解,針對 form 本身用 setTitle() 設定標題,也透過 setDescription()設定標題下的敘述。

form.setTitle('D12 Form').setDescription('Description of form \n Test for new line');

眼尖的朋友應該有看到我有加入一個 '\n' 在 Description,這是「換行符號」,也就是在敘述段落如 description 時,可以透過加上這符號進行換行。換句話說,如果輸入

// 會出現連續的 123
.setDescription('123')
123

// 會出現分成三行的 1, 2, 3 
.setDescription('1\n2\n3')
1
2
3

補充的是,這邊有三個功能是可以加上的,分別是收到回應的確認訊息、是否允許編輯與是否接受重複回應。

form.setConfirmationMessage('Thanks for responding!')
    .setAllowResponseEdits(true)
    .setAcceptingResponses(false);

對應的回應關係圖如下——

這張表是用中文版 Google Form 在新增問題時的順序,也是我們接下來列點介紹的順序。

設定簡答與段論問題(Text / Paragraph Text)

這邊很簡單地用了 setTextItem() 作為了設定問題的方式,並且針對這個新增的問題用 setTitle() 來給予敘述,並且用 setRequired 來設定必填。

let text_name = form.addTextItem().setTitle('Name').setRequired(true);

而對應的段落也是用 .addParagraphTextItem() 即可。

let paragraph_text_item = form.addParagraphTextItem();
paragraph_text_item.setTitle('Any question or suggestion?');

但如果有時候我們想要加上一些限制,像是至少輸入 100 字,那要怎麼做?這時就要用到 createParagraphTextValidation 來執行。範例程式碼如下——

let paragraphtextValidation = FormApp.createParagraphTextValidation()
                                     .setHelpText(“Answer must be more than 100 characters.”)
                                     .requireTextLengthGreatherThan(100);
paragraph_text_item.setValidation(paragraphtextValidation);

而其總共有六種模式可以設定,分別是…

  • 設定回應需要含有以下 Pattern requireTextContainsPattern(pattern):通常是開頭、結尾需要是特定格式。 e.g. email 的信箱位址。
  • 設定回應不得含有以下 Pattern requireTextDoesNotContainPattern(pattern)
  • 設定回應需要吻合以下 Pattern requireTextMatchesPattern(pattern):通常是字句中需要含有特定關鍵字、模式。 e.g. 含有三碼郵遞區號數字後接上文字
  • 設定回應不得吻合以下 Pattern requireTextDoesNotMatchPattern(pattern)
  • 設定回應長度大於或等於 數字requireTextLengthGreaterThanOrEqualTo(number):e.g. 地址文字中不得含有數字(需用國字中文之類)
  • 設定回應長度小於或等於 數字 requireTextLengthLessThanOrEqualTo(number)

上面的功能中所提到的 Pattern,其實就是 Regex(Regular Expression 正規表示式)。這邊給一個使用的範例。

let paragraphtextValidation = FormApp.createParagraphTextValidation()
                                     .requireTextContainsPattern('[a-zA-Z]')                                     
paragraph_text_item.setValidation(paragraphtextValidation);

上面所寫的這個範例就是,檢查輸入的內容只能是英文大寫 [A-Z] 或小寫 [a-z] 。更詳細 Regex 可以到 regexone 和 learn regex 學,很詳盡。

設定選擇題(Multiple Choice)

下面這段程式碼,主要是先用 addMultipleChoiceItem() 創造一個 object,並接著用 setChoices() 和 createChoice)_ 來創造選項們,最後設定 showOtherOption() 來讓填答人可以自行輸入「其他」。

let multipleChoice_item = form.addMultipleChoiceItem();
  multipleChoice_item.setTitle('How do you come to our office?')
                     .setChoices([
                       multipleChoice_item.createChoice('Train'),
                       multipleChoice_item.createChoice('Bus'),
                       multipleChoice_item.createChoice('Drive'),
                       multipleChoice_item.createChoice('Walk')
                     ])
                     .showOtherOption(true);

設定核取方塊(Checkbox)

方式跟設定選擇題幾乎一樣,只差在是用 addCheckboxItem() 來建造。

let checkbox_item = form.addCheckboxItem();
checkbox_item.setTitle('What are your technical skillsets')
             .setChoices([
                    checkbox_item.createChoice('Python'),
                    checkbox_item.createChoice('JavaScript'),
                    checkbox_item.createChoice('HTML5'),
                    checkbox_item.createChoice('CSS3')
             ])
             .showOtherOption(true);

額外有 CheckboxValidationBuilder 可以建造驗證程序,方式包括

  • 至少填入幾個選項 requireSelectAtLeast()
  • 至多填入幾個選項 requireSelectAtMost()
  • 剛好填入幾個選項 requireSelectExactly()

提供官方範例給大家參考~

var checkBoxValidation = FormApp.createCheckboxValidation()
                                .setHelpText(“Select two condiments.”)
                                .requireSelectExactly(2)
                                .build();
checkBoxItem.setValidation(checkBoxValidation);

設定下拉式選單(List)

透過 addListItem() 來建置即可,比較沒看到特別好玩的部分

 let list_item = form.addListItem();
  list_item.setTitle('Option for the Lunch')
           .setChoices([
                list_item.createChoice('Meat'),
                list_item.createChoice('Vegetarian')
            ]);

設定線性刻度(Scale)

透過 addScaleItem() 來建置。基本上是輸入數值。那身為中文使用者,會很想問說,那怎麼樣輸入中文?這時就要透過 setLabels('Bad', 'Good') 的方式。

let scale_item = form.addScaleItem();
scale_item.setTitle('Rate this form')
          .setBounds(1, 5)
          .setBounds('Bad', 'Good');

設定單選方格(Grid) 和 核取方塊格(Checkbox Grid)

藉由 addGridItem 來新增。

let grid_item = form.addGridItem();
  grid_item.setTitle('Rate your interests')
           .setRows(['SDE', 'Test Engineer', 'Project Manager'])
           .setColumns([5, 4, 3,2,1])
           .setHelpText("It won't affect your scores in interviewing.");

且一樣有 GridValidationBuilder 可以建立。但方式只有一種,也就是限制每一直欄都只能有一個被填入:requireLimitOneResponsePerColumn()。程式碼如下。

let gridValidation = FormApp.createGridValidation()
                              .setHelpText("Select one item per column.")
                              .requireLimitOneResponsePerColumn()
                              .build();

  grid_item.setValidation(gridValidation);

那會想問,如果我想設定的是每個橫的行,都必須要填入一個選項呢?就單純用 .setRequired(true) 即可做到了。

那至於「核取方塊格」呢?因為方式都一樣,所以可以單純地把 addGridItem 換成 addCheckboxGridItem。兩者的 validation method 也都都是只有一種,但核取方塊格需要將 GridValidationBuilder 換成 CheckboxGridValidationBuilder() 就是。

設定日期與時間(addDateTimeItem)

日期與時間也相對單純,用 addDateTimeItem() 即可。

let date_time_item = form.addDateTimeItem();
date_time_item.setTitle('When is your availability?');

Step 3 讀取 Google Sheet 裡面的資料來客製 Google Form

好,但我們的重點是客製化表單,要怎麼樣將原本的做表格變客製化呢?這邊先用個簡單的方式。

function writeForm(){
  let data = readData();
  let new_forms_id_arr = [];
  for(row_data of data){
   let form_name = row_data[0];
   let form_description = row_data[1];
   
   let curr_form = FormApp.create(form_name);
   let curr_form_id = curr_form.getId()
   new_forms_id_arr.push([curr_form_id]);
   moveFile(curr_form_id, target_folder_ID)
   curr_form.setTitle(form_name).setDescription(form_description);
   
   let question_function_list = [addNameText,
                                 addLunchList,
                                 addTrafficChoices,        
                                 addInterestGridWithValidation,
                                 addSkillCheckbox,
                                 addAvailableDateTime,
                                 addSuggestionParagraphText,
                                 ]
     
    for(let i = 2; i< row_data.length; i++){
        if(row_data[i] == true){
            question_function_list[i-2](curr_form);
        }
    }
    
   // add general item
   addRateScale(curr_form);
  }
  writeData(new_forms_id_arr)
}

裡頭要用到的功能上方都有寫,可以直接複製喔!

Step 4 將創造後的表單 ID 寫回 Google Sheet

其實已經偷偷寫在上面的 Code 裡面了,請看 Step 3 最後 writeData() 的部分。

完整執行畫面——

回對我們的任務表,確認 Ben 表單中是沒有「下拉選單」午餐編號的。

任務完成!


好,我們總算完成了(落淚),雖然昨日 D11 介紹的第一種方式比較簡單,但實際上想彈性運用,我們會需要很多 D12 的內容。但,如果我們今天真的生了 100 份表單,那要怎麼樣統一回應?總不能慢慢搜集吧。這時候就會需要看我們的 D13 了。

不知不覺就寫了一整天…Orz,希望大家喜歡。一樣提醒,用FormApp.create()來創造表單是有 Quota 限制——每天不超過 250 份。如果還有問題,透過留言之外,也可以到 Facebook Group,想開很久這次鐵人賽才真的開起來哈哈哈,歡迎來當 Founding Member。如果不想錯過可以訂閱按讚小鈴鐺(?),也歡迎留言跟我說你還想知道什麼做法/主題。我們明天見。

目錄

Scroll to Top