HTML出力をする

既に他のページで幾度となく使用してしまい、今更という点がなくもないのですが、それでも、まだまだこのサービスには可能性があり、面白い使い方がありそうなので、研究してみたいと思います。

Google Apps Script超マニュアルでも、主にダイアログボックスとして使用したり、他のAPIサービス、UiAppなどを利用しながら、作成した経緯があります。しかし、このHTML Servicesにはまだまだ他にもウェブアプリケーションとして単体で公開したり、新しいスプレッドシートから使用可能になった「サイドバー」、また、ダイアログボックスでもリッチなダイアログボックスの作成としては、地味に欠かせないサービスとなっています。UiAppよりかは扱いやすい上に、HTMLなので、JavaScriptなどが動かせる、jQueryやCSSも利用可能である等、利点も非常に多いのが特徴です。

但し、1点注意として、このHTMLは完全自由なHTMLというわけではないこと。Cajaと呼ばれるセキュアなHTMLレンダリング環境内で出力をしているので、使えないライブラリがあったり、クロスドメイン対策などでiframeが使えないなどの各種制約があります。

HTML Servicesの基本スタイル

標準的な呼び出しコード

スプレッドシートの場合

function doGet() {
  var output = HtmlService.createTemplateFromFile('overlaplist');
  //return output.evaluate();
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var html = output.evaluate();
  ss.show(html);
}
  • 必ずdoGet()文で作成をする必要性がある。この文でなければ、ウェブアプリケーションとして実行する事が出来ない。
  • スプレッドシートの場合、getActiveSpreadsheetで取得したスプレッドシートに対して、showメソッドを持ってoutput.evaluate()した内容を引数に実行させるとHTML表示がなされる
  • HtmlService.reateTemplateFromFileの引数には、予め用意しておいたHTMLファイルのファイル名を指定する。
  • HTMLファイルは、スクリプトエディタの画面でHTMLファイルとして追加する事ができ、その際に付加したファイル名が青字の部分ということになる。
  • 但し、スプレッドシートの場合、ダイアログとして出力する場合には、showで良いが、ウェブアプリケーションとして公開する場合には、下記のように、returnで返さないと動作しないので注意。

それ以外の場合

function doGet() {
  var output = HtmlService.createTemplateFromFile('overlaplist');
  //return output.evaluate();
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var html = output.evaluate();
  return html;
}

※スプレッドシート以外の場合が大半だと、最後の1行だけが違う。return文で変数に格納したoutput.evaluate()した内容を返すとHTML表示がなされる。

※この方法の場合、実はSpreadsheetApp.getSheetByName()などは使えなくなる。openById()で参照させる必要性があるので注意!!結構嵌るポイントです。

変数の受け渡し

メインのスクリプトからHTML側へ値を受け渡しする方法としては、グローバル変数を使用するのが一般的です。以下のような手順である。

  1. グローバル変数を宣言しておく
  2. スクリプト側で値を入力しておく
  3. HTML側は<? ?>の間にoutput.append(グローバル変数名); といった形で、出力が可能である。HTML文をこの変数に格納している場合には、HTML表示時にレンダリングされて表示される。
  4. 配列などを変数に格納して、グローバル変数として渡し、HTML側に記述したJavaScript内から呼び出せるので、引き続き処理を行う事が出来る。
  5. 逆に、HTML側からそのグローバル変数に対して、値を格納して渡すことも可能である。

また、HTML側からのデータをスクリプト側で用意した関数の引数に渡して、処理を続行させることも可能である。この場合、グローバル変数は必要ではない。

  1. 引数付きの処理を行う関数をスクリプト側に作成する。
  2. HTML側で、例えばボタンクリック時などに発動する関数を用意。そのルーチンの中で、1.の関数の引数に値を渡してあげる
  3. スクリプト側関数などに、引数として値を渡す方法は、google.script.run.関数名(引数1, 引数2);といった形で、スクリプト側関数を実行する方法である。

さらに、HTML側のJavaScriptの中で、GAS側で用意してある変数などを呼び出す場合には、以下の方法を取るのが通常である。

  1. グローバル変数を用意し値を入れておく
  2. そのグローバル変数を返すだけの関数を用意しておく
  3. クライアントAPIにて、2.の関数を呼び出す際に、withSuccessHandlerを設定。そこで設定したルーチンの引数に2.で返した値が入るので、そのままルーチン内で参照できる。もちろん、配列なども渡せる。

参考までに以下のようなコードである。予め、用意しておいた配列データを返し、HTML側でダイアログで1個だけ取り出すというルーチンです。

GAS側コード
function sheetman() {
  var test =  SpreadsheetApp.openById('スプレッドシートのID').getSheetByName("シート名").getRange("A2:C").getValues();
  return test;
}
HTML側コード
<script type="text/javascript">
    function onSuccess(data) {
        alert(data[0][1]);
    }
    google.script.run.withSuccessHandler(onSuccess).sheetman();
</script>

変数の受け渡しその2

スクリプト側で、グローバル変数で宣言し、スクリプト内で動的にデータを生成してHTML側で表示するというのはやってみました。しかし、HTML Servicesなのですが、変数のまま渡すことができません。つまり、変数のまま渡してHTML Services内のJavaScriptで演算をさせたり、また、その後ボタンなどのアクションで更にホスト側のコードを動かした時に、はじめに宣言しデータを格納したはずのグローバル変数にアクセスができなくなっています。その為、データが入っていて表示もできるのに、そのデータを流用して、ボタンを押して何かさせようとすると、再度、コールバック関数の中でデータを生成させるなどという効率最悪のコードを書かなければなりません。

そこで、探してみた所、どうやってグローバル変数のままHTML側に渡し、更にその中のデータそのままに、HTML Services上で生成したボタンによってサーバ処理の時にも参照させるようにする方法というものが見つかりました。この手段があれば、データを上手に再利用することが可能です。

シチュエーション例:データを生成してプレビュー、その後その内容をメール送信

今回のこの項目での事例は、ホスト側でHTMLを作る前にデータを生成しておき、グローバル変数に格納しておきます。このデータを使いまわす事が目的です。

そして、生成後、HTML側のJavaScriptのグローバル変数にその値を格納します。さらに、送信ボタンを押すとそのデータを再度ホスト側にコールバック関数の形で送りつけて、ホストのGASでデータを受け取ったらメールで其の内容を送信するという内容です。ちょうど、プレビュー画面をHTML側で生成して、メール送信でその内容を送る感じですね。

スクリプト側コード

var gamaster = "自分のメアド";
var body = "";
function sendlist() {
  //スプレッドシートデータの取得
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  var ss = sheet.getSheetByName("フィルタ");
  var range = 1;
  
  //rangeの最終行を取得する
  var temprange = ss.getRange("A2:N").getValues();
  var templength = temprange.length;
  
  for(var k = 0;k<templength;k++){
    if(temprange[k][0] == ""){
      break;
    }
    range = range + 1;
  }
  
  //データ件数が0の場合、メッセージを出して終了  
  if(range == 1){
     Browser.msgBox("データの件数が0です"); 
     return 0;
  }
  //データをソートする
  ss.getRange("A2:N" + range).sort;([{column: 4, ascending: false},{column: 6, ascending: true},{column: 14, ascending: false}]); 
  var dataman = ss.getRange("A2:N" + range).getValues();
  var datalength = dataman.length;
  
  //変数を宣言する
  body = "<b>発行対象:</b><br>";
  var firstbuild = "";
  var currentbuild = "";
  var intcnt = 0;
  
  //レコードデータから、必要なレコードのみ取り出しbodyを作成する
  for(var i = 0; i<datalength;i++){
    var tempstatus = dataman[i][13];
    
    //施設名を取得する
    currentbuild = dataman[i][3];
    
    //アカウントタイプを取得する
    var acctype = dataman[i][5];
    
    //送信内容をビルドする
    if(firstbuild == currentbuild){
      
      if(acctype == "個人アドレス"){
        body += "<ul type='disc'>";
        body += "<li>氏名:" + dataman[i][6] + dataman[i][7] + "</li>";
        body += "<li>所属:" + dataman[i][8] + "</li>";
        body += "<li>アカウント名:" + String(dataman[i][9]) + "</li>"
        body += "<li>申請理由:" + dataman[i][10] + "</li>";
        body += "</ul>";
      }else{
        body += "<ul type='disc'>";
        body += "<li>部門名:" + dataman[i][8] + "</li>";
        body += "<li>アカウント名:" + String(dataman[i][9]) + "</li>"
        body += "<li>申請理由:" + dataman[i][10] + "</li>";        
        body += "</ul>";
      }
      
    }else{
      firstbuild = currentbuild;
      body += "<h4>" + firstbuild + "</h4>";
      
      if(acctype == "個人アドレス"){
        body += "<ul type='disc'>";
        body += "<li>氏名:" + dataman[i][6] + dataman[i][7] + "</li>";
        body += "<li>所属:" + dataman[i][8] + "</li>";
        body += "<li>アカウント名:" + String(dataman[i][9]) + "</li>"
        body += "<li>申請理由:" + dataman[i][10] + "</li>";
        body += "</ul>";
      }else{
        body += "<ul type='disc'>";
        body += "<li>部門名:" + dataman[i][8] + "</li>";
        body += "<li>アカウント名:" + String(dataman[i][9]) + "</li>"
        body += "<li>申請理由:" + dataman[i][10] + "</li>";        
        body += "</ul>";
      }
    }
  }
    var data  = {text:body};  
  
    var output = HtmlService.createTemplateFromFile('dialog').evaluate().getContent();
  var html =  HtmlService.createTemplate(output + "<script>\n" + "doIt( " + JSON.stringify(data) + ");\n</script>").evaluate();
  sheet.show(html);    //メッセージボックスとしてを表示する
}
//HTML側から実行させるコールバック関数
function telepon(test){
  var ui = SpreadsheetApp.getUi();
  
  //HTMLメール本文を作成
  var header = "適当な文章<br>"
             + "適当な文章その2<br><hr>";
  
  test = header + test
  
  //メール送信
  MailApp.sendEmail({
    to: gamaster,
    subject: "アカウントの申請",
    htmlBody: test,
    cc: cc,
    name:"アカウントの申請",
  });
  
  ui.alert("送信されました。");
}

