話題の記事

【前編】Backbone.jsでつくるMVPなUIパターン【スクロールスパイ】

2013.04.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回の更新からだいぶ間が開いてしまいましたがこのシリーズはまだ終わりません!今回はTwitter BootstrapにjQuery Pluginとして実装されているScrollSpyを真似して作ってみたいと思います。
ちょっと解説が長くなってしまったので前後編に分けてご紹介します。

後半はこちら >> Backbone.jsでつくるMVPなUIパターン【スクロールスパイ】後編

デザイン

スクリーンショット 2013-03-27 10.02.03

4つの文章に対しそれぞれナビゲーションが存在し、文章のエリアをスクロールして閲覧している文章に対応するナビゲーションがアクティブ表示になるというものです。

このUIのhtmlは以下のようにコーディングしました

<div id="ui-scroll-spy">
  <div class="scroll-spy-nav navbar">
    <div class="navbar-inner">
      <div class="container">
        <ul class="nav">
          <li class="active">
            <a href="#0">Chapter 1</a>
          </li>
          <li>
            <a href="#1">Chapter 2</a>
          </li>
          <li>
            <a href="#2">Chapter 3</a>
          </li>
          <li>
            <a href="#3">Chapter 4</a>
          </li>
        </ul>
      </div>
    </div>
  </div>

  <div class="well scroll-spy-contents">
    <div class="scroll-spy-content">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi id ipsum...
    </div>
    <div class="scroll-spy-content">
      Nulla facilisi. Mauris eget porttitor erat. Integer hendrerit augue id...
    </div>
    <div class="scroll-spy-content">
      Duis ut massa est, eu scelerisque dolor. Sed a est vitae turpis imperd...
    </div>
    <div class="scroll-spy-content">
      Donec eget scelerisque tellus. Ut augue dui, condimentum in ultricies...
    </div>
  </div>
</div>

Bootstrapのデザインに引っ張られていますが

全体を囲むコンテナ
div#ui-scroll-spy
ナビゲーション
div.scroll-spy-nav
文章を囲むスクロールエリア
div.scroll-spy-contents
各文章
div.scroll-spy-content

と定義しています。

仕様

  • データを元にナビゲーションと文章を描画できる
  • 文章エリアをスクロールすると対応するナビゲーションのアクティブ表示が切り替わる
  • ナビゲーションをクリックすると対応する文章の位置までスクロールされる
  • ハッシュフラグメントから状態を再現できる

今回はデータのCRUD等は行わないのでモデルを使用せず以下のクラスで実装してみようと思います。

  • Backbone.Router
  • Backbone.Events
  • Backbone.View

各コンポーネントとクラスを対応させた設計は以下の様なイメージです。

スクリーンショット 2013-03-28 11.15.07

  1. まずページの表示時に ScrollSpyRouterScrollSpyApp の初期化を実行します。
  2. ScrollSpyAppNavListViewContentListView というUIパーツの集合を扱うクラスを初期化しhtmlの描画とイベントの購読などを行います。
  3. その後ユーザの操作により発生したイベントを NavViewContentView がハンドリングし自身の親クラスに対して通知を行なっていく仕組みです。

それでは各コンポーネントを実装していきます。

NavView

スクリーンショット 2013-03-28 18.51.25

継承

まずはナビゲーションのボタンを担当する NavView クラスから実装していきます。Backbone.Viewクラスを継承します。

var NavView = Backbone.View.extend({});

テンプレート

次にボタンのhtmlテンプレートを定義して、コンパイルした状態で取り込むようにします。※テンプレートはhtml側に記述

<script type="text/template" id="ui-scroll-spy-nav-template">
  <li>
    <a href="#<%= i %>">
      <%= name %>
    </a>
  </li>
</script>
var NavView = Backbone.View.extend({
  navTemplate: _.template($('#ui-scroll-spy-nav-template').html())
});

Underscore.js_.template という関数にテンプレート文字列を与えることで、html文字列を生成してくれる関数にコンパイルしてくれます。

コンストラクタ

次はNavViewオブジェクトの新規生成時に自身のUIをjQueryオブジェクトで生成し、格納する親エレメントのjQueryオブジェクトを受け取るようにしておきます。
Backbone.Viewクラスを継承したクラスでは initialize という関数を定義しておくと新しくオブジェクトを生成した際に自動で実行されるようになります。

  initialize: function(options) {
    this.$el = $(this.navTemplate(this.options.data));
    this.$container = this.options.$nav_container;
  }

描画と状態の切替

さらにUIを描画するための render メソッドを実装します。jQuery.fn.append 関数を利用します。

  render: function() {
    this.$container.append(this.$el);
  }

