jQuery MobileでCRUDアプリケーションのフロントを作って学んだ5つのこと(Node.js+jQuery Mobile+MongoDBでCRUDアプリケーションを作る(その2))

はじめに

この記事は、JavaScript Advent Calendar 2011 (フレームワークコース)の16日目の記事です。
Node.js+jQuery Mobile+MongoDBでCRUDアプリケーションを作った際に、フロントを1.0がリリースされたjQuery Mobileを使ったみました。そこで実際にCRUD全部を作ってみて学んだことを5つのポイントにしてまとめました。

前提(基本方針など)

  1. SinglePageApplicationとして作成する。
  2. 基本的にjQuery Mobileの標準機能を使って素直に作る。
  3. ServerSideにはexpress/jadeを使う。
  4. ServerSideはRESTfulなAPIとし、jsonを使う。

jQuery Mobileのリファレンス

本家のドキュメントが一番のリファレンスですが、色々なページに飛ばないと必要な属性が分からないので、2010-10-19 - へっぽこプログラマーの日記が非常に参考になりますので、一読されることをオススメします。

テンプレートエンジン jade について

jadeはexpressがデフォルトで利用するテンプレートエンジンです。通常のHTMLでWebアプリケーションを作る際には、あまりにもプログラマ向けのライブラリなので、ちょっと使えないと思っていたのですが、マークアップ主体のjQuery Mobileとは相性が良いと感じています。インデントベースでタグ記号「<, >」不要、閉じタグ不要なので、記述量が減るのが嬉しいです。
今回のページは、共通部をlayout.jade, body部をindex.jadeに記述しています。実際には次のようになります。

layout.jade
!!! 5
html
  head
    title='Memo'
    meta(name='viewport', content='width=device-width, initial-scale=1')
    link(rel='stylesheet', href='http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.css')
    script(src='http://code.jquery.com/jquery-1.6.4.min.js')
    script(src='http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.js')
  body!= body
layout.jadeのHTMLへの変換後
<!DOCTYPE html>
<html>
  <head>
    <title>Memo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.css">
    <script src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.js"></script>
  </head>
  <body>

  </body>
</html>
index.jade(1ページ分)
div#view(data-role='page')
  div(data-role='header')
    a(data-rel='back', data-icon='back', data-derection='reverse') Back
    h1 Memo View
    a(href='#edit', data-icon='gear', data-transition='flip') Edit

  div(data-role='content')
    div(data-role='filedcontain')
      label(for='memo-view') Memo
      p#memo-view

  div.ui-header(data-role='footer')
    a(href='#add', data-icon='plus') Add
    h4 Kenichiro Murata
    a#del-btn(href='#', data-icon='delete') Delete
  • 閉じタグが不要
  • idはtag#idで表記可能(divの場合はタグを省略できて#idで表記可能だが、分かりやすさ優先で省略しない方針とします)
  • classはtag.classで表記可能
index.jade(1ページ分)のHTMLへの変換後
  <div id="view" data-role="page">
    <div data-role="header">
      <a data-rel="back" data-icon="back" data-derection="reverse">Back</a>
      <h1>Memo View</h1>
      <a href="#edit" data-icon="gear" data-transition="flip">Edit</a>
    </div>

    <div data-role="content">
      <div data-role="filedcontain">
        <label for="memo-view">Memo</label>
        <p id="memo-view">
        </p>
      </div>
    </div>

    <div data-role="footer" class="ui-header">
      <a href="#add" data-icon="plus">Add</a>
      <h4>Kenichiro Murata</h4>
      <a id="del-btn" href="#" data-icon="delete">Delete</a>
    </div>
  </div>

とまぁ、このような感じです。それでは5つのポイントを紹介します。

0. 画面イメージ

node-ninjaで動かしています。

1. footerのボタン配置について

headerは、タイトル以外にa-linkを1つ配置すると左側に、2つ目を配置すると右側に自動でボタンが配置されます。しかし、footerはこのボタン配置のコントロールが少し異なります。header内ではui-btn-rightをclass指定すると位置を制御できますが、footerだと上手く行きません。
複数のボタンを配置するとなると、navbarを使うことになるのですが、ボタン2つまでしか置かないのであれば、headerと同じように配置させたい。そんな時は、footerにui-headerをclass指定すると、headerと同じようにボタンを配置できます。
このTipsは404 Error - Not Foundで紹介されています。

2. ul listview の動的構築

listviewにはServerSideから取得したリストデータを表示します。このアプリでは2種類の方法で実現してみました。

2-1. 初期表示時にjadeのテンプレート機能で初期構築
div#index(data-role='page')
  div(data-role='header')
    h1 Memo

  div(data-role='content')
    ul#memolist(data-role='listview', data-inset='true')
      - for(var index=0; index<memos.length; index++) {
        li
          a(href='#view', id=memos[index]._id)
            p=memos[index].content
      - }

  div.ui-header(data-role='footer')
    a(href='#add', data-icon='plus') Add
    h4 Kenichiro Murata
  • アプリの初期表示画面がlistviewの場合、ServerSideでデータを取得した後、jadeのテンプレート機能を使ってレンダリングし(memosがバインドするリストオブジェクト)、その結果をHTMLとして返します。
  • jadeの組み込み構文?(-for)を使って、ulタグの下にli, aタグを追加しています。
  • aタグのid属性に、バインドしたデータを識別できるようにするためのキーとなるidを埋め込んでいるのが後でポイントになります。
