Sencha Touch で各ビューのテンプレートを完全に分離させてみる

Sencha Touch とか Sencha ExtJS とかでよく言われるロジックとテンプレートの分離についてのお話です。

ビューとロジックの切り分け

Senchaフレームワークでは、ビュー側の取り回しを良くするための機構として Ext.XTemplate というものを用意してあり、一応切り分け出来るようになっています。

ですが、実際にビュー部分を純粋なコーダーさんやデザイナーさんが作成しようとすると、JSファイルを直に触ったりindex.htmlに各テンプレートを記述することになったりなどちょこっと使い勝手が悪いです。

完全に分離したテンプレート用ファイル

  • デザイナーさんやコーダーさんは純粋なhtmlファイルのみを作成
  • プログラマーが作成されたhtmlファイルを利用して、データバインドさせる

っていう流れがベストですよね・・・?

それっぽいもの作ってみた

Backbone × JST みたいな形に出来るのが一番良い気がしますが、色々面倒だったので、とりあえず分離してみました程度。

Carouselコンポーネントに表示されている各ビューが、外部htmlファイルを読み込んで表示しています。

※ 各ブラウザの動作確認はやってないです :(

ファイル構成(Carousel部分のみ)

app/
├ template/
│    └ panel/
│        ├ Battery.html
│        ├ Camera.html
│        ├ Device.html
│        ├ Flicker.html
│        ├ Map.html
│        └ Toolbar.html
└ view/
    ├ Carousel.js ← 表示しているCarouselクラス
    └ panel/ ← 各テンプレートと紐尽くビュークラス
        ├ Battery.js <-> template/panel/Battery.html
        ├ Camera.js <-> template/panel/Camera.html
        ├ Device.js <-> template/panel/Device.html
        ├ Flicker.js <-> template/panel/Flicker.html
        ├ Map.js <-> template/panel/Map.html
        └ Toolbar.js <-> template/panel/Toolbar.html
index.html ← 上記で表示しているHTML

app/templateディレクトリ配下のhtmlがデザイナーさんたちが作成するビューの静的ファイルになります。

プログラマーは、そのhtmlを使って view/panelディレクトリ配下に紐尽くビュークラスを作っていくイメージです。

template/panel/xxx.htmlview/panel/xxx.jsの中身

参考に Device.html/Device.js を見てみます。

template/panel/Device.html

<h1>Device</h1>
<div class="icon-iphone"></div>

<h2>Description</h2>
<p class="description">端末の情報を表示するためのサンプル</p>

<h2>Demo</h2>
<button id="demo-button" class="demo-button">Execute</button>

<h3>device.name</h3>
<p class="description">{name}</p>
<h3>device.model</h3>
<p class="description">{model}</p>
<h3>device.cordova</h3>
<p class="description">{cordova}</p>
<h3>device.platform</h3>
<p class="description">{platform}</p>
<h3>device.uuid</h3>
<p class="description">{uuid}</p>
<h3>device.version</h3>
<p class="description">{version}</p>

<h2>Code</h2>
<pre>
App.Template.get('template/panel/Cameta.html');
</pre>

こんだけです。
所々 {xxxx} となっている部分は、Ext.XTemplateの書式でデータバインディングを行うための記述方法になります。

view/panel/Device.js

Ext.define('App.view.panel.Device', {
    extend: 'App.view.Template',
    requires: [
        'App.cordova.Echo',
        'App.cordova.Device'
    ],
    xtype: 'panel-device',
    config: {
        templateURL: 'template/panel/Device.html',
        templateDat: {
            name    : 'name data',
            model   : 'model data',
            cordova : 'cordova data',
            platform: 'platform data',
            uuid    : 'uuid data',
            version : 'version data'
        }
    },
    onTemplate: function() {
        var me = this;
        me.callParent(arguments);
        me.setData(App.cordova.Device.get());
    },
    onUpdated: function() {
        var me = this;
        me.element.on({
            delegate: '.demo-button',
            tap: me.onExecute
        });
    },
    onExecute: function() {
        App.cordova.Echo.echo('');
    }
});

cordovaの動作サンプルを作成していた際に作ったのものなので、所々 App.cordova.xxx みたいな記述がありますが、今回の件とは関係ないので無視してください :(

後ほど継承元のクラスの説明をしますが、とりあえず上記のような感じで configプロパティに紐付けたいhtmlファイルへのパスと、そのテンプレート内に埋め込んであるバインドプロパティを指定する感じにしてあります。

このViewクラスを作成するに当たって実装した2つのクラスが次の2つ

  • App.util.Template
    • 設定されたhtmlのパスからhtmlを取得して、テンプレートに反映させる処理を行うクラス
  • App.view.Template
    • 上記クラスを利用する機構を載せた基底クラス

まずは、基底クラス側から見ていきます。

App.view.Template

Ext.define('App.view.Template', {
    extend: 'Ext.Container',
    requires: [
        'App.util.Template'
    ],
    config: {
        baseCls: [
            'app-html-template'
        ],
        templateURL: undefined,
        templateDat: {},
        layout: 'fit',
        scrollable: {
            direction: 'vertical',
            directionLock: true
        }
    },
    updateTemplateURL: function(n, o) {
        var me = this;
        // <debug>
        console.log('update template URL : ', n);
        // </debug>
        me.templateURL = n;
    },
    constructor: function() {
        var me = this;
        me.callParent(arguments);
        me.on('updatedata', me.onUpdated, me, {single: true});
        App.Template.get(me.getTemplateURL(), this);
    },
    onTemplate: function(source) {
        var me = this;
        this.setTpl(source);
        this.setData(me.getTemplateDat());
    },
    onUpdated: Ext.emptyFn
});

基底クラスのコンストラクタで、configプロパティに設定されたパスを利用してテンプレートファイルを取得する処理

App.Template.get(me.getTemplateURL(), this);

を行っております。
テンプレートにデータが反映されたタイミングで何かしら処理を行いたい場合は

me.on('updatedata', me.onUpdated, me, {single: true});

としているので、継承元で onUpdatedメソッドを実装しておけばOKです。

※ single: true としているので、1度だけしか走りません(ここは用途によって要調整)

onTemplateメソッドはこの後説明する App.util.Templateクラスで実行されるメソッドになります。

App.util.Template

続いて、実際にテンプレートファイルを取得する処理です。

Ext.define('App.util.Template', {
    requires: [
        'Ext.Ajax'
    ],
    singleton: true,
    alternateClassName: 'App.Template',
    get: function(path, scope) {
        var me = App.Template;
        // <debug>
        path = Ext.String.format('app/{0}', path);
        // </debug>
        if (!Ext.isEmpty(path)) {
            Ext.Ajax.request({
                method  : 'GET',
                url     : path,
                success : me.onSuccess,
                failure : me.onFailure,
                scope   : scope
            });
        }
    },
    onSuccess: function(response, opt) {
        if (Ext.isFunction(this.onTemplate)) {
            this.onTemplate(response.responseText);
        } else {
            this.setTpl(response.responseText);
        }
    },
    onFailure: function(response, opt) {
        console.log('get html failure', arguments);
    }
});

今回のテンプレートは埋め込みでは無いので、上記実装の通りテンプレート読み込み時にAjaxリクエストが発生しちゃいます ;(

Ajaxでhtmlファイルを取得後、呼び出し元側に onTemplateメソッドが実装されていれば onTemplate にレスポンスを渡し、実装されていなければ呼び出し元のコンポーネントに対して取得したHTMLをテンプレートとしてセットします。

外部htmlファイルをテンプレートとしてセットした後の処理は、onTemplateメソッドが呼ばれますので、あとはその中で setDataメソッドでデータをバインドしてあげればそれっぽく動くと思います。

JS側からイベントを張りたい場合は、 App.view.panel.DeviceonUpdatedメソッド内でやっているように

me.element.on({
    delegate: '.demo-button',
    tap: me.onExecute
});

読み込んだテンプレートのイベントを張りたいクラスなんかを指定してあげればOKです。

最後に

なんかそれっぽく動いてる気がしますが、実際に使うとなるとでかいhtml読み込んでいるときにロードマスクだしたり、そもそも各テンプレート毎にリクエスト飛ばすの嫌等々出てくると思うので、ご参考程度に。