vue.jsとBootstrapのタブを組み合わせるサンプル

__追記: コメント欄でjQueryを使わないシンプルなコードを紹介していただいたので，そちらも参考にしていただければと思います。__

## はじめに

[先日紹介したサンプル](http://qiita.com/tondol/items/b48c2e2424b50cb63c24)と同じ構成で，ページ内にBootstrapのタブを組み合わせるコードを紹介します。少なくとも日本語ではググってもイイ感じのサンプルが見つからなかったので……

目標は，各ページをコンポーネントとして実装しているSPAで，「戻る」や「進む」によるページやタブの遷移を自然に行うことです。今回の例で言えば，次のような挙動を実現することです。

- `/`にアクセスする（`home`コンポーネントの内容が表示される）
- リンクから`/example`にアクセスする（`example`コンポーネントの内容が表示される，タブは1個めがアクティブ）
- タブを2個めに切り替える
- リンクから`/`にアクセスする（再び`home`コンポーネントの内容が表示される）
- 戻るボタンを押す（`example`コンポーネントが表示される，タブは2個めがアクティブ）
- 戻るボタンを押す（タブが1個めに切り替わる）

## サンプルコード

いきなり上記の挙動を実現するコードを載せます。

`home`と`example`という2つのコンポーネントがあり，それぞれ`/`というパスと`/example`というパスに対応しています。`example`コンポーネントの方は内部にBootstrapのタブを3個含んでいます。

```html
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/0.11.0/vue.min.js"></script>
<script src="//cdn.rawgit.com/visionmedia/page.js/master/page.js"></script>

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>

<div id="container">
  <div v-component="{{main}}" class="view">
    <div><a href="/">home</a></div>
    <div><a href="/example">example</a></div>
  </div>
</div>

<script type="text/v-template" id="home">
  <h1>home</h1>
  <div><content/></div>
</script>
<script type="text/v-template" id="example">
  <h1>example</h1>
  <div><content/></div>
  <hr />
  <ul class="nav nav-tabs">
    <li class="active"><a href="#tab1" data-toggle="tab">タブ1</a></li>
    <li><a href="#tab2" data-toggle="tab">タブ2</a></li>
    <li><a href="#tab3" data-toggle="tab">タブ3</a></li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane fade in active" id="tab1">
      <p>コンテンツ1</p>
    </div>
    <div class="tab-pane fade" id="tab2">
      <p>コンテンツ2</p>
    </div>
    <div class="tab-pane fade" id="tab3">
      <p>コンテンツ3</p>
    </div>
  </div>
</script>

<script>
  Vue.component("home", {
    template: "#home"
  })
  Vue.component("example", {
    template: "#example",
    ready: function() {
      $(".nav-tabs a").click(function (e) {
        page(window.location.pathname + e.target.hash)
      })

      // exampleに関わるページ遷移の度に呼ばれる
      this.$on("example-init", function() {
        var a = $(".nav-tabs a[href=" + window.location.hash + "]")
        if (a.size() == 0) {
          a = $(".nav-tabs a:first")
        }
        a.tab("show")
      })
    }
  })

  var app = new Vue({
    el: '#container',
    data: {
      main: undefined
    }
  })

  page("/", function(ctx) {
    app.main = "home"
  })
  page("/example", function(ctx) {
    app.main = "example"

    Vue.nextTick(function() {
      app.$broadcast("example-init")
    })
  })
  page()
</script>
```

`example`コンポーネントは次のように表示されます。

![exampleコンポーネントの表示](https://qiita-image-store.s3.amazonaws.com/0/20776/261f8ccc-8ab8-db9a-2e3b-16c290cc00a7.png)

## `example`の初回ロード時に何が起こるか

`app.$broadcast`により`example`コンポーネントの`example-init`イベントハンドラが実行されます。このとき，URLのhash文字列がタブIDに一致すれば該当タブがアクティブになります。hash部分が空ならデフォルトのタブ（ここでは最初のタブ）がアクティブになります。

ちなみに，同じコンポーネント内の遷移であっても，`$broadcast`や`$on`を使うことにより特定のメソッドが呼ばれるようにする仕組みは[先日の記事](http://qiita.com/tondol/items/b48c2e2424b50cb63c24)でも紹介しました。

## タブをクリックしたときに何が起きるか

`click`イベントハンドラが発火し，page関数がタブに対応するhash文字列とともに呼ばれます。対応するpage.jsのコールバック関数が呼ばれ，先ほどと同様，`app.$broadcast`により`example-init`イベントハンドラが呼ばれます。これにより該当タブがアクティブになります。

ちなみに，次のようにクリックハンドラ内で該当タブをアクティブにするだけだと，ブラウザのhistoryにタブの変化が記録されないため，タブ単位での「進む」「戻る」が不可能になります。

```js
$(".nav-tabs a").click(function (e) {
  $(this).tab("show")
}
```

また，次のようにクリックハンドラ内でhash文字列の書き換えをするだけだと，historyにhash文字列の変化のみが記録されるため，タブから別コンポーネントに戻る・あるいは進む場合にコンポーネントが切り替わらない現象が発生します。

```js
$(".nav-tabs a").click(function (e) {
  window.location.hash = e.target.hash
}
```

## 「戻る」「進む」により何が起こるか

戻り先や進み先が`example`コンポーネントである場合は，`example-init`イベントハンドラが呼ばれるため，hash文字列に応じたタブがアクティブになります。

## 問題点

URLのハッシュ部分を直接書き換えてしまうとうまく切り替わらないことがある。コード的にもまだしっくり来ていないので，もう少し綺麗な書き方があれば教えてください。

## 関連リンク

- [vue.jsとpage.jsの組み合わせでルーティングをするときのサンプル](http://qiita.com/tondol/items/b48c2e2424b50cb63c24)