Sencha Touch の NestedList で JSON データを動的にリスト表示

以前、iUI や jQTouch で作成したもの(iUI版は id:fits:20100715, jQTouch版は id:fits:20100731)と同様のサンプルを Sencha Touch で作成してみました。(サーバー側の処理も同じものを使用)

Ext.NestedList を使って実装してみましたが、Ext.NestedList(というより TreeStore)は階層的なデータを一括取得するケースを想定しているようで、今回利用したサーバー側の処理のように階層毎に別の URL からデータを取得するようなケース(スキーマ取得 とテーブル取得でURLとデータスキーマが異なる)での実装はかなり苦労しました。

そのため、今回のようなケース(階層毎に取得先URLやデータのスキーマが異なる)には Ext.List と CardLayout の組み合わせで実装するのが現実的かと思います。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20101005/


ちなみに Sencha Touch 0.96 では、Safari 5.0.2 で NestedList のリストアイテムをダブルクリックすると遷移先の次画面が 2重に繋がってしまう現象が発生してましたが、1.0 では修正されています。
なお、1.0 では Firefox で表示すらできなくなりました。(WebKitPoint が未定義になる模様)

画面構成

画面構成は iUI・jQTouch 版と基本的に同じです。

(1) DBのリスト表示画面

(2) テーブルのリスト表示画面(mysql 選択時)

(3) テーブルの詳細表示画面(columns_priv 選択時)

Ext.NestedList でのリスト表示

まず、JavaScript にハードコーディングした組み込みデータを使って Ext.NestedList でのリスト表示を実装してみます。

注目点は以下の通り。

  • TreeStore を使って階層データを NestedList に設定
  • Ajax でサーバーからデータ取得しなくても TreeStore に proxy の設定が必要
  • 階層データの末端(leaf: true の指定があるデータ)は詳細画面に遷移可能(ただし、詳細画面を作成する getDetailCard 関数を実装する必要あり)
  • getDetailCard 関数の戻り値に recordNode を設定(これが無いとヘッダータイトルと戻るボタン名が正常表示されなかった)

以前試した時に NestedList では Store を使わなかった気がしますが、今では TreeStore を使うようになってます。

なお、リストアイテム毎のアクセサリ表示(右端の " > " の画像)の仕組みが用意されていなかったので、スタイル指定で対応しました。
他に getItemTextTpl を使う手も考えられますが、生成した HTML が span 要素の中に入れられてしまうため、アクセサリ表示の用途には向いていないと思います。

index_static.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Nested List Sample(static data)</title>
<link rel="stylesheet" href="js/resources/css/sencha-touch.css" type="text/css" />
<style type="text/css">
  /* セルのアクセサリ " > " を表示するためのスタイル定義 */
  .x-list-item {
    background-image: url(img/chevron.png);
    background-position: right center;
    background-repeat: no-repeat;
  }
</style>
<script type="text/javascript" src="js/sencha-touch-debug.js"></script>
<script type="text/javascript">
  Ext.setup({
    onReady: function() {
      //画面表示用の階層データ
      var data = {
        items: [
          {title: "cdcol", items: [
            {title: "cds", leaf: true}
          ]},
          {title: "information_schema", items: [
            {title: "CHARACTER_SETS", leaf: true,
              table_name: "CHARACTER_SETS",
              table_type: "SYSTEM VIEW",
              engine: "MEMORY",
              create_time: ""
            },
            ・・・
          ]},
          ・・・
        ]
      };

      //モデル定義
      Ext.regModel("ListItem", {
        fields: [
          {name: "title", type: "string"},
          {name: "table_name", type: "string"},
          {name: "table_type", type: "string"},
          {name: "engine", type: "string"},
          {name: "create_time", type: "string"},
          {name: "items", type: "array"}
        ]
      });

      //実際に ajax の処理をさせなくても
      //proxy 設定(type も必須)が必要な点に注意
      var dbStore = new Ext.data.TreeStore({
        //モデルの指定
        model: "ListItem",
        //データの指定
        root: data,
        proxy: {
          type: "ajax",
          reader: {
            type: "tree",
            root: "items"
          }
        }
      });

      var list = new Ext.NestedList({
        fullscreen: true,
        title: "DB",
        displayField: "title",
        store: dbStore,
        //詳細画面の設定
        getDetailCard: createDetail
      });
    }
  });

  //詳細画面の作成
  function createDetail(record, parentRecord) {
    var rec = record.getRecord();

    return new Ext.Panel({
      //ヘッダータイトルと戻るボタン名を正常表示させるために
      //recordNode の設定が必要
      recordNode: record,
      items: [
        {
          xtype: 'textfield',
          label: 'table_name',
          placeHolder: rec.get("table_name")
        },
        {
          xtype: 'textfield',
          label: 'table_type',
          placeHolder: rec.get("table_type")
        },
        {
          xtype: 'textfield',
          label: 'engine',
          placeHolder: rec.get("engine")
        },
        {
          xtype: 'textfield',
          label: 'create_time',
          placeHolder: rec.get("create_time")
        }
      ]
    });
  }
