jQuery Mobileと動的なページ生成

jQuery Mobileは、デフォルトでリンクがクリックされた際の動作を乗っ取り、ページを動的にDOMへ挿入するようにしています。あるいは手動で $.mobile.changePage() 関数が呼ばれた際も同様です。これはサーバ側でHTMLを出力する場合には非常に優れていますが、時にはJSONなどで取得したデータをクライアント側でジェネレートしたいような場合もあるでしょう。それは通信量やパフォーマンス的な問題かもしれませんし、連携するサービスのフォーマットによる問題かもしれません。

クライアント側でページのマークアップを作成する必要がある場合、$.mobile.changePage()関数による通知の仕組みについて知っておくことが重要です。それによって、ナビゲーションの流れの適切なタイミングで処理をフックすることが出来るようになります。

changePage()関数の呼び出しで、通常は次のようなイベントが発火されます。

  • pagebeforechange
    • ページの読み込みや、ページ切り替え処理の前に呼ばれます。
    • 以前は “beforechangepage” と呼ばれていました。
  • pagechange
    • ページの読み込みと切り替え処理が完了した後に呼ばれます。
    • 以前は “changepage” と呼ばれていました。
  • pagechangefailed
    • 遷移先ページの読み込みに失敗した際に呼ばれます。

これらの通知はページコンテナ要素( $.mobile.pageContainer )に通知され、それから子要素に伝播していきます。

たとえばJSONやメモリ上に展開されたJSオブジェクトなどを基に、ページを動的に埋め込む場合や、あるいは既存ページのコンテンツを根本的に変更するような場合には、pagebeforechangeイベントを使うのが良いでしょう。このイベントでは、読み込まれるURLやページ要素をフックして解析し、デフォルトの changePage() 関数の挙動を preventDefault() を呼び出すことで肩代わりするようなことが可能です。

全体の流れを見るために、こちらの サンプル を見てください。コードの先頭付近にはカテゴリの生データが記述してあります。今回はサンプルのため、このようにメモリ上にオブジェクトが展開されるようにしていますが、実際には動的に別の場所から取得することになるでしょう。

var categoryData = {
  animals: {
    name: "動物",
    description: "All your favorites from aardvarks to zebras.",
    items: [
      {
        name: "Pets",
      },
      {
        name: "Farm Animals",
      },
      {
        name: "Wild Animals",
      }
    ]
  },
  colors: {
    name: "色",
    description: "Fresh colors from the magic rainbow.",
    items: [
      {
        name: "Blue",
      },
      {
        name: "Green",
      },
      {
        name: "Orange",
      },
      {
        name: "Purple",
      },
      {
        name: "Red",
      },
      {
        name: "Yellow",
      },
      {
        name: "Violet",
      }
    ]
  },
  vehicles: {
    name: "乗り物",
    description: "Everything from cars to planes.",
    items: [
      {
        name: "Cars",
      },
      {
        name: "Planes",
      },
      {
        name: "Construction",
      }
    ]
  }
};

最初のページでは、リンクリストが並べられています。URLはハッシュで、表示したいカテゴリを選ぶようになっています。


内部的には、ユーザがこれらのリンクをクリックすると、フレームワークは $.mobile.changePage() を実行するために、通常のリンク処理を乗っ取ります。そして読み込まれるべきページのURLを解析し、やはりデフォルトの処理に委ねるべきかを判別し、そして(そうすべきであれば) changePage() を実行します。

アプリケーションは、ドキュメントレベルで pageChange() 処理中の “pagebeforechange” イベントに処理を挿し込むことが出来ます。

// changePage()の実行を見張ります。
$(document).bind( "pagebeforechange", function( e, data ) {
  // changePage()がページをURL文字列として受け取った場合のみ
  // 処理を実行します。
  if ( typeof data.toPage === "string" ) {
    // 今回制御するのは、URLにカテゴリ指定が含まれる場合のみです。
    var u = $.mobile.path.parseUrl( data.toPage ),
        re = /^#category-item/;
    if ( u.hash.search(re) !== -1 ) {
      // 処理対象のURL指定であった場合、メモリ上に持っているデータから
      // 指定されたカテゴリのページを構築する処理を行う関数を呼び出します。
      showCategory( u, data.options );

      // changePage()関数に対して、処理を引き受けたためにデフォルトの処理を
      // 実行しないように通知します。
      e.preventDefault();
    }
  }
});

なぜ、ドキュメントレベルで見張るのでしょう?端的に言えば、ディープリンクされた場合のためです。この処理は、jQuery Mobileフレームワークが初期化され、最初のURLが処理される前にアプリケーションに渡されなければなりません。

この “pagebeforechange” イベントにバインドされたコールバック関数には、第二引数として data オブジェクトが渡されます。これは $.mobile.changePage() が呼ばれた際に渡されたものです。このオブジェクトは、次のようなプロパティを持っています。

  • toPage
    • 遷移先となるページのコンテンツとなるjQueryオブジェクト、もしくは読み込まれるべきURL。
  • options

このサンプルでは、changePage()に対して最初にURLが渡された時点でのみ処理を行いたいので、冒頭で toPage の型を確認しています。次に、URLをパース用のユーティリティに渡し、処理対象とすべきURLであるかどうかを確認しています。そうであるならば今回実装した、指定URLを基にカテゴリデータを用いて動的にコンテンツを作成する showCategory() 関数を呼び出します。そして、イベントの preventDefault() を呼んでいます。

