読者です 読者をやめる 読者になる 読者になる

Sencha Touch 2 のよくある画面構築とイベント発火まわりを読む

まあたいしたことしてないです

いままで書いてきた Sencha Touch 2 での MVC というかそういうのの具体的な使い方をもうちょっと噛み砕いてた。ちなみにきしださんの本は Amazon で頼んだばかりなのでまだ届いてないです。
公式ドキュメント::Touch 2.1.1 - Sencha Docs
各種サンプル::Kitchen Sink
connpass アプリ::shimizukawa / sencha-touch2-exercise — Bitbucket

よくあるタイトルヘッダーとナビゲーションフッターがあるような画面をつくる

まあよくある iOS アプリみたいなあれです。下にパネルが5個とか並んでて、あとヘッダーにいろいろボタンがあるとか。
結局どうつくったかというと、やっぱ公式チュートリアルとおり Ext.tabPanel を Main.js にして bottom においておく。その items の中身として Ext.navigation.View でヘッダーを付加した view を定義してやる。具体的なコードを貼ってく。
まあ app.js でとりあえず Ext.Viewport.add(Ext.create('Ext.view.Main')) しておく。で、まずベースとなる Main.js は以下

Ext.define('moge.view.Main', {
  // タブパネルを継承して作成する
  extend: 'Ext.tab.Panel',
  xtype: 'Main',

  config: {
    // 場所は下にする
    tabBarPosition: 'bottom',
    title: 'main',

    // タブパネルの中身を定義してやる
    items: [
           { xtype: 'Second' },
           { xtype: 'Third' }
           ]
  }
});

Ext.defineで定義して extend で Ext.tab.Panel を継承してやる。で、 items に今回は2つの要素をいれてやった。で、 Second.js は以下

Ext.define('moge.view.Second', {
  // 通常の Panel ではなく Ext.navigation.View をつかってやる
  extend: 'Ext.navigation.View',
  xtype: 'Second',

  // 必要な Store と xtype の list を読み込んでやる
  // requires ではモジュール的な書き方で読み込む?
  requires: [
    'moge.store.ContactStore',
    'Ext.dataview.List'
  ],

  config: {
    tabBarPosition: 'bottom',
    title: 'second',
    iconCls: 'add',

    items: [{
             // ここがヘッダーのタイトルになる
             title: 'second title',
             // 今回はリスト
             xtype: 'list',

             styleHtmlContent: true,
             scrollable: true,
             // Store の指定はよくわからないがモジュール的な書き方ではなくもう store を指定しているので
             // こうして ContactStore でいい、という話なのかもしれない
             store: 'ContactStore',
             itemTpl: '<div class="contact"><strong>{firstName}</strong> {lastName}</div>',
             grouped: true,
             indexBar: true
           }]
  }
});

これも Ext.define で定義して extend で Ext.navigation.View を継承してやる。そこの config.title でタイトル指定する。今回は Ext.dataview.List で list 表示してる。そこらへんの話はあとで。で、Third.js は以下

// 解説は上記 Second.js と同じ構造
Ext.define('moge.view.Third', {
  extend: 'Ext.navigation.View',
  xtype: 'Third',

  config: {
    tabBarPosition: 'bottom',
    title: 'third',
    iconCls: 'add',

    items: [{
             title: 'third title',
             items: [
               // こっちはボタンウィジェットにした
               {xtype: 'button', text: 'text!!!', ui: 'round', id: 'tx'}
             ]
           }]
  }
});

まあこれも Second.js と同じように定義してやっている。いまのところこうやって画面をつくってやるのがまあベターな線なのかなぁ。

イベントリスナーや Store の指定をする

正直解説がないととっつきづらいので書き残しておく。どうせ忘れるし。
とりあえず上記のような View がある。で、それに対してまずは model 定義してやろう。Main.js を以下のようにした

Ext.define('moge.model.Contact',{
  // Model を継承して作成
  extend: 'Ext.data.Model',
  config: {
    // とりあえずサンプルにならって2つフィールド定義した
    // string や datetime とか指定したいならそれはそれ
    fields: [
             'firstName',
             'lastName'
            ]
  }
});

