注目の記事

Backbone.jsでつくるMVPなUIパターン【タブ】

2013.02.27

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

今回はBackbone.jsを使ってタブUIを作ってみたいと思います。デザインはTwitter Bootstrapをつかって以下のようにしました。

スクリーンショット 2013-02-27 14.05.06

世界的に名高い企業の名前が並んでいますね。

デザインはhtmlを仮組みしてBootstrapで既にできていることにします。(本題と離れてしまうので…すみません><)
ボタンの部分に active というクラスをつけると選択中のデザインに変わり、
内容の部分におなじく active というクラスをつけると display: block; となり表示される仕組みです。

すべてactiveな状態

ui-tab-all-active

すべて非activeな状態

ui-tab-all-deactive

htmlはこちらです

<div id='ui-tab'>
  <ul class='nav nav-tabs'>
    <li class="active">
      <a href="#apple">Apple</a>
    </li>
    <li>
      <a href="#google">Google</a>
    </li>
    <li>
      <a href="#classmethod">Classmethod</a>
    </li>
  </ul>
  <div class='tab-contents'>
    <div class="tab-content active">
      ... apple 文章 ...
    </div>
    <div class="tab-content">
      ... google 文章 ...
    </div>
    <div class="tab-content">
      ... classmethod 文章 ...
    </div>
  </div>
</div>

仕様

  • データを元に一度だけ描画出来る
  • URLのハッシュ値からアクションを起こしタブを切り替えられる
  • タブのボタンを押すとURLのハッシュ値を変更出来る
  • タブに該当しないハッシュ値は無視する

また今回はデータの永続性やビジネスロジックは無くていいのでBackbone.Modelクラスは使わないことにします。なので

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

を使用していきたいと思います。実装のイメージは以下のようになります。

ui-tab-components

TabRouterはURLのハッシュ値が切り替わったイベントに反応してTabAppに指示を出します。TabAppは受け取った指示を元にタブ切り替えのイベントを発火し、TabViewがそのイベントに反応してタブ切り替えを実行する仕組みです。

※ちなみにjQueryも使用していきます。

TabView

さてデザインを確認して頂きましたので、ここからコーディングに入ります。まずはBackbone.Viewを継承したTabViewというクラスを作りましょう。

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

これでBackbone.Viewに実装されている機能を継承した TabView が出来ました。
ここにテンプレートを取り込むコードを追加してみます。

var TabView = Backbone.View.extend({
  btnTemplate: _.template($('#ui-tab-btn-template').html()),
  contentTemplate: _.template($('#ui-tab-content-template').html())
});

タブボタンのテンプレート(html内に記述)

<script type="text/template" id="ui-tab-btn-template">
  <li>
    <a href="#<%= name %>">
      <%= name[0].toUpperCase() + name.slice(1) %>
    </a>
  </li>
<script>

タブ内容のテンプレート(html内に記述)

<script type="text/template" id="ui-tab-content-template">
  <div class="tab-content">
    <%= content %>
  </div>
<script>

Underscore.jsのメソッド template を使って各テンプレート文字列からhtmlを生成できる関数へコンパイルしています。

次にこれらの要素を追加するコンテナをインスタンス作成時に取得しておくようにします。また外からどのタブを担当しているのかわかるように name というプロパティもセットしておきます。

  initialize: function(options) {
    this.$container = $('#ui-tab');
    this.$btn_container = this.$container.find('.nav-tabs');
    this.$contents_container = this.$container.find('.tab-contents');
    this.name = this.options.name;
  }

html側はテンプレート化した部分を削除しておきました。

<div id='ui-tab'>
  <ul class='nav nav-tabs'>
  </ul>
  <div class='tab-contents'>
  </div>
</div>

render メソッドを実装してデータを元に描画できるようにします。

  render: function() {
    this.$btn = $(this.btnTemplate(this.options.data));
    this.$content = $(this.contentTemplate(this.options.data));
    this.$btn_container.append(this.$btn);
    this.$contents_container.append(this.$content);
  }