pagebeforechangeイベントで preventDefault() を呼んでいるのは、最初の $.mobile.changePage() 関数の呼び出しには、これ以上処理を続行して欲しくないためです。preventDefault()関数を呼ぶことで、jQuery Mobileに対して、自前で changePage() の処理を引き受けることを通知しています。

もし preventDefault() が呼ばれなければ、通常通りの changePage() による処理が続行されます。1点注意が必要なのは、preventDefault()が呼ばれていない場合で、コールバックが引数に渡された data オブジェクトの toPage や options の内容を変更すれば、それがこの後の channgePage() の処理に影響を与えるということです。たとえば、特定のURLを別のページにリダイレクトしたいような場合、コールバックの中で data.toPage プロパティの値を別のURLやページ要素に変えてやることで実装できます。同様に、オプションを追加したり削除することによって changePage() 関数の挙動を変更することが可能です。

ここまでで、changePage() 関数に処理を挿入する方法がわかりました。次に、実際にこのサンプルがどのようにページをマークアップしているのかを見てみましょう。このサンプル上、各カテゴリページは同じページコンテナを使いまわしています。どのリンクがクリックされても、いずれも showCategory() 関数が処理を引き受けます。

// URLを基に指定されたカテゴリに該当するデータを読み込みます。
// データを使ってアクティブなページを構築し、DOMに埋め込む
// 一連の処理です。
function showCategory( urlObj, options )
{
  var categoryName = urlObj.hash.replace( /.*category=/, "" ),

    // 選ばれたカテゴリをURLから抽出します。
    // このデータはデモのため既にメモリ上にあるものから連想配列で
    // 取得していますが、Ajaxなどで取得することも可能です。
    category = categoryData[ categoryName ],

    // コンテンツを表示するためのページコンテナは、既にDOM上に
    // 用意してあります。
    // どのコンテナを使うのかはURL上の'?'より前がIDになるように
    // しているので、それを取得します。
    pageSelector = urlObj.hash.replace( /\?.*$/, "" );

  if ( category ) {
    // ページを埋め込むコンテナオブジェクトを取得します。
    var $page = $( pageSelector ),

    // ページのヘッダを取得します。
    $header = $page.children( ":jqmData(role=header)" ),

    // コンテンツ部分のコンテナを取得します。
    $content = $page.children( ":jqmData(role=content)" ),

    // コンテンツ部分に埋め込むマークアップを作成します。
    markup = "

" + category.description + "

    ", // このカテゴリに該当するアイテム群 cItems = category.items, // アイテム数 numItems = cItems.length; // このカテゴリにあるアイテムを、リストアイテムとして // マークアップに追加していきます。 for ( var i = 0; i < numItems; i++ ) { markup += "
  • " + cItems[i].name + "
  • "; } markup += "
"; // ヘッダからh1要素を見つけ、カテゴリ名を設定します。 $header.find( "h1" ).html( category.name ); // コンテンツ要素に作成したマークアップを埋め込みます。 $content.html( markup ); // ページを、ページとして拡張します。 // リストをマークアップして埋め込み、ページの内容が揃った // ところで、ページウィジェットになるよう page() 関数を // 呼び出しています。 $page.page(); // 埋め込んだリストを、リストビュー化します。 $content.find( ":jqmData(role=listview)" ).listview(); // data-urlの値がIDそのままであることは望ましくありません。 // ブラウザのロケーションバーにカテゴリを含んだ正しいものが // 入るよう、属性値を更新してやります。 //options.dataUrl = urlObj.href; // 更新したページを changePage() 関数に渡してやり、ページの // 切り替えを促します。 $.mobile.changePage( $page, options ); } }

サンプルの中で、URLのハッシュ部分は2種類のパーツに分けられています。

#category-items?category=vehicles

最初のパーツは “?” の前までの部分で、作成したコンテンツが挿入されるページコンテナのIDになっています。次の “?” 以降の部分はページ内容として、どのデータを用いるべきかを表しています。この showCategory() 関数で最初にやっていることは、ハッシュを分解して、コンテンツを挿入するページのIDを取得していることと、処理対象のカテゴリを見てメモリ上にあるJavaScriptオブジェクトからデータを取得することです。次にデータを基にマークアップを作成し、ページのヘッダやコンテンツにそれらを挿入します。それ以前に要素内にあったものは、全て消してしまいます。

マークアップが挿入された後は、今つくったばかりの要素に対して、適切な jQuery Mobileウィジェットにする呼び出しを行ないます。

ここで興味深い問題がおきているのは、通常jQuery MobileがブラウザのURLにあるロケーションハッシュを表示中のページに対応するものに書き換えてしまうことです。今回同じページを各カテゴリで使い回している関係上、これは望ましく有りません。そのため、showCategory()関数内でページオブジェクトの dataUrl プロパティにURLを設定するようにしています。

これが、今回のサンプルの概要です。今回のものは、JavaScriptを非実行にした場合の挙動など、とても良いサンプルとは言い難いかもしれません。Cグレードのブラウザでは、空のページが見えてしまいます。いずれ、そうした環境下でも美しく動作するサンプルを提供するつもりですので こちら の更新を確認しておいてください。