Rails で JavaScript テンプレートの Hogan.js を使う

はじめに

JavaScript テンプレートには backbone-rails がデフォルトでサポートしている EJS を使っていたけど、 backbone-rails 使うのやめたから EJS をあえて使う理由が無くなった。

自分の観測範囲内だと、JavaScript テンプレートでは Hogan.js と Handlebars の情報が目にすることが多かったので、 Rails で使っている JavaScript テンプレートを Hogan.js で書き換えることにした。

HoganAssets の導入

HoganAssets という gem を使うと、 Rails のアセットパイプラインで Hogan.js のテンプレートをコンパイルできるようになる。

Gemfile に次の行を追加し、bundle でインストールする。

gem "hogan_assets"

config/initializers/hogan_assets.rb を作成し、

HoganAssets::Config.configure do |config|
  config.path_prefix = "app/assets/javascripts"
end

を記述。 上記の設定で、例えばテンプレートの場所が

app/assets/javascripts/backbone/templates/tasks/task.mustache

にあるとき、JavaScript からは

HoganTemplates["backbone/templstes/tasks/task"]

でアクセスできるようになる。

あとは app/assets/javascripts/application.js に次の1行を追加。

//= require hogan.js

これで Hogan.js を使う準備は終了。

テンプレートとビューの修正

Hogan.js のテンプレートは拡張子 .mustache (例:list.mustache)。

