LoginSignup
3
5

More than 1 year has passed since last update.

GoogleAppsScriptハンズオン/Googleカレンダーに予定を一括追加するツールを作る!

Last updated at Posted at 2020-08-20

対象

本記事は以下のような方を対象にしています。

  • HTML/JavaScriptの基本は理解している!
  • GoogleAppsScriptでWebアプリが作れると聞いたがやったことはない!
  • 説明はいいからとにかく何か作ってみたい!

本記事ではGASがどんなものかの紹介はあまりしません。またGASには今回紹介するWebアプリ以外に様々な使い方がありますが、それらに関しても特に触れません。GASでのWebアプリ制作が気になる!説明されるよりも実際に使ってみて感じるほうがわかりやすい!という需要のため、GASを触ったことがなくても、なんとなくの理解でも、まずは動くものの完成までたどり着けるようなコンテンツを目指したいと思います。

作るもの

不規則に繰り返す複数の予定をGoogleカレンダーに一撃でパパっと追加できる便利ツールです。なぜそんな物を作るのか?
僕が欲しいからです!!
作るついでにハンズオンの題材にしてしまえーというやつですね。1回の開発で2度美味しい。バイトの予定を手作業でGoogleカレンダーに追加するのが面倒過ぎたので作ってみました。

スクリーンショット

image.png

動作概要

入力画面で予定を追加する年月と予定のタイトルを指定し、各勤務予定(最大6パターンまで)の開始と終了時刻を入れる。
それぞれの勤務パターンで出勤する日付をカンマ区切で入力する。
予定を追加をクリックすると、Googleカレンダーの予め指定したカレンダーに入力内容通りの予定が一気に追加される。
追加が完了したら何月何日の何時から何時に予定が追加されたのかの記録が「ログ」の欄に表示される。

おおまかな制作イメージ

Webアプリの開発自体が初めてという方にも今回のアプリの全体像を掴んで頂くために図を用意しました。大抵のWebアプリはユーザーが操作を行ったり情報を表示したりするクライアントサイドと、APIという窓口を通じて届くクライアントからの要求に応じて処理を行うサーバーサイドの2つから成っています。今回はGSファイル(これがGAS本体)とHTMLファイル(JavaScriptも含む)という2つのファイルを作りますが、前者がサーバーサイドのプログラム、後者がクライアントサイドの見た目を指定するファイルに当たります。
image.png

プロジェクトを作成

早速作成に入りましょう。
GoogleDriveにアクセスします。
左上の「新規」から「その他→GoogleAppsScript」と進みます。
image.png

左上の「無題のプロジェクト」からプロジェクトの名前を適当に決めましょう。
これでGASを使う準備ができました。
image.png

必要なファイルを作成

今回作るプロジェクトには
①予定の内容を入力する画面(HTML)
②入力された内容を受けてGoogleカレンダーに予定を追加するスクリプト(GSファイル,これがGASのスクリプト本体)
の2つのファイルが必要になるので作っていきます。
このうち②のGSファイルについては先程プロジェクトを作ったときに最初から作られてるので①のHTMLファイルを新たに作ります。
「ファイル→New→HTMLファイル」でHTMLファイルを作ります。
image.png
名前は適当にindexとしておきます。拡張子は不要です。
image.png
HTMLファイルが作成されました!これは後ほど使います。
image.png

サーバーへ送るデータの形式を決める

Webアプリではユーザー(以後クライアントと表現します)とサーバーの間でデータをやり取りすることが必須です。今回はユーザーが入力した予定に関する情報をサーバー側に送る必要がありますね。そこで、クライアントで入力されたデータをどのような形で送り、サーバー側で受け取るかをきちんと定めておきます。

送る内容を列挙する

今回サーバーへ送りたいのは「入力された予定に関するデータ」です。
もっと詳しく挙げてみると

  • 予定のタイトル
  • (予定を追加したい)年
  • (予定を追加したい)月
  • 予定1の開始時刻
  • 予定1の終了時刻
  • 予定1の日付
  • 予定2の開始時刻
  • 予定2の終了時刻
    (略)
  • 予定6の終了時刻
  • 予定6の日付
    の21個がすべてのデータになります。

データ形式の検討

さて、フロントのHTMLからこの21個のデータをサーバーに送れれば良いことはわかったので「どんなデータ構造で」送るかを考えてみます。
もちろんこれら21個をバラバラで送ることも不可能ではありませんが、あまり気持ちよくはないのでオブジェクトや配列を使って工夫してみたいと思います。

予定1から6までそれぞれについて①その開始時刻、②終了時刻、③その予定が入っている日(カンマ区切りで複数の日付)の3つのデータがありますので、それらを1つにまとめて{}で囲ってみます。

