LoginSignup
3
6

More than 3 years have passed since last update.

Googleフォームをカスタマイズしてさらに編集可能にした

Posted at

はじめに

京都のテイクアウト情報が集まるサービスを作った(GAS)と関連する内容です。
こちらも是非ご覧ください!

また記事で触れているサービスはこちらです。京都在住の方は是非使ってみてください👘
https://kyoto-take-out.web.app/

このサービスではGoogleフォームを使用してお店の情報を集めております。
一つの回答が一つのお店の情報としており、今までは回答を編集していただくことで、お店の情報をアップデートしていました。
しかし、フォームが上手くいかなかったという問い合わせや、画像が投稿できない1 2といった問題がありました。

そこで、サービス上にGoogleフォームをカスタマイズして埋め込むことを考えました。
google form カスタマイズなどとググると色々な情報が出てくるのですが、
そのままの方法ですと編集が出来ません。(普通にやるとGoogleフォームの画面に行ってしまう)

そこで少し工夫をすることで、Googleフォームをカスタマイズし、なおかつ編集可能にしたので、
その時にやったことを書いていきます。

使用技術

  • TypeScript
  • GAS
  • Googleフォーム
  • Spreadsheet

概要

今回やったことの概要は以下のようになっています。

まずGoogleフォームについてなのですが
formaction"https://docs.google.com/forms/d/e/xxx/formResponse"となっており、これを自作のフォームに貼り付けることでフォームをカスタマイズし埋め込むことができます。

そして、フォームの回答一つ一つにはIDがついており、編集時のformaction"https://docs.google.com/forms/d/e/xxx/formResponse?edit2=***"のように回答のIDが付与されています。

そこで、自作サービスのフォームにアクセスするURLにパラメータとしてフォームの回答IDを渡し、フロントでactionに付与することで編集を可能にしています。

この時に編集前のフォームの回答は自動では取得できないので、一緒にパラメータとして渡しています。
実際には回答内容そのままではなく、店舗ID(フォームのIDではない)を渡しています。

なお、フロント側の処理については書かないのでご了承ください。

やったこと

GASプロジェクトの作成

まず、GASのプロジェクトを作成するのですが、この時はフォームから作成するようにしてください。
つまり

  1. Googleフォームにアクセス
  2. 右のメニューからスクリプトエディタを選択

とするようにしてください。

今まで知らなかったのですが、GASはプロジェクトの作成方法(紐づいている先?)によって、トリガーに設定できる内容と受け取れる値が異なります

今回はフォームのトリガーが必要なので、必ずフォームから作成するようにしてください。

コード作成

全文

main.ts
import { SpreadsheetService } from "./spreadsheetService";

const handleSubmitForm = (e: GoogleAppsScript.Events.FormsOnFormSubmit) => {
  console.log(e);
  const formAnswer = e.response;

  const spreadsheetService = new SpreadsheetService();

  const arrayOfAnswer = spreadsheetService.getDataFromFormAnswer();

  console.log(formAnswer.getTimestamp());
  // timestampが同じで店舗名が同じものを取得
  const targetAnswer = arrayOfAnswer.find((answer) => {
    if (formAnswer.getTimestamp().getTime() === answer.timestamp.getTime()) {
      const itemResponses = formAnswer.getItemResponses();
      const itemResponseOfStoreName = itemResponses.find(
        (item) => item.getItem().getTitle() === "店舗名"
      );
      const value = itemResponseOfStoreName.getResponse();
      return value === answer.storeName;
    }

    return false;
  });

  const storeID = targetAnswer ? targetAnswer.ID : undefined;
  if (!storeID) console.log("storeIDがありません");
  const formID = formAnswer.getId();

  console.log(`[storeID]: ${storeID}`);
  console.log(`[formID]: ${formID}`);

  let formAnswerText = "";
  formAnswer.getItemResponses().forEach((itemResponse) => {
    const question = itemResponse.getItem().getTitle();
    const answer = itemResponse.getResponse();
    formAnswerText += `${question} : ${answer}\n`;
  });

  let body = "この度はフォームのご回答誠にありがとうございます。\n\n";

  body += "ご回答内容は以下の通りです。\n\n";
  body += formAnswerText + "\n";

  body += "ご回答内容の編集はこちらからお願いします。\n";

  body += `https://***/form?storeID=${storeID}&formID=${formID}\n\n`;

  const options: GoogleAppsScript.Gmail.GmailAdvancedOptions = {
    name: "NAME",
    bcc: "test@gmail.com",
  };

  GmailApp.sendEmail(
    targetAnswer.email,
    "【編集リンクあり】フォームのご回答誠にありがとうございます",
    body,
    options
  );
};
spreadsheetService.ts
export class SpreadsheetService {
  spreadsheetID: string;
  constructor() {
    this.spreadsheetID = PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID");
  }

