Closure Library で HTML から UI を生成する
非常に久しぶりの記事投稿となってしまいましたが orz 本日は Closure Library ネタを書いてみようと思います。最近は、 Closure Library もだいぶ知られるようになってきたみたいで、 Closure Library についての日本語のつぶやきもだいぶ増えてきて嬉しい限りです。
Closure Library で UI を構築する場合、もっとも簡単な方法は render メソッドを利用して JavaScript でコンポーネントを配置していく方法です。この方法はプログラマにとってはわかりやすいのですが、デザイナーさんの自由度が下がってしまいます。また、コードで直接 UI を配置するというのは、表示とロジックの分離という面でも問題になるかもしれません。
こうしたことに対処するため、 Closure Library には decorate という別の方法も用意されています。これは HTML 内のタグを適切なコンポーネントに差し替えるというもので、とくにツールバーやメニュー等の表示によく利用されます。しかし、それに限らずアプリケーション全体の UI を decorate で構築することも可能なので、本日は decorate を行う独自コンポーネントの構築なども含めてご紹介します。
話題の Google+ も、デザイナーさんが作成したテンプレートを元に UI コンポーネントを配置しているとのことなので、おそらく decorate を最大限に活用しているはずです(実際にコードを解析したわけではないので、憶測ですが)。多数の UI 要素を持つアプリケーションを開発する際にはとくに便利なので、ぜひ参考にしてください。
decorate メソッドの基礎
decorate メソッドの説明をする前に、まずは通常の(JavaScript のみで行う) UI コンポーネントの生成方法を説明しておきます。以下は簡単なツールバーを表示する例です。
goog.require('goog.dom');
goog.require('goog.ui.Toolbar');
goog.require('goog.ui.ToolbarButton');
goog.require('goog.ui.ToolbarToggleButton');
var toolbar = new goog.ui.Toolbar();
var btn1 = new goog.ui.ToolbarButton('ボタン');
var btn2 = new goog.ui.ToolbarToggleButton('トグルボタン');
toolbar.addChild(btn1);
toolbar.addChild(btn2);
toolbar.render(goog.dom.getElement('toolbar'));
最初に必要なライブラリを goog.require で読み込み、ツールバー本体を作成します。そして、必要なボタンを同様に作成し、子コンポーネントとしてツールバーに追加します。最後に render() メソッドを呼ぶことで必要な DOM 要素がドキュメントに挿入され、 UI が表示されます。
これが decorate を使う方法だと、以下のようになります。
goog.require('goog.dom');
goog.require('goog.ui.Toolbar');
goog.require('goog.ui.ToolbarButton');
goog.require('goog.ui.ToolbarToggleButton');
var toolbar = new goog.ui.Toolbar();
toolbar.decorate(goog.dom.getElement('toolbar'));
単にツールバー本体を作成し、 decorate メソッドを呼ぶだけです。ツールバー内部に表示するボタンは、 HTML に記述します。以下、その記述例です。(ソースはこちら)
ツールバーの要素内にある div の CSS クラス名を見て、適切なボタンに差し替えてくれます。編集可能にしてありますので、ラベルを変更したり、新しいボタンを加えたりしてみてください。「ツールバーを作成する」ボタンをクリックすれば、ツールバーに反映されるはずです。各ボタンの div 要素内にはタグも書けるので、 img タグで画像ボタンにもできます。
どのクラス名でどの UI コンポーネントが表示されるかは Closure Library 本に一覧表を掲載していますので、参考にしていただければ。もっとも、だいたい予想がつくでしょうが :)
ただし、 goog.require で読み込んでいないクラスは、いくら CSS クラス名を指定しても表示されません。上記であれば、 ToolbarButton と ToolbarToggleButton しか読み込んでいないので、その他のコンポーネントは表示できません。また、当然ながら適切な CSS も読み込んでおかないと正しく表示されないので、注意してください。
ボタンクリックの処理
ボタンが配置できたので、それがクリックされた時の処理を記述してみましょう。これにはいろいろな方法がありますが、個人的にオススメなのは、 Toolbar などの親コンポーネントにイベントハンドラを設定し、 getId メソッドで target の ID を取得して処理を振り分ける方法です。
var toolbar = new goog.ui.Toolbar();
toolbar.decorate(goog.dom.getElement('toolbar'));
goog.events.listen(toolbar, goog.ui.Component.EventType.ACTION, onAction);
function onAction(e) {
  var id = e.target.getId();
  switch(id) {
  case 'btn-save':
    // id1 のボタンの処理
    break;
  case 'btn-load':
    // id2 のボタンの処理
    break;
  // ...
  }
}
各ボタンの DIV 要素に id 属性を付けておけば、その値がそのままコンポーネントの ID になります。したがって、 HTML には以下のように記述しておけば良いでしょう。
<div id="toolbar"> <div id="btn-save" class="goog-toolbar-button">保存</div> <div id="btn-load" class="goog-toolbar-button">読込</div> </div>
特筆すべき点は、イベントがコンポーネントの親子関係を辿って最上位のコンポーネントまで通知されることです。階層の深いところにあるボタンなどを、最上位のコンポーネントで一括処理できます。こうすることで処理の見通しが良くなり、ボタンの場所を移動してもコード修正が不要になります。デザイナーさんが自由に試行錯誤してもらえますね :)
さきほどのサンプルでも、クリックされたボタンの ID を alert() で表示するようになっていますので、 id 属性を追加して試してみてください。
独自のコンポーネントで decorate を利用する
goog.ui.Toolbar や goog.ui.Container といったクラスを利用すれば、上記のように decorate で子コンポーネントを配置できます。しかし、ときには独自に作成したコンポーネントで decorate を利用したい場合もあるでしょう。とくに、 Toolbar や Container 内に配置できるのは goog.ui.Control のサブクラスだけなので、それ以外のコンポーネントを配置したい場合は、コンテナとなるコンポーネントを独自に定義しなければなりません。
以下は、 decorate で子コンポーネントを作成する、最も単純なコンポーネントです。
goog.provide('myapp.MyComponent');
goog.require('goog.dom.classes');
goog.require('goog.ui.registry');
goog.require('goog.ui.MyComponent');
goog.require('goog.ui.CustomButton'); // 使用するコンポーネントを require する
/** @constructor */
myapp.MyComponent = function(opt_domHelper) {
  goog.base(this, opt_domHelper);
};
goog.inherits(myapp.MyComponent, goog.ui.Component);
myapp.MyComponent.CSS_CLASS = goog.getCssName('myapp', 'mycomponent');
myapp.MyComponent.prototype.decorateInternal = function(element) {
  goog.base(this, 'decorateInternal', element);
  goog.dom.classes.add(element, myapp.MyComponent.CSS_CLASS);
  var contentEl = this.getContentElement();
  var childEl = dom.getFirstElementChild(contentEl);
  while(childEl) {
    var nextEl = dom.getNextElementSibling(childEl);
    var decorator = goog.ui.registry.getDecorator(childEl);
    if(decorator) {
      decorator.setElementInternal(childEl); // addChild() の前に要素を設定しておく
      this.addChild(decorator);
      decorator.decorate(childEl);
    }
    childEl = nextEl;
  }
};
ポイントは、 decorateInternal で呼び出している goog.ui.registry.getDecorator です。この関数に DOM 要素を渡すと、その CSS クラス名を見て、適切なコンポーネントを作成して返してくれます。あとは addChild で子コンポーネントに登録し、子コンポーネントの decorate を呼び出せば、子コンポーネントが表示されます。
ただし、 goog.require していないクラスは表示できないというのは、前述したとおりです。
独自コンポーネントを decorate で配置できるようにする
せっかくなので、 MyComponent も decorate で他のコンポーネントの中に配置できるようにしてみましょう。これは非常に簡単で、以下のコードを上のソースに加えるだけです。
goog.ui.registry.setDecoratorByClassName(
  myapp.MyComponent.CSS_CLASS,
  function() { return new myapp.MyComponent(); });
第一引数に CSS クラス名、第二引数にファクトリ関数を指定して goog.ui.registry.setDecoratorByClassName を呼び出します。こうしておくと、 getDecorator に指定した CSS クラス名を持つ DOM 要素が渡されたときに、ファクトリ関数が呼ばれて適切なコンポーネントが作成される、という仕組みです。
以上、本日は HTML をもとに UI コンポーネントを作成・配置する decorate の使い方をご紹介しました。デザイナーさんとの共同作業では非常に便利ですし、また個人で開発している場合もメンテナンスが楽になるでしょう。 render と decorate をうまく使い分けて、効率よく開発したいものですね。
詳しくはこちらの記事をどうぞ!

この記事にコメントする