{予定1の開始時刻,予定1の終了時刻,予定1が入っている日}

その塊が6個あるので6個まとめて[]で囲ってみます。これで下記のように規則的でわかりやすい構造が出来上がりました。(ここで言う{}[]はまとまりを表すために仮で用いているものであって特定の言語の記法に則ったものではありません。)

[
{予定1の開始時刻,予定1の終了時刻,予定1が入っている日},
{予定2の開始時刻,予定2の終了時刻,予定2が入っている日},
{予定3の開始時刻,予定3の終了時刻,予定3が入っている日},
{予定4の開始時刻,予定4の終了時刻,予定4が入っている日},
{予定5の開始時刻,予定5の終了時刻,予定5が入っている日},
{予定6の開始時刻,予定6の終了時刻,予定6が入っている日}
]

ここで用いた[]は同じもの複数個をまとめる構造で、JavaScriptでは配列にあたります。{}は1つのものに関する異なる複数の情報をまとめる構造で、オブジェクトにあたります。
上記の構造をJavaScriptの文法に則った形に表してみるとこんな感じになります。

const eventList = [
{start:"9:00",end:"10:00",day:"1,13"},
{start:"12:00",end:"16:00",day:"2,6,7,22"},
{start:"16:00",end:"20:00",day:"3,5,16"},
{start:"10:00",end:"18:00",day:"4,15,19,20"},
{start:"20:00",end:"23:00",day:"5,18,28"},
{start:"13:00",end:"21:00",day:"1,9,23"}
]

このような構造にすることで0番目の予定の開始時刻ならば(eventList[0].start)のように.startをつける、という直感的なデータの呼び出しが可能になります。

//呼び出しの例
eventList[0].start
//出力:"9:00"
eventList[4].day
//出力:"5,18,28"
eventList[2]
//出力:{start:"16:00",end:"20:00",day:"3,5,16"}

少しGASから話がそれましたが、今回は

  • 予定のタイトル
  • 月  
  • start,end,dayの3つの値を持つオブジェクト6つが入った配列1個

という構成でデータの受け渡しを行うことにしましょう。

GASのスクリプトを作る

まずはユーザーが入力した予定に関する情報をクライアントのHTMLから受け取ってGoogleカレンダーに予定として追加するスクリプト、つまりサーバー側で動くGASのスクリプト本体から作っていきます。
クライアント側から送られてくるデータは

  • 予定のタイトル
  • 月  
  • start,end,dayの3つの値を持つオブジェクト6つが入った配列1個

の4つでしたのでこれらを引数として受け取り、Googleカレンダーに予定を追加するスクリプトを作ります。

GASに戻り「コード.gs」を開きます。既にmyFunction関数が作られていますね。
image.png

まずは今回メインとなるGoogleカレンダーに予定を追加する部分をつくりたいと思います。
Googleカレンダーへの予定追加は
1.予定を追加したいカレンダーを取得する
2.取得したカレンダーに予定追加を行う
の2ステップで行います。

カレンダーIDの取得

1の準備として追加したいカレンダーのカレンダーIDが必要となります。
Googleカレンダーにアクセスし、マイカレンダーの中から今回作るツールの予定追加先にするカレンダーの︙からメニューを開きます。
image.png
設定と共有を開きます。
image.png
表示されたページの下の方「カレンダーの統合」という項目にカレンダーIDが書かれています。hogemogepiyoyoyoyooooo@group.calendar.google.comみたいな形の文字列がカレンダーIDです。これをコピーしておきましょう。
image.png

カレンダーへの予定追加をやってみる

GASに戻り、先程の2ステップ
1.予定を追加したいカレンダーを取得する
2.取得したカレンダーに予定追加を行う
を思い出します。今①をやってましたね。カレンダーIDを用意できたのでGASのスクリプトを書いていきます。

先程のカレンダーIDをもつカレンダーを取得するにはCalendarAppクラスを使います。このクラスのgetCalendarByIdメソッドを使ってカレンダーを取得します。クラス?メソッド?なんやねんそれ?!という人はとにかく以下のように書けばカレンダーを取得できるんだと思っていただいてOKです!
myFunction関数を以下のように書き換えてみます。hogemogepiyoyo@group.calendar.google.comの部分は先ほどコピーしたカレンダーIDに書き換えてください。


function myFunction() {
  const targetCal = CalendarApp.getCalendarById("hogemogepiyoyo@group.calendar.google.com");
}

このように書くことで取得したカレンダーオブジェクトを定数targetCalに代入しています。

続いて②の取得したカレンダーへの予定追加をやってみます。myFunction関数の2行目の下に以下を追加します。


  const startTime = new Date(2020,7,1,10,00,00);
  const endTime = new Date(2020,7,1,11,00,00);
  targetCal.createEvent("会議",startTime,endTime);

