WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

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 をうまく使い分けて、効率よく開発したいものですね。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles