はじめに
Vue.jsでアプリ作成中に複数アコーディオンを開閉する実装でハマりました。
ネットで参考になりそうな記事を探していると同じようなところで悩んでる方が散見されたので、どなたかの参考になれば幸いです。
対象読者
Vue.js初心者の方(私含む)
前提
フロントエンドはVue.js、サーバーサイドはRailsで作成。
投稿(post)内容をカード形式で一覧にし、投稿のタイトル名をクリックすると投稿の詳細内容が表示されるようにします。
ハマったところ
アコーディオン開閉時に全てのアコーディオンが開閉されてしまう。本当はカードごとに開閉したいのに。。
コード
fetchPostsメソッドでPostモデルの内容を取得し、投稿のタイトル、写真を表示します。toggleShowPostでshowプロパティのtrue/falseを制御することによって、投稿の詳細内容の表示・非表示を切り替えています。
<template>
<div class="container">
<div class="row">
<div class="col s7 m7" v-for="post in posts" v-bind:key="post.id">
<div class="card">
<div class="card-image">
<img :src="post.image.url"/>
</div>
<div class="card-content" v-on:click="toggleShowPost">
<div class="card-title">
{{post.title}}
</div>
<div class="card-more">
<div v-if="show">
<font-awesome-icon icon="angle-up" size="lg"/>
</div>
<div v-else>
<font-awesome-icon icon="angle-down" size="lg"/>
</div>
</div>
</div>
<div class="card-detail" v-show="show">
{{post.content}}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'PostHome',
data: function() {
return{
posts: [],
show: false
}
},
mounted: function(){
this.fetchPosts();
},
methods: {
fetchPosts(){
axios.get(`api/v1/posts`).then(res => {
for(var i = 0; i < res.data.posts.length; i++) {
this.posts.push(res.data.posts[i]);
}
}, (error) => {
console.log(error);
});
},
toggleShowPost(){
this.show = !this.show
}
}
}
</script>
全てのアコーディオンが開閉されてしまうのは、toggleShowPost内のthisがVueComponent全体を指しており、すべてのshowプロパティに対して切り替えが適用されてしまっていたことが原因でした。
つまり、カードごとにshowプロパティを定義し、切り替える必要があります。
解決方法
具体的にどう実装すればいいのか調べていたところ、こちらの記事にたどり着きました。
Rails x Vue.jsで複数アコーディオンの実装 - Techのdatebook-備忘録-
この記事を参考に修正したコードがこちら
<template>
<div class="container">
<div class="row">
<div class="col s7 m7" v-for="(post, index) in posts" v-bind:key="post.id">
<div class="card">
<div class="card-image">
<img :src="post.image.url"/>
</div>
<div class="card-content" v-on:click="toggleShowPost(index)">
<div class="card-title">
{{post.title}}
</div>
<div class="card-more">
<div v-if="show[index]">
<font-awesome-icon icon="angle-up" size="lg"/>
</div>
<div v-else>
<font-awesome-icon icon="angle-down" size="lg"/>
</div>
</div>
</div>
<div class="card-detail" v-show="show[index]">
{{post.content}}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'PostHome',
data: function() {
return{
posts: [],
show: {}
}
},
mounted: function(){
this.fetchPosts();
},
methods: {
fetchPosts(){
axios.get(`api/v1/posts`).then(res => {
for(var i = 0; i < res.data.posts.length; i++) {
this.posts.push(res.data.posts[i]);
}
}, (error) => {
console.log(error);
});
},
toggleShowPost(key){
this.$set(this.show, key, !this.show[key])
}
}
}
</script>
変更箇所
- showプロパティを配列に変更し、配列の添字をカードごとにユニークな値に変更した(ここではv-forディレクティブのindex)
- toggleShowPostはshowプロパティにクリックしたカードのBoolean値を追加する
最初にクリックした時はshowプロパティにkeyが存在しないのでthis.show[key]はundefined
となるため、それを反転(!this.show[key])することによってtrue
になる - 押したときに表示させたい投稿内容(post.content)に
v-show="show[index]
を設定。(Font Awesomeアイコンはv-if="show[index]"
とv-else
で切り替え)