最後にアクティブ表示を切り替えられるように activate関数と deactivate関数を実装しておきます。これはhtmlのclass属性に active を付け外しすることで切り替えるようにします。

  activate: function() {
    this.$el.addClass('active');
  },
  
  deactivate: function() {
    this.$el.removeClass('active');
  }

コード全体

以上でNavViewの実装は完了です。まとめたコードは以下になります。JsDocも書いてみました!

var NavView = Backbone.View.extend({
  /**
   * NavViewのUIを表示するejsテンプレート
   *
   * @param {Object} data
   * @return {String} dataを組み込んだhtml文字列
   *
   */
  navTemplate: _.template($('#ui-scroll-spy-nav-template').html()),

  /**
   * 新しいNavViewオブジェクトが生成された時に呼び出される
   *
   * @param {Obejct} options
   * @property {jQuery Object} $el
   * @property {jQuery Object} $container
   * @return undefined
   *
   */
  initialize: function(options) {
    this.$el = $(this.navTemplate(this.options.data));
    this.$container = this.options.$nav_container;
  },

  /**
   * NavViewのUIを表示する
   *
   * @property {jQuery Object} $el
   * @return undefined
   *
   */
  render: function() {
    this.$container.append(this.$el);
  },

  /**
   * UIにactiveクラスを与える
   *
   * @return undeifned
   *
   */
  activate: function() {
    this.$el.addClass('active');
  },

  /**
   * UIからactiveクラスを取り除く
   *
   * @return undefined
   *
   */
  deactivate: function() {
    this.$el.removeClass('active');
  }
});

NavListView

スクリーンショット 2013-03-28 18.49.14

継承

次にNavViewの集合を扱う NavListView クラスを実装します。こちらも Backbone.View を継承します。

var NavListView = Backbone.View.extend({});

コンストラクタ

まずは initialize関数を実装してNavListViewオブジェクトの生成時に受け取ったデータを使ってNavViewを生成し、自身の nav_views プロパティに格納しておきます。

  initialize: function(options) {
    this.nav_views = [];
    this.$el = this.options.$container.children('.scroll-spy-nav').find('.nav');

    var self = this;
    _.each(this.options.data, function(_data, i) {
      var options = {
        data: _.extend({ i: i }, _data),
        $nav_container: self.$el
      };

      var navView = new NavView(options);
      self.nav_views.push(navView);
    });
  }

少しコードが長くなってしまいましたが、options.data で受け取った配列を _.each で回し、データのインデックス番号とUIを格納してほしいコンテナを付け加えてNavViewの生成時に渡しています。

描画と状態の切替

次に nav_views に格納されている全NavViewを一気に描画できるように renderAll 関数を実装しておきます。

  renderAll: function(){
    _.each(this.nav_views, function(navView) {
      navView.render();
    });
  }

さらに ScrollSpyApp から指示を受けてNavViewのアクティブ表示を切り替えられるように changeCurrentTo 関数を実装します。
インデックス番号を受付けて該当するNavViewには activate関数を、それ以外には deactivate関数を実行するようにしています。

  changeCurrentTo: function(index) {
    _.each(this.nav_views, function(navView, i) {
      var method = i == index ? 'activate' : 'deactivate';
      navView[method]();
    });
  }

コード全体

以上でNavListViewの実装は完了です。以下が全てをまとめたコードになります。

var NavListView = Backbone.View.extend({
  /**
   * 新しいNavListViewオブジェクトを生成した時に呼ばれる
   * データを渡してNavViewオブジェクトを生成しnav_viewsプロパティに格納しておく
   *
   * @param {Obejct} options
   * @property {Array} nav_views
   * @property {jQuery Obejct} $container
   * @return undefined
   *
   */
  initialize: function(options) {
    this.nav_views = [];
    this.$el = this.options.$container.children('.scroll-spy-nav').find('.nav');

    var self = this;
    _.each(this.options.data, function(_data, i) {
      var options = {
        data: _.extend({ i: i }, _data),
        $nav_container: self.$el
      };

      var navView = new NavView(options);
      self.nav_views.push(navView);
    });
  },

  /**
   * 各NavViewオブジェクトのrender関数を呼び出す
   *
   * @return undefined
   *
   */
  renderAll: function(){
    _.each(this.nav_views, function(navView) {
      navView.render();
    });
  },

  /**
   * 受け取ったindexから該当のNavViewオブジェクトのactivate関数を呼び出し
   * その他のNavViewオブジェクトのdeactivate関数を呼び出す
   *
   * @return undefined
   *
   */
  changeCurrentTo: function(index) {
    _.each(this.nav_views, function(navView, i) {
      var method = i == index ? 'activate' : 'deactivate';
      navView[method]();
    });
  }
});

つづく

前編はここまでです。実装の続きとサンプルは後編で紹介させて頂きます!