取得したカレンダーに対して予定を追加するにはcreateEventメソッドを使います。createEventメソッドの引数は(予定のタイトル,開始時刻,終了時刻)の3つです。
予定のタイトルは単に文字列で渡しますが、開始時刻と終了時刻はただの文字列ではなくDateオブジェクトで渡さなければいけません。オブジェクトという言葉が聞き慣れない方はとりあえず「Dateオブジェクトは日付や時刻を表すのに特化したデータ型」くらいの認識でOKです。Dateオブジェクトはある1つの特定の時刻(例えば日本時間2020年8月7日金曜日14時26分17秒)を表す年,月,日,曜日,時刻,タイムゾーンなどの情報を1つにまとめて持っており、時刻を表すのに最適です。
上記プログラムの3,4行目でそのDateオブジェクトを作っています。オブジェクト(正確にはインスタンス)を新規で作る場合newという演算子を用いて以下のように書けばOKです。


//書き方
//const startTime = new Date(年,月-1,日,時,分,秒);
//実際
const startTime = new Date(2020,7,1,10,00,00);
//startTmeには日本時間で"2020年8月1日10:00:00"を表すDateオブジェクトが代入される

ここで、月の部分のみ"指定したい月-1"にする(8月にしたければ7と書く)必要があることに気をつけましょう。これはそういう仕様なので仕方ありません。

開始時刻と終了時刻それぞれのDateオブジェクトを作れたらcreateEventメソッドにそれらを投げます。書き方はtargetCal.createEvent(予定のタイトル,開始時刻,終了時刻)です。targetCalは①で取得したカレンダーオブジェクトが代入された定数のことでしたね。

targetCal.createEvent("会議",startTime,endTime);

ここまででコード.gsの中身は以下のようになっています。Ctrl+Sで保存しておきましょう。

function myFunction() {
//hogemogepiyoyo@group.calendar.google.comは自身のカレンダーIDに書き換えてください。
  const targetCal = CalendarApp.getCalendarById("hogemogepiyoyo@group.calendar.google.com");
  const startTime = new Date(2020,7,1,10,00,00);
  const endTime = new Date(2020,7,1,11,00,00);
  targetCal.createEvent("会議",startTime,endTime);
}

実行してみる

では一度実行してみて、予定が追加できるか確認してみましょう。
実行するには「実行→関数を実行→myFunction」と進みます
image.png
このスクリプトはGoogleアカウントのデータ(今回はGoogleカレンダー)への書き込みを行うので、その権限を要求されます。許可を確認へ進みます。
image.png
アカウントを選択するとこのような画面になるので「詳細」から「myFirstGAS(安全ではないページ)に移動」をクリックします。
image.png
予想通りGoogleカレンダーへのアクセス権限を要求されるので許可しましょう。
image.png
するとウィンドウが自動で閉じ、関数が実行されます。
image.png
この黄色い「Running function myFunction」が消えれば実行終了です。実際に予定が追加されたか確認してみましょう。Googleカレンダーを開くと...
image.png
確かに2020年8月1日10:00~11:00で"会議"というタイトルの予定ができていました!!
このように認証周りの手続きをGUIでポチポチするだけで済ますことができ、たった4行でGoogleカレンダーへの予定追加ができてしまうのがGASの素晴らしいポイントですね。

クライアントから送られてきたデータを受け取れるようにする

最終的にはクライアントから送られてきたデータを元に予定の追加を行うので、送られてきたデータを受け取るところを作っていきます。
既に以下の4つのデータを受け取るということは決めてありましたね。先にそれぞれの変数や配列に名前を付けておきます。

  • 予定のタイトル(title)
  • 年(year)
  • 月(month)
  • start,end,dayの3つの値を持つオブジェクトが6つ入った配列(eventList)

コード.gsの中身を以下のように書き換えます。
関数名をaddEventsとし4つの引数title,year,month,eventListをもたせています。それ以外は変えていません。

function addEvents(title,year,month,eventList){
  const targetCal = CalendarApp.getCalendarById("hogemogepiyoyo@group.calendar.google.com");
//hogemogepiyoyo@group.calendar.google.comは自身のカレンダーIDに書き換えてください。
  const startTime = new Date(2020,7,1,10,00,00);
  const endTime = new Date(2020,7,1,11,00,00);
  targetCal.createEvent("会議",startTime,endTime);
}

予定のタイトルを"会議"としていましたが、クライアントから送られてきたtitle変数の中身をタイトルにしたいので

  targetCal.createEvent("会議",startTime,endTime);

の部分を

  targetCal.createEvent(title,startTime,endTime);

と書き換えてしまいましょう。