2-2. jsonデータをajaxで取得して動的に構築する
  $(function() {

    $("#index").bind('pageshow', function(e, ui) {
      $.get(
        'memo/list'
        , function(data) {
            $("#memolist").empty();
            for(var index=0; index < data.length; index++) {
              $("#memolist").append('<li><a href="#view" id="' + data[index]._id + '"><p>' + data[index].content + '</p></a></li>');
            }
            $("#memolist").listview('refresh');
          }
        );
    });

  });
  • pageshowイベントは最初に表示するページに対してはイベントが発生しません。(mobileinitで順序に注意して定義すると可能なようですが、今回は試していません)
  • ul listviewの子ノードを削除する際に、remove()を使うとulノード自体も削除されてしまうので、empty()を使います。
  • ulノードが残っていれば、DOMを構築した後に、ulに対してlistview('refresh')を実行することで、リストが再構成されます。

3. listviewで選択された行を特定し、キーとなるidを取得して、ServerSideからデータを取得する

script
  var mstore = {};

  $(function() {
    $("#memolist").delegate('a', 'click', function(e) {
      mstore.selectedid = this.id;
    });

    $("#index").bind('pageshow', function(e, ui) {
      $.get(
        'memo/list'
        , function(data) {
            $("#memolist").empty();
            for(var index=0; index < data.length; index++) {
              $("#memolist").append('<li><a href="#view" id="' + data[index]._id + '"><p>' + data[index].content + '</p></a></li>');
            }
            $("#memolist").listview('refresh');
          }
        );
    });

    $("#view").bind('pageshow', function(e, ui) {
      $("#memo-view").html('');
      $.get(
        'memo/' + mstore.selectedid
        , function(data) {
            $("#memo-view").html(data.content);
          }
        );
    });
  • listviewのaタグのクリックした際に、選択した行からキーとなるidを取得するには、次の手順を踏んでいます。
    • aタグのidにキーとなるidを埋め込んでおく
    • aタグのclickイベントを補足して、選択行のキーとなるidを保存
    • ページ遷移時のpageshowイベントで、保存していたidを使ってajaxによるデータ取得を行う
  • aタグのclickイベントを補足する時に、delegateかliveを使うのがポイント(ここでbindを使うとイベントが補足できてもaタグのidが取得できません)
    • bindはイベント設定時に要素が存在している必要がある
    • live, delegateはイベント設定時に要素が存在している必要がない
    • つまり、今回のように動的にDOMを削除、追加するような場合ではlive, delegateを使わないと、選択したaタグのidが取得できません。その他の違いは0-9, jQueryのliveやdelegateは実際何をやってるのかに詳しく書かれています。

4. ダイアログをscriptから表示させる

div#msg-dialog(data-role='page')
  div(data-role='header')
    h1 Message

  div(data-role='content')
    p#message
    a(href='#index', data-role='button') OK

script
  $(function() {
  
    function onSuccess(data) {
      $("#message").html(data.message);
      $.mobile.changePage('#msg-dialog', {transition : 'slidedown', role : 'dialog'});
    };
  • $.mobile.changePageを使って、roleにdialogを指定します。

5. ajaxをfalseにせずに、jQuery Mobileのイベントプロセスに合わせる

div#edit(data-role='page')
  div(data-role='header')
    a(data-rel='back', data-icon='back', data-derection='reverse') Back
    h1 Memo Edit
    a#save-ebtn(href='#', data-icon='check') Save

  div(data-role='content')
    div(data-role='filedcontain')
      label(for='memo-edit') Memo
      textarea#memo-edit

  div.ui-header(data-role='footer')
    a(href='#add', data-icon='plus') Add
    h4 Kenichiro Murata
    $("#save-abtn").bind('click', function(e) {
      $.post(
        'memo'
        , { content : $("#memo-add").val() }
        , onSuccess
        , 'json'
      );
    });

    function onSuccess(data) {
      $("#message").html(data.message);
      $.mobile.changePage('#msg-dialog', {transition : 'slidedown', role : 'dialog'});
    };

今回はajaxによるページ遷移をfalseにせずに、jQuery Moileに身をゆだねる?ようにしています。上記3.にてclick時には選択行のidだけ保存して、pageshowイベントで処理するなどがそうです。
また、今回の例では保存する、削除するなどのServerSideに処理を要求する場合、次の手順で処理しています。

  • aタグのhrefには#だけを指定しページ遷移しない(a#save-ebtn)
  • aタグのclickイベントを補足し、ServerSideに要求する
  • コールバック内でダイアログを表示し、OKボタンでページ遷移

(追加)

最後に

このアプリケーションのソースは以下で公開しています。