用意しておいたテンプレート関数にdataを通してhtmlの文字列を作成したら、そのままjQueryに渡してDOMを構築しています。
最後にinitialize関数で取得したコンテナにappendしたのでこれを実行すればブラウザに表示されるはずです。

UIをactive、deactiveにできるようにします。

  activate: function() {
    this.$btn.addClass('active');
    this.$content.addClass('active');
  },

  deactivate: function() {
    this.$btn.removeClass('active');
    this.$content.removeClass('active');
  }

単純にactiveというクラスをつけたり外したりするだけです。
以上でTabViewの実装は終わりです。全てをまとめたコードはこちらです。

var TabView = Backbone.View.extend({
  btnTemplate: _.template($('#ui-tab-btn-template').html()),
  contentTemplate: _.template($('#ui-tab-content-template').html()),

  initialize: function(options) {
    this.$container = $('#ui-tab');
    this.$btn_container = this.$container.find('.nav-tabs');
    this.$contents_container = this.$container.find('.tab-contents');
    this.name = this.options.name;
  },
  
  render: function() {
    this.$btn = $(this.btnTemplate(this.options.data));
    this.$content = $(this.contentTemplate(this.options.data));
    this.$btn_container.append(this.$btn);
    this.$contents_container.append(this.$content);
  }
  
  activate: function() {
    this.$btn.addClass('active');
    this.$content.addClass('active');
  },

  deactivate: function() {
    this.$btn.removeClass('active');
    this.$content.removeClass('active');
  }
});

TabApp

TabAppはアプリケーションの振る舞いを定義する部分です。Routerからデータや指示を受け取りViewを操作したり変更を通知するのが役目です。
作成にはTabViewと違って Backbone.Events を Underscore.jsの extend を使って継承します。これはBackbone.Eventsが厳密にはクラスではなくイベント系の関数を内包したモジュールとして定義されているためです。

var TabApp = _.extend({}, Backbone.Events);

それではここに初期化メソッドを追加しておきます。

  views: [],
  tab_names: [],
  
  initialize: function(data) {
    var self = this;
    _.each(data, function(_data) {
      var tabView = new TabView({ data: _data });

      tabView.listenTo(self, 'change', function(name) {
        if (tabView.name === name) {
          tabView.activate();
        } else {
          tabView.deactivate();
        }
      });
      
      self.views.push(tabView);
      self.tab_names.push(_data.name);
    });
    
    return this;
  }

まずはタブ生成のためのデータを受け取るようになっています。各データ毎にTabViewのインスタンスを生成して自身の views という配列プロパティに格納しておきます。
その間にある部分ですが、これはTabApp自身が change というイベントを発生させた時に各TabViewがactiveになるべきか非activeになるべきかを判断して表示を切り替えられるようにしてあります。
また、存在するタブの名前を調べやすくするために tab_names という配列も用意しておきました。
最後に自分自身をreturnしておきます。

TabViewのインスタンスが用意できたらページの表示時に一度だけ全てのタブを描画する関数を実装しておきます。

  wasRendered: false,

  renderAll: function(callback) {
    if (!this.wasRendered) {
      this.wasRendered = true;

      _.each(this.views, function(view) {
        view.render();
      });

      if (callback) callback();
    }
  }

一度だけしか実行してほしくないので wasRendered というフラグを用意しておきました。後は全てのviewのrenderを呼び出すだけですが、callbackも受け付けるようにしておきました。

次にタブボタンをクリックした時にタブを切り替えるイベントを発生させる関数を実装します。

  changeTo: function(name) {
    if (_.indexOf(this.tab_names, name) >= 0) {
      this.trigger('change', name);
    }
  }

切り替えたいタブ名を受付けて tab_names に入っているものであれば change イベントを発生させるようにしました。またリスナー関数にはタブ名が渡るように第二引数に入れてあります。

以上でTabAppの実装は完了です。全てをまとめたコードはこちらです。