次にcreateEventメソッドによって予定追加を行うにはyear,month,eventListの3つで送られてきた複数の予定の開始時刻と終了時刻を文字列からDateオブジェクトに変える必要がありましたね。そこで、複数ある予定1つ1つについて開始と終了の2つのDateオブジェクトを作って返す関数をつくります。

色々なやり方があると思いますが、僕はyear,monthと配列eventListの要素1個(今回の中身はオブジェクト)である"開始時刻","終了時刻","その予定の入る複数の日付をカンマ区切りから配列に変換したもの"の5つを引数にしました。ちなみに配列eventListの中身はこんな感じでしたね。

//eventListの中身の例
const eventList = [
{start:"9:00",end:"10:00",day:"1,13"},
{start:"12:00",end:"16:00",day:"2,6,7,22"},
{start:"16:00",end:"20:00",day:"3,5,16"},
{start:"10:00",end:"18:00",day:"4,15,19,20"},
{start:"20:00",end:"23:00",day:"5,18,28"},
{start:"13:00",end:"21:00",day:"1,9,23"}
]

作った関数はこんな感じです。(これ書いてる人もJavaScript初心者なのでもっと良い書き方があったらごめんなさい...)
これをコード.gsに書き加えます。

function convertToDateObject(year,month,start,end,day){
  //開始時刻startと終了時刻endを1セットにしたオリジナルの"eventオブジェクト"を定義
  function event(){
    this.start;
    this.end;
  }
  //戻り値用の配列(上で定義したeventオブジェクトを入れます)
  let returnList = [];
  //forループ内で使う使い捨ての変数。
  let tmp;
  //引数にもらった時間での予定が入っている日付1つ1つについて開始時刻,終了時刻のDateオブジェクトを作る
  for(let i=0;i<day.length;i++){
    //引数のうちどれか1つでも空な場合をはじく
    if(year&&month-1&&day[i]&&end.split(':')[0]&&end.split(':')[1]){
    //eventオブジェクトを新規作成
    tmp = new event();
    //開始時刻と終了時刻のDateオブジェクトを作ったeventオブジェクトに入れる
    tmp.end = new Date(year,month-1,day[i],end.split(':')[0],end.split(':')[1]);
    tmp.start = new Date(year,month-1,day[i],start.split(':')[0],start.split(':')[1]);
    //完成したeventオブジェクトtmpを配列returnListへ格納
    returnList.push(tmp);
    }
  }
  return returnList;
}

Dateオブジェクトに変換する関数を作ったので、それを使って受け取った文字列データを変換して予定を作成していきます。
addEvents関数を以下のように書き換えます。

function addEvents(title,year,month,eventList){
  const targetCal = CalendarApp.getCalendarById("hogemogepiyoyo@group.calendar.google.com");
  //hogemogepiyoyo@group.calendar.google.comは自身のカレンダーIDに書き換えてください。
  let convertedEventList;//変換後のeventオブジェクトを入れる配列
  for(i of eventList){//配列eventListの全ての要素に対してDateオブジェクトへの変換を行う
    convertedEventList = convertToDateObject(year,month,i.start,i.end,i.day.split(','));
    for(j of convertedEventList){//変換後の配列の全ての要素(=eventオブジェクト)の内容で予定を作成する。
       //j.startは開始時刻を表すDateオブジェクト(j.endも同じ)
       targetCal.createEvent(title,j.start,j.end);
    }
  }
}

予定を追加する部分はできました。しかしこのままではこの関数には戻り値がないので追加の処理が終わったかどうか、正しく処理ができたかわかりにくくなってしまいます。そこで以下のように戻り値を付けてみましょう。

function addEvents(title,year,month,eventList){
  const targetCal = CalendarApp.getCalendarById("hogemogepiyoyo@group.calendar.google.com");
  //hogemogepiyoyo@group.calendar.google.comは自身のカレンダーIDに書き換えてください。
  let convertedEventList;//変換後のeventオブジェクトを入れる配列
  let returnMessage = "";//戻り値として返すメッセージ
  for(i of eventList){//配列eventListの全ての要素に対してDateオブジェクトへの変換を行う
    convertedEventList = convertToDateObject(year,month,i.start,i.end,i.day.split(','));
    for(j of convertedEventList){//変換後の配列の全ての要素(=eventオブジェクト)の内容で予定を作成する。
       //j.startは開始時刻を表すDateオブジェクト(j.endも同じ)
       targetCal.createEvent(title,j.start,j.end);
       //予定を追加した時の内容をそのままメッセージに入れる
       returnMessage += title+" : "+j.start+" - "+j.end+"\n";
    }
  }
  return returnMessage;
}

これによって各予定のタイトル、開始と終了時刻がテキストになって返されるようになりました。
これでサーバー側のGASスクリプトはほぼ完成です!!