kitchensink のサンプルまんまです。で、それに対して Store を定義してやる

Ext.define('moge.store.ContactStore',{
  // Store を継承して作成
  extend: 'Ext.data.Store',

  config:{
    // Model を読み込む
    model: 'moge.model.Contact',
    // kitchensink のサンプルにそってグルーピングとソートをしてやった
    sorters: 'firstName',
    grouper: function(record){
      return record.get('firstName')[0];
    },
    // 今回はてきとうなダミーデータをいれてやった
    // 実際は proxy で ajax か jsonp で通信してやるとおもう
    data: [
      {firstName: 'hoge', lastName: 'moge'},
      {firstName: 'joge', lastName: 'mlge'},
      {firstName: 'moge', lastName: 'mjge'},
      {firstName: 'doge', lastName: 'mage'},
      {firstName: 'doge', lastName: 'msge'},
      {firstName: 'aoge', lastName: 'mdge'}
    ]
  }
});

まあコメントにかいたとおり、Model に対してどう扱うかの Store を定義した。で、上記で貼った View に対してイベントリスナーを貼ってやる Controller を定義してやる

Ext.define('moge.controller.Main',{
  // Controller の作成
  extend: 'Ext.app.Controller',

  config: {

    // セレクターの定義。key に対して ExtJS 特有のセレクターを記述する
    refs: {
      third: '#tx',
      second: 'Second > list'
    },

    // 定義したセレクターに対してイベントリスナーを張る
    // ちなみに refs で定義しなくても直接ここにセレクターを定義しても良い
    control: {
      third: {
        // ボタンウィジェットに対して今回はタップイベントで発火させる
        tap: 'onFire'
      },
      second: {
        // リストビューに対して今回はタップイベントで発火させる
        itemtap: 'onList'
      }
    }
  },

  // イベントリスナーで貼ったときに実際に動かす関数
  // 引数はそれぞれイベントごとに定義されているので公式ドキュメント参照
  onFire: function(el, ev){
    alert(el);
  },
  onList: function(list, index, item, record){
    alert(index);
  }
});

コメントにだいたいかいたけど、セレクターの定義、実際にセレクターをつかったイベントリスナーを張る、そして実際に関数を発火させるという流れになっている。コメントにかいたけど、 refs は定義しないで control に直接セレクターを記述してやっても良い。とはいうが、たぶん分離させて可読性をあげるという意味と、あとなんかほかに設定ができるんだと思う。今回はやらなかった。

セレクターと実際に生成されるウィジェットについて

セレクターとはいうが、だいたい jQuery に似ているけど生の document.querySelectorAll とも似てるけど、ちょっと違う Ext.ComponentQuery というのをつかっている。中身は公式ドキュメント読んで。なので、セレクターとはいうが中身は Ext.ComponentQuery である。だから jQuery とかだと $("input[name='hogemoge']") とかやるけどちょっとかわってくるから気をつけたほうがいいと思う。
で、あと実際に HTML として生成されるものは基本的に div と span である。というのは xtype: 'button' でボタンウィジェットをつくったとしても、実際に生成されるのは

みたいなものなので、当然 $("button") みたいな要素ではトレない。なので上記のように Ext.ComponentQuery でとってやる必要がある。上記のコード Third だと xtype: 'button' に id: "tx" ってのをつけているから #tx でとれる。Second だと document.querySelectorAll みたいに Second > list とセレクターっぽい指定でとっている。
ついでにイベントで取れる要素は debugger 仕込んでやってみる限り tap の場合 element と event でるとか、itemtap の場合 record.data.hoge_property でとれるとかある。まあそれはおいおい自分でデバッグしてみてやれば必要な値は取れる感じ。

つかれた

ちなみにおれはずっとイベントリスナーが発火しなくてイラリング☆してたんだけど、 onFire, onList を config のなかにかいていた。……そりゃうごかないよね。だいぶつかれました。まあでもだいたいよくある画面設計とイベントリスナーは確認できたとは思う。まああとは画面が増えたら組み合わせで泥臭くやるのかなぁと。あとはもうちょっと実践的な画面遷移のサンプルを示せるといいのだけど、どうなることやら。