HTML側コード

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<html>
<head>
  <meta http-equiv="Content-Type"
  content="text/html; charset=utf-8">
  <title>送信内容</title>
  <style>
    h1 {font-size:16pt; background:#AAFFAA;padding:5px;}
    p {margin:10px;}
    .kousin {

overflow-y: auto;

width:98%; height:200px;

padding:0px;

border:0px dotted #ffffff;

color:#000000;

background-color:#ffffff;

line-height:1.5em;

margin-left: auto;

margin-right: auto;

    }
  </style>
  <script>
    var tempra = "";
    function doIt(data) {
      tempra = data.text;
    }
    function serverFunc(){
      //host側に承認プロセスを渡す
      google.script.run.telepon(tempra);
    }
  </script> 
</head>
<body>
  <h1>重複している項目一覧</h1>
  <div class="kousin">
  <?
    output.append(body);
  ?>
  </div>
<div align="center">
<input type="button" class="action" value="送信する"
  onclick="serverFunc()" />
<input type="button" class="create" value="キャンセル"
  onclick="google.script.host.close()" />
</div>
</body>
</html>

実行結果

図:スプレッドシートのデータを整形してダイアログとして表示させてみた

解説

今回のトリッキーな方法は、このサイトに記載されていた方法で、ポイントは、HTML生成時に特別な方法で、データをプッシュすることにあります。以下にそのポイントを列挙

  1. 一旦通常通り、HTMLをevaluateさせて、その内容をgetContentで取得しておく
  2. body変数に格納しておいたHTMLデータを{text: body}というJSONの記述で、data変数に格納しておく。
  3. 更に、1.で取得した内容に加える形で、doIt関数として<script>文字列のを組み立てて、関数の内容はJSON.stringify(data)と言った形で、JSONを取得するコードを書いておき、これを再度evaluateさせて表示させる。表示された時にこのdoIt関数は自動的に実行される。
  4. HTML側の<script>のセクションには、グローバル変数であるtempraを宣言しておく
  5. スクリプトとして、3.の内容に沿うようにdoIt関数を記述しておき、tempra変数にJSON化されたデータを格納しておく(data.text)。
  6. ボタンを押すと発動するserverFunc関数を用意し、スクリプト側のtelepon関数を実行するように記述しておく(google.script.run.telepon)。引数には変数tempraを指定しておく。
  7. スクリプト側telepon関数は、引数の値をメール送信メソッドのhtmlbodyの引数にそのまま渡してあげる。
  8. 無事にメールが飛ぶ

こんな仕組みです。この方法ですと、ややこしいように見えても、確実にスクリプト側とHTML側とで変数レベルでやり取りが出来るので、オススメです。

コントロールの値を取得してみる

これが、結構ハマりました。JavaScriptが自由気ままに使えるかと思っていたのですが、例えばチェックボックスを通常のdocument.formid.controlid.checkedで取れるかと思ったらダメでした。全くコードが動きません。これもCajaの制限の一つなのかもしれません。この辺りのフォームコントロールの値の取得関係に関してはドキュメントが例の如くなかったので、StackOverFlowを漁ってみました。

なぜ使えないのかはわかりませんが、以下の方法では取得ができたので、この方法を応用して様々な値を取得することが出来れば、セッティング用ダイアログとかをバーンと用意して、HTML出力でユーザに設定してもらうなんてことが出来るかもしれません。

//HTML側コード
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<div>
<script>
function serverFunc(){
  var wahahanode=document.getElementById("bike").checked;
  var sampleNode=document.getElementById("result");
  sampleNode.innerHTML="設定は" + wahahanode + "保存されたよ";
  }
</script> 
<form name="berry">
   <input type="checkbox" name="theFile" id="bike" checked="True">自動的に請求日付を取得する<p>
   <input type="button" class="action" value="設定する" onClick="serverFunc()">
</form>
   <p id="result"></p>
</div>

要するに、getElementByIdで指定して、その状態をcheckedで取得すれば良いというだけの話でした。

後は、取得した値を例えばgoogle.script.runでGAS側コードにでも渡して上げて、スクリプトプロパティにでも保存するコードを掛けば、設定用ダイアログが作れるという寸法です。逆に、スクリプトプロパティに保存してある値は、スクリプトレットで直接グローバル変数を呼び出して上げれば、checkedに値をぶちこんで表示させることができるんじゃないかなと思います(やってないけど)。

ちなみに、これで表示をさせるとこんな感じ。

図:チェックボックスタイプのセッティングダイアログを表示させてみた。

ちなみに、自分はチェックボックスの値の読み書きは以下のような感じで行っている。

GAS側コード
var seikyuman = ""; //グローバル変数
function checkman() {
  var Properties = PropertiesService.getScriptProperties();
  seikyuman = Properties.getProperty("seikyu");
  var html = HtmlService.createTemplateFromFile('autocheck');
  var output = html.evaluate().setWidth(400).setHeight(200);
  SpreadsheetApp.getUi().showModalDialog(output, '請求日付の自動取得');
}
function setseikyu(parameter) {
   var ui = SpreadsheetApp.getUi();
   var Properties = PropertiesService.getScriptProperties();
   Properties.setProperty("seikyu", parameter)
   
   ui.alert("請求日付の自動取得の設定を保存しました。")
}
function dango(){
  var Properties = PropertiesService.getScriptProperties();
  seikyuman = Properties.getProperty("seikyu");
  var temp;
  
  if(seikyuman == "true"){
    temp = "checked";
  }else{
    temp = "";
  }
  
  Logger.log(temp);
  return temp;
}
HTML側コード(autocheck)
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<div>
<script>
  function serverFunc(e){
    var wahahanode=document.getElementById("bike").checked;
    google.script.run.setseikyu(wahahanode);
  }
</script> 
<hr>
請求日付の自動取得にチェックを入れて保存すると、インプットダイアログではなくCSVファイルから請求日付を取得して取り込みます。
任意で取得日を入れたい場合には、チェックを外して下さい。
<p>
<form name="berry">
   <input type="checkbox" name="theFile" id="bike" <?= dango(); ?>><b>自動的に請求日付を取得する</b><p>
   <input type="button" class="action" value="設定する" onClick="serverFunc()">
</form>

CSSで装飾してみる

といっても、特別なことは特にありません。HTML側はほぼ自由に作成が可能になっていますから、参照すべきCSS要素が書かれてさえいれば問題ありません。外部CSSの場合には参照方法に気を付けてください。https://でないとブラウザ側で弾かれるようになっているので。また、Googleからはスクリプト内で使用する用のCSSを公開しています。これはGoogle Appsの各アプリケーションでも用いられている標準的なCSSパッケージで、使い方も以下のように1行をHTML側の頭に入れて、対象のボタン等にclass idを加えて上げるだけです。

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">

ボタンの側は、

<button class="action" onclick='関数名()'>選択画面を開く</button>

といった具合。

このCSSは、ボタンだけじゃなく、HTML Servicesで使用することのできる「文字列、チェックボックス、ラジオボタン、プルダウンメニュー、テキストボックス、テキストフィールド」が用意されていますので、このようなUIを利用するときには、適用しておいたほうが、デザイン的にも良いでしょう。試しに、cssファイルを作って(ここのをお借りしました。単体のcssファイルとして作成しております)、Dropboxの公開フォルダを参照させるようにし、ボタンにclass idを付け加えてみました。すると、以下のスクリーンショットのようになります。

図:ボタンに自作のCSSを当ててみるとこうなる(右:google版、左:自作)

コードは、以下のようになっています。

CSS参照コード

<link rel="stylesheet" href="ここにDropboxの公開リンクのURLを貼り付けてあります">

ボタンにclass(今回は、button8を指定。cssファイルの中にあります)を指定

<button class="button8" onclick='getOAuthToken()'>選択画面を開く</button>

このように、CSSでかなりインターフェースのカスタマイズが出来る上に、外部CSSもきちんと参照が出来ますから、スプレッドシートのダイアログや単体のウェブサービスでGoogle Apps Scriptを用いる場合には、CSSが出来るようになっていると非常にお得です。ひょっとしたらCSSで色々動かしたり、Ajax!?的な動作も可能なのかもしれません。

スクリプトレットとHTML側JavaScript

スクリプトレットとは、HTMLファイル内でJavaScriptではなく、Google Apps Scriptを実行させることのできる特殊な機能で、グローバル変数の参照に始まって、ローカル変数の参照から、Google Apps Scriptの各種APIをそのまま実行させることもできる非常に便利な機能です。この機能を併用することで、予め処理をスクリプトレット内で行わせて、結果を表示させてといったようなことが出来るようになります。但し、スクリプトレット内で宣言した変数はスクリプトレット内でしか使用することが出来ません。当然、外側のJavaScriptで呼び出すことも出来ないので、必ず値の受け渡しは、グローバル変数や予め定義させておいた関数経由で行わせるようにしましょう。

簡単なソースコード

<html>
<head>
  <meta http-equiv="Content-Type"
  content="text/html; charset=utf-8">
  <title>重複分析結果</title>
  <style>
    h1 {font-size:16pt; background:#AAFFAA;padding:5px;}
    p {margin:10px;}
  </style>
</head>
<body>
  <h1>重複している項目一覧</h1>
  <p>
<?
  
  output.append(viewovlap); 
  
  ?>
</p>
<div align="center">
<input type="button" value="メッセージを閉じる"
  onclick="google.script.host.close()" />
</div>
</body>
</html>

上記のコードは、予めGoogle Apps Script側でグローバル変数であるviewovlapに対して様々なHTML文を格納させておいてあり、それをレンダリングさせてるだけのものです。output.append("変数名")にてHTML表示させています。非常によく使う方法なので、覚えておくと良いでしょう。また、スクリプトレットは、<??>タグの間に記述します。この間は、Google Apps Scriptで記述が可能になります。当然、GAS特有のAPIが利用できますので、この中で処理を行わせることもできます。

また、一部分だけ使いたい場合には、<?= ?>といったような記述を使うことも可能です。この場合、1行しか記述できません。が、あまり使う機会はないと思います。

変わった利用方法

このスクリプトレットは、GAS側の変数や関数、GASで処理を書くなどに使うのが主な目的なのですが、GitHubを漁っていたら変わった使い方をしている人を発見。

ソースコードはリンク先を見ていただくとして、この方の方法はスクリプトレットとして別に用意してあるHTMLを結合させて1つのページを作っています。メインとなるPage.htmlの上下にそれぞれ別に用意してあるHTMLファイルを出力させて結合させているわけです。以下に其の部分のコードだけ。

<?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>

直接、GASでHTML Servicesの出力メソッドを記述し、その出力結果をgetContentで受けてるわけです。これはStylesheetというhtmlファイルを参照しています。この使い方は、ヘッダーとフッターを分けてレンダリングすることが可能であり、デザインの統一などで使えるかもしれません。そして、Page.htmlにはコアとなる部分のみを記述するわけです。

進行中の表示とDOM

出力させたHTML ServicesのHTMLはJavaScriptが使えますが、当然DOMも使えます。自分はまだまだヒヨッコなので、DOMといっても、指定IDのElement内容を書き換える程度しか出来ませんが、これを利用して処理が終わるまでの間の進捗表示などをさせています(単なるアニメーションGIFを表示させてるだけですが)。<div>タグにidを設定し、document.getElementById('対象のID').innerHTMLにて、予め用意しておいたHTML文に差し替えているわけです。以下に該当部分だけを紹介。下記に外部に用意したspinner画像を呼び出し、取り込み中表示に切り替える書き換えをしています。

取り込み実行をしたら、ボタンもその他のパーツも必要ないので、<div>の中をまるごと置き換える、画面遷移的な表示にしています。

JavaScript内で書き換えのメソッド実行部分
      document.getElementById('result').innerHTML =
          '<img border="0" src="https://dl.dropboxusercontent.com/u/3688633/ProgressSpinner.gif">'
          +'<p><b><div style="color:red; font-size:10pt;">現在取り込み中...しばらくそのままお待ち下さい!!</div></b>';
divタグが設定されている部分
<div id="result">
  <button class="action" onclick='getOAuthToken()'>CSVファイルの選択</button>
</div>

図:spinner表示中のダイアログ

クライアントサイドAPI

Google Apps Script特有の妙なコマンドとして、クライアントサイドAPIというものがあります。これは、HTML Servicesで出力されたHTML内で実行させることのできる特殊なAPIで、呼び出されるコードはクライアントサイドで実行されます。2種類あり、出力したHTMLで動的なことを行わせるなら覚えておかないといけないものなので、身につけておきましょう。

google.script.host

極めて基本的な機能だけ提供している機能です。以下のようになっています。

※setHeightやsetWidthは、場面毎に表示させたダイアログのサイズをリサイズしたい場合などに使います。メインの表示と進捗状況表示中とで同じダイアログを共有する場合、リサイズしないと不格好なので、自分も使っています。

google.script.run

こちらは、HTML Service内で独自に作成しておいた関数などを呼び出す為の機能で、GAS側に用意してあるコードを実行させたり、送信ボタンを押した時の挙動などをコントロールするための機能です。少々扱いの難しいものですが、送信ボタンなどのフォームを利用する場合には必須の機能です。

google.script.runの書き方

基本的には、google.script.runに続けて、これらのメソッドをつなげて、ボタンのClickハンドラーなどに指定しておくのが通常の使い方。2番め以降のHandlerなどは、常に独自関数に続けて使われる。以下に記述例を示す。

GAS側コード

function getUnreadEmails() {
  // 'got' instead of 'get' will throw an error.
  return GmailApp.gotInboxUnreadCount();
}
HTML側コード
<script>
  function updateButton() {
    ・・・ここに成功時のルーチンを書く・・・
  }
  function updateButton2(){
・・・ここに失敗時のルーチンを書く・・・
  }
</script>
<input type="button" value="Not Clicked"
    onclick="google.script.run
        .withSuccessHandler(updateButton)

.withFailureHandler(updateButton2)

        .getUnreadEmails()" />

上記のコードを分解してみると、

  1. getUnreadEmails関数を実行する
  2. 実行が成功したら、updateButton関数を実行する
  3. 実行が失敗したら、updateButton2関数を実行する

といったメソッドチェーンになっています。メソッドは自由に組み合わせて使えますが、必ずそれ用にコールバック用の処理は用意が必要です。それが、updateButtonなどの用意しておいたクライアントサイドの関数です。ここに記述するものとして、それぞれ用のダイアログをalertで表示したり、DOMで特定の場所にメッセージを記述したりなどが考えられます。

つまり、このクライアントサイドAPIでは、google.script.run.成功時や失敗時のハンドラー.呼び出したい関数という流れでコードを組みます。もちろん、ハンドラーは失敗時のみとか成功時のみでも構わないわけです。なくても良いですね。

フォームを送信時

formを送信する場合には、GAS側コードにフォームの内容を送って挙げなければいけません。その為、以下のような処理をする必要があります。

GAS側コード
function serverFunc(theForm) {
   var fileBlob = theForm.theFile;         // This is a Blob.
   var adoc = DocsList.createFile(fileBlob);    
   var testman = adoc.getUrl();
   
   return testman;
}
HTML側コード
<div>
<script>
function cliHandler(e){
  var sampleNode=document.getElementById("result");
  sampleNode.innerHTML="ファイルは<a href=" + e + ">ここに</a>アップロードされたよ";
  }
</script> 
<form>
   <input type="file" name="theFile">
   <input type="button"  value="アップロード" id="button" onclick="google.script.run.withSuccessHandler(cliHandler).serverFunc(this.parentNode)">
   <p id="result"></p>
</form>
</div>

違う点は、独自の関数であるserverFuncの引数にthis.parentNodeを指定していること。これは、つまり、formで送る内容を格納しているわけです。

引数を引き受けるGAS側コード内では、この中から、theFileと名付けられたフォーム要素から値を取り出し、その返り値(今回はファイルのURL)をreturnしています。

この時点で関数の実行が成功していれば、cliHandlerはserverFuncからの返り値を受け取り、それをinner.HTMLでそれを表示させている という流れになります。上記のコードの色の流れが頭に入れば、ややこしさは解消するのではないかと思います。

外部ライブラリを使ってみる

HTML Services内では、外部JavaScriptライブラリを呼び出せます。googleがホストしているライブラリの他にも、DropBoxに配置した自前のライブラリも当然使用することが可能です(呼び出す時は、https://で行うこと)。

CSSを読み込むのと同じように、以下の構文を付け加えるだけです。

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

上記のコードは、google CDNのものを呼び出しているので、頭のhttps://は省略しています。

今回は、jQueryを使ってみますが、他にもたくさんのライブラリがホスティングされているので、これによりHTML Servicesの利便性は相当上がっています。しかし、外部であっても使用できるライブラリには一定の制限が掛けられているらしいです。実際に、ウェブ上のサンプルを調査してみました。それが以下のコードです。オリジナルのコードを拝借していますが、バージョンが古いので、一部参照先を書き換えています。下のサンプルは、リサイズをするとアニメーションするjQuery UIの機能を利用したスクリプトです。少々、ロードまで時間が掛かります。

また、外部ライブラリなどを使う場合には、htmlservicesの実行に関して、sandboxモードでなければならないようです。また、実行にあたっては、EMULATEDとNATIVEの2つがあり、後者のほうが実行速度は早いが、より多くのライブラリが動くのは遅いが前者となっているようです。今回は後者でやってみたら動きませんでしたので、EMULATEDを使用しています。

※試しにVisualization APIのライブラリを読み込ませてみましたが、こちらはEMULATEDでも動作しませんでした。

ソースコード(EMULATED)

Appスクリプト側コード
function doGet() {
  return HtmlService.createHtmlOutputFromFile('htmledit')
      .setSandboxMode(HtmlService.SandboxMode.EMULATED);
}
HTML側コード
<link rel="stylesheet" href="https://dl.dropboxusercontent.com/u/3688633/jquery/jquery-ui.css">
<link rel="stylesheet" href="https://dl.dropboxusercontent.com/u/3688633/jquery/jquery-ui.min.css">
<link rel="stylesheet" href="https://dl.dropboxusercontent.com/u/3688633/jquery/jquery-ui.structure.css">
<link rel="stylesheet" href="https://dl.dropboxusercontent.com/u/3688633/jquery/jquery-ui.structure.min.css">
<link rel="stylesheet" href="https://dl.dropboxusercontent.com/u/3688633/jquery/jquery-ui.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.9.1/jquery-ui.min.js"></script>
<script type="text/javascript">
<!--
$(document).ready(function() {
  $("#resizable").resizable({
    animate: true
  });
});
//-->
</script>
<style type="text/css">
body {
  font-size: 10pt;
  margin: 5px;
}
#resizable {
  height: 200px;
  width: 200px;
}
#resizable h3 {
  text-align: center;
}
.ui-resizable-helper {
  border: 1px dotted lightgray;
}
</style>
<p>
下の黒いエリアをリサイズ時にアニメーションを適用します。
</p>
<div id="resizable" class="ui-widget-content">
  <h3 class="ui-widget-header">Animate</h3>
  <p>リサイズ可能</p>
</div>

サンプル

ソースコード(NATIVE)

こちらのサイトにあったコードそのまんまですが・・・・。jQuery UIにてカレンダーを表示しているサンプルです。

GAS側コード
function doGet() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  var html =  HtmlService.createTemplateFromFile('index').evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE).setHeight(550).setWidth(800);
  sheet.show(html)
  
}
HTML側コード(index)
<div class="demo" >
<style type="text/css"> 
.demo { margin: 30px ; color : #AAA ; font-family : arial sans-serif ;font-size : 10pt } 
                            p { color : red ; font-size : 14pt } 
</style>
<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.9.1/themes/cupertino/jquery-ui.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.9.1/jquery-ui.min.js"></script>
click here : <input type="text" name="date" id="datepicker" />
<input type="text" id="alternate" size="30"></p>
<script>
    $( "#datepicker" ).datepicker({
      altField: "#alternate",
      altFormat: "DD, d MM, yy",
      showWeek: true,
      firstDay: 1,
     });
</script>
</div>

サンプル

単体のスクリプトとして使用してみる

Google Apps Scriptは、通常はスプレッドシートなどのバックグラウンドに仕込んで利用するものなのですが、Google Sitesやドライブに単体のスクリプトファイルを作ったり埋め込んだりすることも可能です。Google Sitesへの埋め込みの場合、ドライブに作成した単体のスクリプトの公開URLを参照させたり、Sitesの管理メニューにある「Appスクリプト」から、作成することになりますが、いずれの場合であっても、HTMLサービスを活用することが可能です。この方法がウェブアプリケーションとして使う方法です。

単体のスクリプトとして独立したファイルにしておいたほうがメンテナンス性が良いので、Google Sitesで使用する場合には、管理メニューの中のAppスクリプトを使わないというのもひとつの手です。

使い方

  1. Google Driveに単体のスクリプトファイルもしくは、Google SiteのAppスクリプトにプロジェクトを作成する
  2. スクリプトコードを記述する
  3. スクリプトコードから呼び出すHTMLコードを記述する
  4. メニュー[公開] -> [ウェブアプリケーションとして導入]を選択する
  5. プロジェクトバージョン、実行ユーザ、パーミッションを保存して、更新
  6. 公開URLを取得する
  7. 単体のスクリプトファイルの場合、6.のURLを直接開いたり、Google Siteの埋め込みAppスクリプトの参照URLに入れて上げると見ることが出来る。
  8. Google SiteのAppスクリプトの場合、埋め込みAppのリストにプロジェクト名で出てくる。
  9. パーミッションが適正であれば、見ることが可能になる。

ソースコード

Google Apps Script側コード

function doGet() {
  return HtmlService.createHtmlOutputFromFile('myPage');
}
function serverFunc(theForm) {
   var fileBlob = theForm.theFile;         // This is a Blob.
   var adoc = DocsList.createFile(fileBlob);    
   var testman = adoc.getUrl();
   
   return testman;
}

HTML側コード(myPage.html)

<div>
<script>
function cliHandler(e){
  var sampleNode=document.getElementById("result");
  sampleNode.innerHTML="ファイルは<a href=" + e + ">ここに</a>アップロードされたよ";
  }
</script> 
<form>
   <input type="file" name="theFile">
   <input type="button"  value="アップロード" id="button" onclick="google.script.run.withSuccessHandler(cliHandler).serverFunc(this.parentNode)">
   <p id="result"></p>
</form>
</div>

実行結果(アップロード後)

注意点

  • 実行ユーザとは、そのスクリプトは誰の権限で実行するかを指定するもの。もちろん匿名ユーザの場合、スクリプト作者しか指定できない。
  • パーミッションはファイルの共有パーミッションと同じ。
  • 通常のスクリプトと異なり、必ずdoGetで始まる。
  • 今回のスクリプトは、DocsList.createで特にフォルダを指定していないので、スクリプト実行ユーザとして指定してあるユーザのルートディレクトリにファイルがバンバンアップロードされます。
  • DOMでもってresultノードへアップロード結果を書き換えて表示させています。今回はURLをゲットさせているので、innerHTMLにて整形しハイパーリンクテキストを表示させています。
  • 送信ファイルは変数ではBlob型として取得されているので、スクリプトを記述する際には、改めてBlobの指定は必要ない。
  • [公開]の[ウェブアプリケーションとして導入]は、実は物凄く嵌る項目。実際に公開されているのは、版を保存した時のコードであり、最新版が公開されているわけではない。なので、コードを編集したら、公開し直さないと公開URLでの閲覧画面は変更が掛からないので注意!最新のコードでチェックはあくまでもその時限りのURLなので、更新したつもりが古いままだったなんてことになりかねません。

参考リンク