入力画面(HTML)をつくる

完成イメージはこんな感じです。
image.png
最終的にはCSSフレームワーク"Bulma"を使って見た目を整えてこんな感じにしました。
image.png
といってもHTMLの説明までしていると大変なことになるので割愛させていただきます。
最初に作成したindex.htmlの中身を以下のように書き換えます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>予定一括追加くん</title>
    <base target="_top">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
</head>

<body style="margin:2rem;"> 
    <h1 class="title is-large">予定を一括追加するよ君</h1>
    <div class='field'>
        <label class="label">予定のタイトル</label> 
        <div class="level-left">
          <input type="text" id="title" class="input" style="width:15rem;">
        </div>
    </div>
    <div class='field'>
        <label class="label">年月</label> 
        <div class="level-left">
            <input type="text" id="year"class="input" style="width:5rem;"><input type="text" id="month"class="input" style="width:5rem;"></div>
    </div>
    <div id="shiftflame">
        <label class="label">勤務予定</label> 
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        <br>
    </div>
    <div class='field'>
    <div class='control'>
        <input type="button" value="予定を追加" onclick="" class="button is-link">
    </div>
    </div>

    <div class="control" style="width:60rem;">
    ログ<br>
    <textarea id="log" rows="20" readonly class="textarea has-fixed-size"></textarea>
  </div>
  <script>
    //年と月を自動入力
    const date = new Date(); 
    document.getElementById("year").value = date.getFullYear();
    document.getElementById("month").value = date.getMonth()+1;

  </script>
</body>

</html>

それぞれのテキストボックスには以下のようにidまたはname属性が付けてあります。
タイトル:id="title"
年: id="year"
月: id="month"
予定(6つとも同じ)
 開始時刻: name="start"
 終了時刻: name="end"
 出勤日: name="day"
ログ: id="log"

※HTMLにstyle=""としてスタイルを指定するなんで邪道だ!!と思われるかもしれませんが、GASではJavaScriptやCSSを別ファイルにするには少々ややこしい手順を踏む必要があり面倒という事情に加え、ハンズオンなのでお手軽性重視ということでこのような方法をとっておりますことをご理解ください。

入力内容をサーバーへ送れるようにする

入力画面はできたので、入力内容をサーバーへ送る部分を作っていきます。といっても面倒なDateオブジェクトへの変換などはサーバー側で実装済みなので、入力内容を取得してオブジェクトや配列にまとめて送るだけと比較的シンプルです。
この処理はHTMLファイルの<script>タグ内にJavaScriptで記述していきます。混同しがちですが今から作るのはクライアントのブラウザー上で動くJavaScriptです。先程まで作っていたのはサーバー側で動くJavaScript(GASスクリプト)でした。

HTMLの<script>タグ内に下記のように書きます

<script>

//年と月を自動入力
const date = new Date(); 
document.getElementById("year").value = date.getFullYear();
document.getElementById("month").value = date.getMonth()+1;

function sendData(){
  //オリジナルのeventオブジェクトを定義
  function event() {
      this.start;
      this.end;
      this.day;
  }
  /*============入力データを取得============*/
  //start,end,dayというname属性を持つ要素は複数あるので以下3つは配列になります
  const start = document.getElementsByName("start");
  const end = document.getElementsByName("end");
  const day = document.getElementsByName("day");
  //以下3つはただの変数
  const year = document.getElementById("year").value;
  const month = document.getElementById("month").value;
  const title = document.getElementById("title").value;


  /*============取得したデータを配列,オブジェクトに入れる============*/
  let count = 6;///予定の種類数
  let tmp;//forループ内の使い捨て
  let eventList = [];//最終的にサーバーに送信する、"予定の配列"。eventオブジェクトが入る
  //予定の種類分ループ(今回は6個)
  for(let i=0;i<count;i++){
      //eventオブジェクトを新規作成
      tmp = new event();
      //HTMLから取得した文字列をeventオブジェクトに入れる
      tmp.day=day[i].value;
      tmp.end=end[i].value;
      tmp.start=start[i].value;
      //できたeventオブジェクト"tmp"をeventListへ格納
      eventList.push(tmp);
  }

/*完成した配列eventListとyear,month,titleをサーバーへ送る*/
  //これから実装します
}
</script>

今作成したsendData関数を入力画面の「予定を追加」ボタンが押されたときに発動したいので、「予定を追加」ボタンのonclick属性にonclick="sendData()"を指定します。
つまりボタンの部分は下記のようになります。

<div class='field'>
    <div class='control'>
        <input type="button" value="予定を追加" onclick="sendData()" class="button is-link">
    </div>
</div>

ここまででサーバーに送信する準備が完了しました。次にサーバーへの送信部分を作っていきます。

入力内容をサーバーに送る