</script>
</head>
<body>
</body>
</html>

Ext.NestedList での JSON データ動的リスト表示

基本的に前述のサンプルと同じ内容ですが、以下の点が異なっています。

  • TreeStore に url を設定し、サーバーからスキーマ(table_schema)一覧を取得
  • スキーマをクリックした際にサーバーから該当するテーブルの一覧を取得し、TreeStore に動的データ追加(コメントで private 指定のある fillNode を使用)
  • テーブル一覧取得時に同期型の Ajax 処理を行わせるため jQuery を使用

なお、table_schema を画面表示項目に指定したため、テーブルの一覧取得時にも table_schema キーに対してテーブル名を設定するようにしています。(ヘッダーのタイトルや戻りボタン名にも影響するため、階層毎の変更は困難)

また、TreeStore に fillNode する際、元々 TreeStore に指定したモデル(SchemaItem)とは異なるモデル(TableItem)で作成したレコードを追加していますが、この点に関しては一応問題なく動作します。

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Nested List Sample</title>
<link rel="stylesheet" href="js/resources/css/sencha-touch.css" type="text/css" />
<style type="text/css">
  /* セルのアクセサリ " > " を表示するためのスタイル定義 */
  .x-list-item {
    background-image: url(img/chevron.png);
    background-position: right center;
    background-repeat: no-repeat;
  }
</style>
<script type="text/javascript" src="js/jquery-1.4.4.js"></script>
<script type="text/javascript" src="js/sencha-touch-debug.js"></script>
<script type="text/javascript">
  Ext.setup({
    onReady: function() {
      Ext.regModel("SchemaItem", {
        fields: [
          {name: "table_schema", type: "string"}
        ]
      });

      Ext.regModel("TableItem", {
        fields: [
          {name: "table_name", type: "string"},
          {name: "table_type", type: "string"},
          {name: "engine", type: "string"},
          {name: "create_time", type: "string"}
        ]
      });

      var dbStore = new Ext.data.TreeStore({
        model: "SchemaItem",
        proxy: {
          type: "ajax",
          url: "databases",
          reader: {
            type: "tree"
          }
        }
      });

      var list = new Ext.NestedList({
        fullscreen: true,
        title: "DB",
        displayField: "table_schema",
        store: dbStore,
        //テーブル詳細画面の設定
        getDetailCard: createDetail
      });

      //アイテム選択時の処理
      list.on("itemtap", function(subList, subIdx, el, e) {
        var rec = subList.getStore().getAt(subIdx);

        var schema = rec.get("table_schema");

        //table_schema 表示画面の時だけテーブル一覧の取得処理を実行
        if (schema) {
          var req = new jQuery.ajax({
            method: "get",
            url: "tables/" + rec.get("table_schema"),
            async: false
          });

          var jsonObj = eval("(" + req.responseText + ")");

          var records = [];
          var Model = Ext.ModelMgr.getModel("TableItem");

          Ext.each(jsonObj, function(item, index, allItems) {
            item["leaf"] = true;
            //ヘッダータイトルとリスト表示用に設定
            item["table_schema"] = item.table_name;

            var record = new Model(item, item.table_name);
            record.raw = item;
            records.push(record);
          });

          //TreeStore へのデータ追加
          dbStore.fillNode(rec.node, records);
        }
      });
    }
  });

  //詳細画面の作成
  function createDetail(record, parentRecord) {
    ・・・ index_static.html と同じ・・・
  }
</script>
</head>
<body>
</body>
</html>