<td>
  <input name="selected"
       type="checkbox"
       {{#selected}}checked{{/selected}}/>
</td>
<td>
  {{#completed}}<del>{{/completed}}
  <a href="#/tasks/{{id}}">{{name}}</a>
  {{#completed}}</del>{{/completed}}
</td>
<td>
  <a href="#tasks/{{id}}/edit">
    <i class="icon-pencil"></i>
  </a>
  <a class="delete-task" href="#">
    <i class="icon-remove"></i>
  </a>
</td>

JavaScript/CoffeeScript では、 HoganTemplates というオブジェクトからコンパイルされたテンプレートを取得できる。

# タスクを表示するビューを表します。 
class TaskView extends Backbone.View
  tagName: "tr"
  
  template: HoganTemplates["backbone/templates/tasks/task"]
  
  # ビューを描画します。 
  render: =>
    ctx = @model.toJSON()
    html = @template.render(ctx)
    @$el.html(html)
    return @

まとめ

HoganAssets を導入することで、Rails のアセットパイプラインで Hogan.js のテンプレートをコンパイルできるようになった。 Hogan.js でテンプレートを書き変えている最中だけど、今のところ EJS と比べテンプレートはスッキリしている。

テンプレートにロジックは書くべきじゃないのについ書いてしまう、自分みたいな人間にはうってつけだな。

backbone-rails から Bower+Backbone.js に移行

はじめに

Rails で Backbone.js を使ってアプリを開発するときは、決まって backbone-rails を使っていた。

…んだけど、Backbone.js の v1.0.0 が出たというのに、backbone-rails は未だ対応してない(2013/07/02 現在)。

JavaScript ライブラリを gem でインストールすることに違和感あったし、 この際なんで backbone-rails にサヨナラすることに決めた。

これから JavaScript ライブラリは Bower で管理していく。 以下は移行メモ。

Bower をインストー

あらかじめ Node を homebrew や nodebrew や nvm を使ってインストールしておき、

npm install -g bower

を実行して Bower をグローバルにインストールする。 -g オプションは必須。

Bower を使って JavaScript ライブラリをインストー

Rails プロジェクトのルート(RAILS_ROOT) に .bowerrc と components.json を作成する。

まず .bowerrc。

{
  "directory": "vendor/assets/components",
  "json": "component.json"
}

RAILS_ROOT/vendor/assets/components 下にインストールするように設定している。

次に components.json

{
  "dependencies": {
    "jquery": "latest",
    "jquery-ujs": "latest",
    "underscore": "latest",
    "backbone": "latest"
  }
}

Backbone.js を Railscsrf-token や params に対応させる、rails-backbone の backbone_rails_sync.js が Backbone.js 1.0.0 に対応してなかった。

フォークして修正するより、Backbone.sync のソースコードをもとに実装し直したほうが早そうだったんで、 同じようなものを自作してみた。

(function($) {

var methodMap = {
  'create': 'POST',
  'update': 'PUT',
  'patch':  'PATCH',
  'delete': 'DELETE',
  'read':   'GET'
};

var getUrl = function(object) {
  if (!(object && object.url)) return null;
  return _.isFunction(object.url) ? object.url() : object.url;
};

// Throw an error when a URL is needed, and none is supplied.
var urlError = function() {
  throw new Error('A "url" property or function must be specified');
};

// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
  var type = methodMap[method];

  // Default options, unless specified.
  _.defaults(options || (options = {}), {
    emulateHTTP: Backbone.emulateHTTP,
    emulateJSON: Backbone.emulateJSON
  });

  // Default JSON-request options.
  var params = { type: type, dataType: 'json' };

  // Ensure that we have a URL.
  if (!options.url) {
    params.url = getUrl(model) || urlError();
  }

  // Ensure that we have the appropriate request data.
  if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
    params.contentType = 'application/json';

    var data = {};
    var attrs = options.attrs || model.toJSON(options);
    if (model.paramRoot) {
      data[model.paramRoot] = attrs;
    } else {
      data = attrs;
    }

    params.data = JSON.stringify(data);
  }

  // For older servers, emulate JSON by encoding the request into an HTML-form.
  if (options.emulateJSON) {
    params.contentType = 'application/x-www-form-urlencoded';
    params.data = params.data ? {model: params.data} : {};
  }

  // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
  // And an `X-HTTP-Method-Override` header.
  if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
    params.type = 'POST';
    if (options.emulateJSON) params.data._method = type;
    var beforeSend = options.beforeSend;
    options.beforeSend = function(xhr) {
      xhr.setRequestHeader('X-HTTP-Method-Override', type);
      if (beforeSend) return beforeSend.apply(this, arguments);
    };
  }

  // Don't process data on a non-GET request.
  if (params.type !== 'GET' && !options.emulateJSON) {
    params.processData = false;
  }

  // If we're sending a `PATCH` request, and we're in an old Internet Explorer
  // that still has ActiveX enabled by default, override jQuery to use that
  // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
  if (params.type === 'PATCH' && noXhrPatch) {
    params.xhr = function() {
      return new ActiveXObject("Microsoft.XMLHTTP");
    };
  }

  // Set Rails csrf-token.
  options.beforeSend = _.wrap(options.beforeSend, function(func, xhr) {
    if (!options.noCSRF) {
      var token = $('meta[name="csrf-token"]').attr('content');
      if (token) {
        xhr.setRequestHeader('X-CSRF-Token', token);  
      }
    }
    if (func) {
      return func(this, xhr);
    }
  });

  // Make the request, allowing the user to override any Ajax options.
  var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
  model.trigger('request', model, xhr, options);
  return xhr;
};

})(Backbone.$);

あとは

bower install

を実行すると、ライブラリが RAILS_ROOT/vendor/assets/components 下にインストールされる。

アセットパイプラインで読み込めるように設定する

RAILS_ROOT/config/application.rb を編集し、アセットのパスを追加する。

config.assets.paths << Rails.root.join("vendor", "assets", "components")

app/assets/javascripts/application.js を編集し、利用するライブラリを require する。

//= require jquery/jquery
//= require jquery-ujs/src/rails
//= require underscore/underscore
//= require backbone/backbone
//= require backbone_rails_sync/backbone_rails_sync
//= require_directory .
//= require ./backbone/main

RAILS_ROOT/app/assets/javascripts/backbone を読み込むのは、backbone-rails の慣習。 この下に Backbone.js を使ったコードを置いている。

これで Backbone.js 導入完了

あとは RAILS_ROOT/app/assets/javascripts/backbone ディレクトリの中に、Backbone.js のモデルやビューを実装していく。

まとめ

JavaScript ライブラリの管理を gem から Bower に移行した。 これでライブラリがバージョンアップしたとき素早く対応できる。 今後、新規に Rails アプリを作る場合も、JavaScript ライブラリは Bower で管理する予定。 こうなってくると、Grunt や Yeoman も導入したくなってきた。

シングルページアプリケーションでは Rails で Web API を実装し、 クライアント側は Backbone.js と Grunt や Bower や Yoeman 等を使って実装する、 って感じのクライアントとサーバーが疎結合な構成にしていきたい。

Backbone の View をテストする

以前 Konacha を導入してから、 Rails アプリの JavaScript コードをテストできるようになった。

JavaScript コードの大半は、Backbone を使って実装した UI なんだけど、 困ったことに View のテストをほとんど書けていない。 比較的テストが書きやすい Model は、がっちりテスト書いてある。 なんとか View も単体テストできないものか。

View のテスト方法を模索している最中、Konacha を導入して使えるようになるアサーションライブラリ Chai の、 Chai jQuery というプラグインを知った。

Chai jQuery プラグインを使えば View のテストが書けそうだな。早速試してみた。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Testing Backbone View</title>
    <link rel="stylesheet" href="mocha.css" />
  </head>
  <body>
    <!--mocha のテストランナーが表示される要素-->
    <div id="mocha"></div>

    <!--FormView が使うテンプレート-->
    <script id="form_template" type="text/template">
      <div class="control-group">
        <label for="name">Name</label>
        <div class="controls">
          <input name="name" type="text" value="<%= name %>"/>
        </div>
      </div>
      <div class="control-group">
        <label for="completed">Completed</label>
        <div class="controls">
          <input name="completed" type="checkbox" <% if(completed) { %>checked<% } %>/>
        </div>
      </div>
      <div class="form-actions">
        <input type="submit" class="btn btn-primary" />
      </div>
    </script>

    <!--mocha のセットアップ-->
    <script src="jquery.js"></script>
    <script src="mocha.js"></script>
    <script>mocha.setup('bdd')</script>

    <!--chai と chai プラグインを読み込む-->
    <script src="chai.js"></script>
    <script src="chai-jquery.js"></script>

    <!--テスト対象の View を定義-->
    <script src="underscore.js"></script>
    <script src="backbone.js"></script>
    <script type="text/javascript">
    var Task = Backbone.Model.extend({
      defaults: {
        name: "",
        completed: false
      }
    });

    var FormView = Backbone.View.extend({
      template: _.template($("#form_template").html()),

      initialize: function() {
        this.model.on("sync", this.render, this);
      },

      render: function() {
        var ctx = this.model.toJSON();
        var html = this.template(ctx);
        this.$el.html(html);
        return this;
      }
    });
    </script>

    <!--mocha+chai で Backbone の View のスペックを書く-->
    <script type="text/javascript">
    describe("FormView", function() {
      describe("render", function() {
        var view, model;

        beforeEach(function() {
          model = new Task({ name: "foo", completed: true });
          view = new FormView({ model: model });
        });

        it("自身を返す", function() {
          var result = view.render();
          chai.expect(result).to.eq(view);
        });

        it("DOM を構築する", function() {
          view.render();
          chai.expect(view.$el).to.have("input.btn.btn-primary[type=submit]");
          chai.expect(view.$el).to.have("input[name=name][type=text][value=foo]");
          chai.expect(view.$el).to.have("input:checked[type=checkbox][name=completed]");
        });
      });
    });
    </script>

    <!--mocha のテストランナー実行-->
    <script>
      mocha.checkLeaks();
      mocha.globals(['jQuery']);
      mocha.run();
    </script>
  </body>
</html>

開発中はしょっちゅうテンプレートが変わるので、厳密なチェックはやらずに、セレクタでチェックするくらいでいい。

上記のサンプルでは HTML に直接書いたけど、この方法だと本番用テンプレートを使ってテストできないので、 ビューの中にテンプレートを移動するか、外部ファイルにするべきだったな。

まぁ、Rails アプリなら rails-backbone を使えば、テンプレートは外部ファイルで作成するから問題ないはず。 ただ、いずれは Bower で Backbone 入れたいんだけど、その場合テンプレートどうすればいいんだろうな。

Backbone を使って実装したアプ​リのパフォーマンス改​善には Underscore​ の throttle と debounce が便利

ネタ元はこちら。

自分の場合、Web アプリの UI を開発するときって、たいてい Backbone 使っている。

この Backbone が依存している Underscore には throttle や debounce メソッドがあって、 上記の記事と同じように、メソッドの呼び出し回数を制限できる。

既に導入している Underscore で出来るなら、わざわざ新しいライブラリを導入しなくていいよね。ってわけでサンプル書いてみた。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Debounce Sample</title>
  </head>
  <body>
    <div id="main">
    </div>

    <script id="user_template" type="text/template">
      <button class="update-button">Update</button>
      <div>
        <label>FirstName:</label>
        <%= firstName %>
      </div>
      <div>
        <label>LastName:</label>
        <%= lastName %>
      </div>
      <div>
        <label>RenderCount:</label>
        <%= renderCount %>
      </div>
    </script>
    <script type="text/javascript" src="lib/jquery.js"></script>
    <script type="text/javascript" src="lib/underscore.js"></script>
    <script type="text/javascript" src="lib/backbone.js"></script>
    <script type="text/javascript">
      var User = Backbone.Model.extend({
        defaults: {
          firstName: "",
          lastName: "",
          age: 0
        }
      });

      var UserView = Backbone.View.extend({
        template: _.template($("#user_template").html()),
        el: $("#main"),
        events: {
          "click .update-button": "onUpdate"
        },
 
        initialize: function() {
          this.renderCount = 0;
 
          // firstName と lastName を 10 回更新すると、
          // 合計で 20 回 render が呼ばれてしまう
          //this.model.on("change", this.render, this);
 
          // throttle だと、まず1度描画してから 100ms 待つため、
          // 少しちらついてしまう
          //this.model.on("change", _.throttle(this.render, 100), this);
 
          // debounce だと 100ms 待ってから描画するので
          // ちらつかない
          this.model.on("change", _.debounce(this.render, 100), this);
        },
 
        render: function() {
          var json = this.model.toJSON();
 
          // テンプレートの描画回数も出力する
          json.renderCount = ++this.renderCount;
 
          var html = this.template(json);
          this.$el.html(html);
          return this;
        },
 
        // 2 つのフィールドを10回更新する
        // 何も対策をしないと、change イベントが 20 回発生する
        onUpdate: function() {
          for (var i = 0; i < 10; i++) {
            this.model.set("firstName", "foo" + i);
            this.model.set("lastName", "bar" + i);
          }
        }
      });
 
      $(function() {
        window.main = new UserView({ model: new User() });
        window.main.render();
      });
    </script>
  </body>
</html>

Underscore の方がシンプルに書けて使い勝手が良い。実に自分好みだ。

Backbone.js と jQuery.mmenu を使ったスライドナビゲーション

Backbone でモバイル版の UI を作っていて、 FacebookGmailiPhone アプリみたいな、 スライド表示されるメニューを実装したくなった。

そういったメニューを実装するための jQuery プラグインは既にいくつか存在していて、

あたりを試してみたけど、どれもシックリこない。

そんな中、最近 jQuery.mmenu っていう jQuery プラグインを知った。

触ってみた感じ、自分が求めているものに一番近い。今のところは。

そこで jQuery.mmenu と Backbone の組み合わせに挑戦。 Backbone と jQuery.mmenu を組み合わせたサンプルは次の通り。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>MMenu Backbone Sample</title>
    <link rel="stylesheet" href="mmenu.css" />
    <link rel="stylesheet" href="examples.css" />
  </head>
  <body>
    <div id="main">
      <div id="header">
        <a href="#menu"></a>
        MMenu Backbone
      </div>
      
      <div id="content">
      </div>

      <nav id="menu">
        <ul></ul>
      </nav>
    </div>

    <script id="menu_template" type="text/template">
      <a class="menu-title" href="#/menu/<%= id %>"><%= name %></a>
    </script>
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="jquery.mmenu.js"></script>
    <script type="text/javascript" src="underscore.js"></script>
    <script type="text/javascript" src="backbone.js"></script>
    <script type="text/javascript">
      var Menu = Backbone.Model.extend({
        defaults: {
          id: 0,
          name: ""
        }
      });

      var MenuCollection = Backbone.Collection.extend({
        model: Menu    
      });
      
      var Main = Backbone.Model.extend({
        defaults: {
          selectedMenu: null
        },
        initialize: function() {
          this.menus = new MenuCollection();
        },
        selectedMenu: function() {
          return this.get("selectedMenu");
        },
        selectMenu: function(id) {
          var menu = this.menus.get(id);
          this.set("selectedMenu", menu);
        }
      });
    
      var MenuView = Backbone.View.extend({
        template: _.template($("#menu_template").html()),
        tagName: "li",
        events: {
          "click a.menu-title": "onClick"
        },
        render: function() {
          var json = this.model.toJSON();
          var html = this.template(json);
          this.$el.html(html);
          return this;
        },
        onClick: function(e) {
          // メニューをクリックされたら手動で閉じる
          $("#menu").trigger("close");
        }
      });

      var MainView = Backbone.View.extend({
        el: $("#main"),
        initialize: function() {
          this.model.on("change:selectedMenu", this.renderContent, this);
        },
        render: function() {
          this.renderMenu();
          return this;
        },
        renderMenu: function() {
          var $ul = this.$el.find("#menu>ul");
          $ul.empty();

          this.model.menus.each(function(menu) {
            var view = new MenuView({ model: menu });
            view.render();
            $ul.append(view.el);
          }, this);
        },
        renderContent: function() {
          var $content = this.$el.find("#content");
          $content.empty();

          var menu = this.model.selectedMenu();
          if (!menu) {
            return;
          }

          $content.append(menu.get("name"));
        }
      });

      var MainRouter = Backbone.Router.extend({
        routes: {
          "menu/:id": "showMenu"
        },
        initialize: function() {
          // サンプルメニューを作成
          this.main = new Main();
          for (var i = 0; i < 20; i++) {
            this.main.menus.add({ id: i, name: "Menu" + i });
          }

          this.mainView = new MainView({ model: this.main });
          this.mainView.render();
        },
        showMenu: function(id) {
          this.main.selectMenu(id);
        }
      });

      $(function() {
        window.router = new MainRouter();
        Backbone.history.start();  

        // メニューをクリックして自動で閉じると
        // location.hash が #menu になってしまうので
        // メニューをクリックして自動で閉じないようにしておく。
        $("#menu").mmenu({
             closeOnClick  : false
        });
      });
    </script>
  </body>
</html>

ブラウザで表示してみた。

f:id:griefworker:20130522203007p:plain

左上のボタン?をクリックして、メニューを開いてみる。

f:id:griefworker:20130522203014p:plain

メニューをクリックすると

f:id:griefworker:20130522203023p:plain

メニューが閉じて、選択したメニューの名前が表示された。

jQuery.mmenu の自動開閉を有効にしていると、localtion.hash が #menu になってしまい、Backbone の Router が反応してくれなくて嵌った。使い方はシンプルだから、スンナリ実装できると思っていたんで、予想外の躓きだったな。

Sencha Touch の Model と Store の使い方メモ

Sencha Touch の Model を Store を使って、 Web API からデータを取得する方法をメモしておく。

まず、次のようなモデルを定義したとする。

Ext.define("App.model.Post", {
  extend: "Ext.data.Model",
  config: {
    fields: [
      { name:"name", type:"string" },
      { name:"body", type:"string" }
    ]
  }
});

Sencha Touch のドキュメントでは下のような、Ext.data.Store を使ってデータを取得する方法が載っていた。

var store = Ext.create("Ext.data.Store", {
  model: "App.model.Post",
  proxy: {
    type: "ajax",
    url: "/posts.json",
    reader: {
      type: "json"
    }
  }
});
store.load({
  callback: function(record, operation, success) {
    // データを取得できたときの処理
  }
});

データを取得するのが1回きりなら、これでもいいんだけど、実際はいろんな箇所で呼び出すはず。 カスタム Store を定義して、再利用できるようにしておいたほうがいい。

Ext.define("App.store.Posts", {
  extend: "Ext.data.Store",
  config: {
    model: "App.model.Post",
    proxy: {
      type: "ajax",
      url: "/posts.json",
      reader: {
        type: "json"
      }
    }
  }
});

カスタム Store を使ってデータを取得するコードは、ほんの少しだけ短くなった。

var store = Ext.create("App.store.Posts");
store.load({
  callback: function(record, operation, success) {
    // データを取得できたときの処理
  }
});

書き捨てのサンプルじゃなく、ちゃんとしたアプリ作るときは、カスタム Store を定義するべきだな。

Bootstrap を使ってデザインしたアプリに Sidr でメニューを作成

Twitter Bootstrap はレスポンシブ Web デザインをサポートしているから、 小さい画面ではナビゲーションバーのメニューが隠れて、代わりにボタンが表示される。

このボタンをタップするとメニューがドロップダウン(?)表示されるんだけど、これ好みじゃない。 FacebookGmailiPhone アプリみたいに、横からスライド表示されるメニューにしたい。

そこで、Sidr と Bootstrap を組み合わせて、それっぽいメニューを実装してみた。

この手のメニューを実装するプラグインは Sidr のほかに PageSlide や Snap なんかがあるけど、 試した中では Sidr が一番 Bootstrap と組み合わせやすかった。他のは動かすだけで一苦労。

Bootstrap と Sidr を組み合わせたサンプルがこちら。

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <meta charset="utf-8"/>
    <link rel="stylesheet" href="bootstrap/css/bootstrap.css" />
    <link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.css" />
    <link rel="stylesheet" href="sidr/stylesheets/jquery.sidr.dark.css" />
  </head>
  <body>
    <div class="navbar navbar-inverse navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container-fluid">
          <a id="sidebar_button" href="#" class="btn btn-navbar" style="float:left;">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>

          <a href="#" class="brand">Sample</a>
        </div>
      </div>
    </div>

    <div class="container-fluid">
      <div class="row-fluid">
        <table class="table">
          <tbody>
            <tr>
              <td>
                <input type="checkbox" />
                Task
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <!--これがメニューになる-->
    <div id="sidr">
      <ul>
        <li>
        Lists
        <ul>
          <li><a href="#">list</a></li>
          <li><a href="#">list</a></li>
          <li><a href="#">list</a></li>
          <li><a href="#">list</a></li>
        </ul>
        </li>
        <li>
        Tags
        <ul>
          <li><a href="#">tag</a></li>
          <li><a href="#">tag</a></li>
          <li><a href="#">tag</a></li>
          <li><a href="#">tag</a></li>
        </ul>
        </li>
      </ul>
    </div>

    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="bootstrap/js/bootstrap.js"></script>
    <script type="text/javascript" src="sidr/jquery.sidr.min.js"></script>
    <script type="text/javascript">
      $(function() {
          var $tbody = $("tbody");
          var $template = $tbody.html();
          for (var i = 0; i < 20; i++) {
              $tbody.append($template);
          }
      });

      // サイドバーにする要素を指定しない場合、
      // id に sidr が設定された要素を使う。
      $("#sidebar_button").sidr();
</script>
  </body>
</html>

Bootstrap のナビバーボタンは本来右端に表示されるけど、直接スタイルを指定して左端に表示している。

この HTML を Chrome で表示。

f:id:griefworker:20130423200231p:plain

左上のボタンをクリックすると

f:id:griefworker:20130423200243p:plain

スライドしてメニューが表示された。

ここまでは期待通り。 本格的にアプリに組み込むのはこれからなので、もしかしたらこの先で躓くかもしれない。 その時は Sidr のソースコードを改変するか、おとなしく自作しよう。