var TabApp = _.extend(Backbone.Events, {
  views: [],
  tab_names: [],
  wasRendered: false,
  
  initialize: function(data) {
    var self = this;
    _.each(data, function(_data) {
      var tabView = new TabView({ data: _data });

      tabView.listenTo(this, 'change', function(name) {
        if (tabView.name === name) {
          tabView.activate();
        } else {
          tabView.deactivate();
        }
      });
      
      self.views.push(tabView);
      self.tab_names.push(_data.name);
    });
    
    return this;
  },

  renderAll: function(callback) {
    if (!this.wasRendered) {
      this.wasRendered = true;

      _.each(this.views, function(view) {
        view.render();
      });

      if (callback) callback();
    }
  },
 
  changeTo: function(name) {
    if (_.indexOf(this.tab_names, name) >= 0) {
      this.trigger('change', name);
    }
  }
});

TabRouter

TabRouterはURLのハッシュ値の変更に反応して、TabAppに指示を出すのが役目です。TabViewと同じように Backbone.router を継承して作成します。

var TabRouter = Backbone.Router.extend({});

まず最初にデータを受け取りTabAppを初期化する関数を実装します。

  initialize: function(options) {
    this.options = options;
    this.app = TabApp.initialize(this.options.data);
  }

こうしておけばデータを元に生成したTabViewを内包したTabAppを使えるようになります。
次は route を定義してタブの変更を受け取れるようにして、受け取ったタブ名を使って切り替えるアクションも実装しておきます。

  routes: {
    '*tab': 'defaultAction'
  },
  
  defaultAction: function(tab) {
    var self = this;
    this.app.renderAll(function() {
      self.navigate(self.options.data[0].name, { trigger: true });
    });

    if (tab.length) {
      this.app.changeTo(tab);
    }
  }

routesには全タブ名は書かず *tab という形にしてとりあえずなんでも受け付けるようにしました。紐付けているアクションは defaultAction 関数です。
ここではページ表示時にタブを描画するための処理が走るようになっています。TabApp側で一回しか実行しないようにしてあるのでif文でかこまなくても大丈夫です。
また navigate 関数を使って初回時のみデータの先頭のタブが開かれるようにしています。オプションのtriggerをtrueにセットしておかないとハッシュ値の変化をRouterが拾ってくれません。
その下に新しいハッシュ値が渡された場合に空文字列でないかチェックしてからTabAppの changeTo を呼び出すようにしました。

以上でTabRouterの実装は完了です。まとめたコードはこちらです。

var TabRouter = Backbone.Router.extend({
  initialize: function(options) {
    this.options = options;
    this.app = TabApp.initialize(this.options.data);
  },

  routes: {
    '*tab': 'defaultAction'
  },

  defaultAction: function(tab) {
    var self = this;
    this.app.render(function() {
      self.navigate(self.options.data[0].name, { trigger: true });
    });

    if (tab.length) {
      this.app.changeTo(tab);
    }
  }
});

アプリケーションをスタートさせる

さて全てのコンポーネントが揃いました!最後に Backbone.history を使ってページ表示時にTabRouterのハッシュ値監視を始めてもらうようにします。これはhtml側にjavascriptを直接書くようにします。

<script type="text/javascript">
  jQuery(function() {
    var data = {
      { name: 'apple', content: 'commodo consequat id interdum rhoncus Maecenas...' },
      { name: 'google', content: 'commodo consequat id interdum rhoncus Maecenas...' },
      { name: 'classmethod', content: 'commodo consequat id interdum rhoncus Maecenas...' }
    };

    new TabRouter(data);
    Backbone.history.start();
  });
</script>

全体をjQueryに渡す匿名関数で囲ったのでDOMの構築後に実行されます。そしてTabRouterをnewしていますが、特にインスタンスを受け取る必要はありません。最後に Backbone.history.start を実行して hashchange イベントに反応できるようにしておきます。

デモ

そして完成したのがこちらです!!!

http://cm-backbone-ui.herokuapp.com/tab