追記: コメント欄でjQueryを使わないシンプルなコードを紹介していただいたので,そちらも参考にしていただければと思います。
はじめに
先日紹介したサンプルと同じ構成で,ページ内にBootstrapのタブを組み合わせるコードを紹介します。少なくとも日本語ではググってもイイ感じのサンプルが見つからなかったので……
目標は,各ページをコンポーネントとして実装しているSPAで,「戻る」や「進む」によるページやタブの遷移を自然に行うことです。今回の例で言えば,次のような挙動を実現することです。
-
/
にアクセスする(home
コンポーネントの内容が表示される) - リンクから
/example
にアクセスする(example
コンポーネントの内容が表示される,タブは1個めがアクティブ) - タブを2個めに切り替える
- リンクから
/
にアクセスする(再びhome
コンポーネントの内容が表示される) - 戻るボタンを押す(
example
コンポーネントが表示される,タブは2個めがアクティブ) - 戻るボタンを押す(タブが1個めに切り替わる)
サンプルコード
いきなり上記の挙動を実現するコードを載せます。
home
とexample
という2つのコンポーネントがあり,それぞれ/
というパスと/example
というパスに対応しています。example
コンポーネントの方は内部にBootstrapのタブを3個含んでいます。
<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
の初回ロード時に何が起こるか
app.$broadcast
によりexample
コンポーネントのexample-init
イベントハンドラが実行されます。このとき,URLのhash文字列がタブIDに一致すれば該当タブがアクティブになります。hash部分が空ならデフォルトのタブ(ここでは最初のタブ)がアクティブになります。
ちなみに,同じコンポーネント内の遷移であっても,$broadcast
や$on
を使うことにより特定のメソッドが呼ばれるようにする仕組みは先日の記事でも紹介しました。
タブをクリックしたときに何が起きるか
click
イベントハンドラが発火し,page関数がタブに対応するhash文字列とともに呼ばれます。対応するpage.jsのコールバック関数が呼ばれ,先ほどと同様,app.$broadcast
によりexample-init
イベントハンドラが呼ばれます。これにより該当タブがアクティブになります。
ちなみに,次のようにクリックハンドラ内で該当タブをアクティブにするだけだと,ブラウザのhistoryにタブの変化が記録されないため,タブ単位での「進む」「戻る」が不可能になります。
$(".nav-tabs a").click(function (e) {
$(this).tab("show")
}
また,次のようにクリックハンドラ内でhash文字列の書き換えをするだけだと,historyにhash文字列の変化のみが記録されるため,タブから別コンポーネントに戻る・あるいは進む場合にコンポーネントが切り替わらない現象が発生します。
$(".nav-tabs a").click(function (e) {
window.location.hash = e.target.hash
}
「戻る」「進む」により何が起こるか
戻り先や進み先がexample
コンポーネントである場合は,example-init
イベントハンドラが呼ばれるため,hash文字列に応じたタブがアクティブになります。
問題点
URLのハッシュ部分を直接書き換えてしまうとうまく切り替わらないことがある。コード的にもまだしっくり来ていないので,もう少し綺麗な書き方があれば教えてください。