クライアントからサーバーへのデータ送信といえばPOSTリクエスト!!と思われるかもしれませんが、今回POSTはしません。GASにはもっと簡単にサーバーとクライアントでデータの受け渡しが可能な仕組みが用意されているのでそれを使ってみたいと思います。
クライアントのHTMLの<script>タグ内の最下行に//これから実装しますとコメントを入れた部分にこれを書き加えます。

google.script.run.addEvents(title,year,month,eventList);

何やら見慣れない物が出てきましたが、これこそがGASでWebアプリを作るときに大活躍するgoogle.script.runクラスです。
ざっくりいうと、クライアントのJavaScriptからサーバーのGASスクリプトの中の任意の関数を実行させることができます。書き方は
google.script.run.サーバー側GASの関数名(引数1,引数2,・・・);となります。
ここでは先程サーバー側に用意したaddEvents関数を呼び出して実行しています。さらにこの関数には4つの引数がありましたので、それらも同時にサーバーへ送る事ができます。

さらにgoogle.script.runクラスにはwithSuccessHandlerメソッドというのがあり、これを一緒に使うことでサーバー側の任意の関数の処理が終了した後に行いたい処理を指定することができます。今回は"予定の追加が完了したら、追加した内容を入力画面のログ欄に表示する"という動作にこれを使います。
先程の1行を書き換えます。

const callback = message =>{
            if(!message){
                message = "予定は追加されませんでした";
            }
            document.getElementById("log").value = message;
        };
google.script.run.withSuccessHandler(callback).addEvents(title,year,month,eventList);

何やら行数が一気に増えてしまいましたが、1~6行目ではサーバー側の関数の実行後に行いたい処理を関数にして定数callbackに代入しています。この定数callbackをwithSuccessHandlerメソッドに渡すことで"予定の追加が完了したらこの処理(callbackの中身の関数)を実行してください"とお願いしているわけですね。このように何かの処理をするとき、その処理の完了後に実行してほしい内容を記した関数を「コールバック関数」といいます。ちなみにこの関数の引数になっているmessageというのはサーバー側の関数の戻り値が入ってきます。別に名前はサーバー側の戻り値の変数名とあわせなくても問題ありません。
これによってgoogle.script.runクラスの引数によってクライアントからサーバーにデータを送り、withSuccessHandlerメソッドのコールバック関数によってサーバーからデータを受け取ることができました!これもたった1行で書けるのが凄い!

ここまでで作りたい機能がほぼ完成しました。

アクセス時に入力画面が表示されるようにする

このWebアプリにアクセスすると先程作ったHTMLの入力画面が表示されるようにしていきます。
サーバー側のGSファイルコード.gsにdoGet関数を追加します。

function doGet(){
  let html;
  html = HtmlService.createTemplateFromFile("index");
  return html.evaluate().setTitle("予定一括追加くん");
}

GASでは関数にdoGetやdoPostという名前をつけることで以下のような特別な動作をさせることができます。

  • doGet関数 : Getリクエストが来たとき自動的に実行される
  • doPost関数: Postリクエストが来たとき自動的に実行される

「リクエスト」というのはhttpリクエストと言い、クライアントとWebサーバーの間でのHTTP通信のうちクライアントからWebサーバーへ向かう時の通信の事を指します。httpリクエストにはその内容や目的に応じていくつか種類(メソッドという)がありますが、GASで使えるのはGetリクエストとPostリクエストの2種類です。これら2つは主に以下のようなときに使います。

  • Getリクエスト:(クライアントからサーバーに対し)僕に〇〇のデータを送ってください!とお願いする場合
  • Postリクエスト:(クライアントからサーバーに対し)僕から〇〇のデータを送ります!という場合

あまり馴染みがないように感じるかもしれませんが、我々が普段日常的にやっている以下のようなのことがGet,Postリクエストにあたります。(必ずしもこの限りではありません)

  • ブラウザのアドレスバーにURLを入力してアクセスするとき:Getリクエストをしている
    URLが示すWebサーバーに対し、そのページのHTMLファイルを送ってください!とお願いしています。

  • Webサービスの会員登録画面に必要事項を入力して送信するとき:Postリクエストをしている
    そのWebサービスのサーバーに対し会員登録のための情報を送っています。

本題に戻ります。今回はこのWebアプリにアクセスすると先程作ったHTMLの入力画面が表示されるようにしたいということは、Getリクエストが来たらそのHTMLを送ってあげるような動作をしたいということになります。
**上記のプログラムでは"index"という名前のHTMLファイルに"予定一括追加くん"というタイトルをつけて送り返すという処理を行っています。**これもGAS特有の書き方ですので初めのうちはコピペでOKでしょう。

ここまでで作ったプログラム