  getSheetByName = (sheetName: string): GoogleAppsScript.Spreadsheet.Sheet => {
    // シート情報を取得
    const sheet = SpreadsheetApp.openById(this.spreadsheetID).getSheetByName(
      sheetName
    );
    if (!sheet) {
      console.error(`「${sheetName}」が存在しません`);
      return undefined;
    }
    return sheet;
  };

  getIndexesOfFormAnswer = (firstRow: string[]) => {
    return {
      timestamp: firstRow.indexOf("タイムスタンプ"),
      email: firstRow.indexOf("メールアドレス"),
      storeName: firstRow.indexOf("店舗名"),
    };
  };

  getDataFromFormAnswer = (): SpreadsheetModel.FormAnswer[] => {
    const sheetName = "フォームの回答 1";
    const sheet = this.getSheetByName(sheetName);

    const firstRow = this.getFirstRowFromSheet(sheet);

    const data = sheet
      .getRange(2, 1, sheet.getLastRow() - 1, firstRow.length)
      .getValues();

    const indexOf = this.getIndexesOfFormAnswer(firstRow);

    for (const key in indexOf) {
      if (indexOf[key] < 0) {
        console.log(`[${sheetName}]に[${key}]が存在しません`);
      }
    }

    const arrayOfAnswer: SpreadsheetModel.FormAnswer[] = [];
    data.forEach((row, index) => {
      arrayOfAnswer.push({
        ID: index + 1,
        timestamp: row[indexOf.timestamp],
        email: row[indexOf.email],
        storeName: row[indexOf.storeName],
      });
    });
    return arrayOfAnswer;
  };

  getFirstRowFromSheet = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => {
    const indexOfLastColumn = sheet
      .getRange(1, 1)
      .getNextDataCell(SpreadsheetApp.Direction.NEXT)
      .getColumn();
    const firstRow = sheet.getRange(1, 1, 1, indexOfLastColumn).getValues()[0];

    return firstRow;
  };
}

詳細解説

SpreadsheetServiceはスプレッドシートから情報を取ってくるもので、
handleSubmitFormはフォーム回答時のトリガーに設定しており、eにはフォームの回答内容などが入っています。(詳しくはGoogle Forms eventsを参照)

const spreadsheetService = new SpreadsheetService();

const arrayOfAnswer = spreadsheetService.getDataFromFormAnswer();

console.log(formAnswer.getTimestamp());
// timestampが同じで店舗名が同じものを取得
const targetAnswer = arrayOfAnswer.find((answer) => {
  if (formAnswer.getTimestamp().getTime() === answer.timestamp.getTime()) {
    const itemResponses = formAnswer.getItemResponses();
    const itemResponseOfStoreName = itemResponses.find(
      (item) => item.getItem().getTitle() === "店舗名"
    );
    const value = itemResponseOfStoreName.getResponse();
    return value === answer.storeName;
  }

  return false;
});

スプレッドシートより取得したものからフォームの回答とtimestampが同じで、店舗名も同じものを取得しています。
こうしている理由は、スプレッドシートのフォームの回答が何番目かを店舗のIDとしており、編集用リンクにはこのIDを渡したいからです。
パラメータに直接回答内容を入れれば楽なのですが、そうした場合は前回の編集リンクから編集しないとおかしなことになってしまうので、店舗のIDにすることで、実際にサービス上で編集する際に最新のデータを取得するようにしています。

timestampだけだと同じタイミングの回答があるとおかしなことになるので、店舗名も使用しています(これでも理論的には完全ではありませんが、実用上は恐らく大丈夫だろうという感じ)

また、フォームの回答時のトリガーは回答が反映されてから発火するっぽいので、新しい回答時にも回答の内容がスプレッドシートからも取得できます。(厳密には同期的ではないかもしれないですが未確認。念の為スリープとか最初にしたほうがいいかも)

トリガー設定

あとはGASにclaspなどでpushし、GASエディタから以下のようにトリガーを設定すれば完了です。
image.png

フロントでの処理

以上で編集用リンクの作成とメールの送信ができるようになりました。
あとはフロントエンドでURLのパラメータを処理し、データを取得し、formに初期値を入れれば編集フォームの完成です。

おわりに

今回はGoogleフォームをカスタマイズし、さらに編集もできるようにしました。
案外使えそうだなと思ったので、参考にしてみてください。
お役に立てましたら幸いです。

ありがとうございました🙇‍♂️


  1. GASのHTML serviceを使いアップローダーを作ってみたりしたがイマイチだった。 

  2. Googleアカウントを使うようにすれば可能なのですが、Googleアカウントを持っていないユーザも多いと判断し、導入しませんでした。 

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6