コード.gs(サーバー側のスクリプト)
8行目の"hogemogepiyoyo@group.calendar.google.com"の部分をご自分のカレンダーIDに書き換えるのをお忘れなく!

function doGet(){
  let html;
  html = HtmlService.createTemplateFromFile("index");
  return html.evaluate().setTitle("予定一括追加くん");
}

function addEvents(title,year,month,eventList){
  const targetCal = CalendarApp.getCalendarById("ngkgf499kjp8e33tq7aqasl83o@group.calendar.google.com");
 let convertedEventList;//変換後のeventオブジェクトを入れる配列
  let returnMessage = "";//戻り値として返すメッセージ
  for(i of eventList){//配列eventListの全ての要素に対してDateオブジェクトへの変換を行う
    convertedEventList = convertToDateObject(year,month,i.start,i.end,i.day.split(','));
    for(j of convertedEventList){//変換後の配列の全ての要素(=eventオブジェクト)の内容で予定を作成する。
       //j.startは開始時刻を表すDateオブジェクト(j.endも同じ)
       targetCal.createEvent(title,j.start,j.end);
       //予定を追加した時の内容をそのままメッセージに入れる
       returnMessage += title+" : "+j.start+" - "+j.end+"\n";
    }
  }
  return returnMessage;
}

function convertToDateObject(year,month,start,end,day){
  //開始時刻startと終了時刻endを1セットにしたオリジナルの"eventオブジェクト"を定義
  function event(){
    this.start;
    this.end;
  }
  //戻り値用の配列(上で定義したeventオブジェクトを入れます)
  let returnList = [];
  //forループ内で使う使い捨ての変数。
  let tmp;
  //引数にもらった時間での予定が入っている日付1つ1つについて開始時刻,終了時刻のDateオブジェクトを作る
  for(let i=0;i<day.length;i++){
    //引数のうちどれか1つでも空な場合をはじく
    if(year&&month-1&&day[i]&&end.split(':')[0]&&end.split(':')[1]){
    //eventオブジェクトを新規作成
    tmp = new event();
    //開始時刻と終了時刻のDateオブジェクトを作ったeventオブジェクトに入れる
    tmp.end = new Date(year,month-1,day[i],end.split(':')[0],end.split(':')[1]);
    tmp.start = new Date(year,month-1,day[i],start.split(':')[0],start.split(':')[1]);
    //完成したeventオブジェクトtmpを配列returnListへ格納
    returnList.push(tmp);
    }
  }
  return returnList;
}

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>予定一括追加くん</title>
    <base target="_top">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
</head>

<body style="margin:2rem;"> 
    <h1 class="title is-large">予定を一括追加するよ君</h1>
    <div class='field'>
        <label class="label">予定のタイトル</label> 
        <div class="level-left">
          <input type="text" id="title" class="input" style="width:15rem;">
        </div>
    </div>
    <div class='field'>
        <label class="label">年月</label> 
        <div class="level-left">
            <input type="text" id="year"class="input" style="width:5rem;"><input type="text" id="month"class="input" style="width:5rem;"></div>
    </div>
    <div id="shiftflame">
        <label class="label">勤務予定</label> 
        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>

        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>

        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>

        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>

        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>

        <div class='field'>
        <div class="level-left">
            <input type='text' placeholder="開始" name="start" class="input" style="width:5rem;"><input type='text' placeholder="終了" name="end" class="input" style="width:5rem;"> 
            出勤日
            <input type='text' name="day" class="input" style="width:20rem;" placeholder="カンマ区切りで日付を入力">
        </div>
        </div>
        <br>
    </div>
    <div class='field'>
    <div class='control'>
        <input type="button" value="予定を追加" onclick="sendData()" class="button is-link">
    </div>
    </div>

    <div class="control" style="width:60rem;">
    ログ<br>
    <textarea id="log" rows="20" readonly class="textarea has-fixed-size"></textarea>
  </div>
 <script>

//年と月を自動入力
const date = new Date(); 
document.getElementById("year").value = date.getFullYear();
document.getElementById("month").value = date.getMonth()+1;

function sendData(){
  //オリジナルのeventオブジェクトを定義
  function event() {
      this.start;
      this.end;
      this.day;
  }
  /*============入力データを取得============*/
  //start,end,dayというname属性を持つ要素は複数あるので以下3つは配列になります
  const start = document.getElementsByName("start");
  const end = document.getElementsByName("end");
  const day = document.getElementsByName("day");
  //以下3つはただの変数
  const year = document.getElementById("year").value;
  const month = document.getElementById("month").value;
  const title = document.getElementById("title").value;


  /*============取得したデータを配列,オブジェクトに入れる============*/
  let count = 6;///予定の種類数
  let tmp;//forループ内の使い捨て
  let eventList = [];//最終的にサーバーに送信する、"予定の配列"。eventオブジェクトが入る
  //予定の種類分ループ(今回は6個)
  for(let i=0;i<count;i++){
      //eventオブジェクトを新規作成
      tmp = new event();
      //HTMLから取得した文字列をeventオブジェクトに入れる
      tmp.day=day[i].value;
      tmp.end=end[i].value;
      tmp.start=start[i].value;
      //できたeventオブジェクト"tmp"をeventListへ格納
      eventList.push(tmp);
  }

/*完成した配列eventListとyear,month,titleをサーバーへ送る*/
  const callback = message =>{
            if(!message){
                message = "予定は追加されませんでした";
            }
            document.getElementById("log").value = message;
        };
google.script.run.withSuccessHandler(callback).addEvents(title,year,month,eventList);
}
</script>
</body>

</html>

一度動作チェックしてみよう

ここまでで最低限動作する段階になりましたので、実際に動かしてみましょう。
Webアプリとして使用するためにはデプロイという作業を行います。
GASの画面から「公開→Wevアプリケーションとして導入」と進みます
image.png
Project versionはNew、Execute the app as(このWebアプリを誰として実行するか)はMe、Who has access to the app(このWebアプリの公開範囲)はOnly myselfを選択し「Deploy」をクリックします。
Only myselfを選択したので自分のGoogleアカウントでしかこのWebアプリにはアクセスできない状態になります。
image.png
デプロイが完了したというメッセージとともにURLが表示されます。Current web app URLのURLは現在デプロイされて利用可能になっているバージョンのURLです。(完成版もしくは最新安定版のようなイメージ)。その下にあるlatest codeをクリックすると飛ぶURLはまだデプロイする前の最新の状態が常に保たれているバージョンのURLです。(開発版、アルファ版、ベータ版のようなイメージ)。
image.png
これからGASで開発を進めていくときは、
なにかプログラムを編集したら"latest code"のURLに飛んで動作を確認

不具合があれば修正
を繰り返し、きりの良いところまで完成したら
デプロイを行って"Current web app URL"のほうのURLに飛んでWebアプリを使う

また改善したい部分や新しく付けたい機能のアイディアが生まれた

プログラムを編集し"latest code"のURLで確認

修正してデプロイ
というように進めます。
このように開発用の環境(latest codeのほう)と実用の環境(currentのほう)の2つの環境が用意されているので適宜使い分けましょう。

今回は編集して動作確認の段階なので"latest code"のURLをクリックして開きます。もし先程のウィンドウを閉じてしまった場合はデプロイの画面をもう一度開くと"latest code"が表示されるのでそこから飛びましょう。先程デプロイしているのでもう一度する必要はありません。
image.png
アクセスすると作ったHTMLが表示されれば成功です!
もし「スクリプトが完了しましたが何も返されませんでした」と表示される場合はdoGet関数に間違いがないか確認してみましょう。
image.png

実際に入力してみてカレンダーに追加されるかやってみましょう。こんな感じで入力して送信をクリック
image.png
送信をクリックしてすぐには何も起きませんが、数秒後にはログの部分にサーバーから返されてきた追加内容が表示されます。
image.png

Googleカレンダーを開いてみると確かに入力した通りに予定が追加されてましたー!
image.png
これで全て完成になります。

うまくいかない場合

GASで作ったプログラムが何らかのエラーで止まっている場合、見るべき場所が2つあります。1つはブラウザでF12を押して表示される開発者ツールのコンソールタブです。こちらは皆さんご存知でしょう。
2つ目はGASのダッシュボードです。開くにはGASの画面で「表示→ログ」と進み
image.png
Apps Script ダッシュボードをクリックします
image.png
するとこのように何時何分に実行したどの関数が成功したか失敗したかやエラーの内容を確認することができます。
このログには多少のラグがあり、実行されてすぐには出ない場合があるということも覚えておきましょう。またクライアントのJavaScriptに問題があってそもそもGASの関数を実行すらできていない場合は当然こちらのログには表示されません。(ブラウザの開発者ツールを見ましょう。)
image.png

最後にデプロイして完成

最初にデプロイしてから何か編集した場合のみもう一度デプロイが必要になります。うまく動作し何も編集していなければこの手順は不要です。

GASの画面から「公開→Wevアプリケーションとして導入」と進みます。Project versionはnewを選択しそれ以外はそのままで「更新」をクリックします。
image.png
そしたら今度は"Current web app URL"のほうのURLに飛んでみましょう。このURLが完成版になりますので、もし今後実用する場合はここのURLにアクセスすればOKです。ブックマークでもしておきましょう。
image.png

最後までご覧いただきありがとうございました。お疲れ様でした!

3
5
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
5