やりたいのに出来なくて困ったこと
例えばこんなことがしたい場合です。
- あるオブジェクトの一覧がある
- そのオブジェクト一覧を画面に描画する際に、v-forを使う
- ここでは表に一覧を描画するとします
- その表の各行(つまり各オブジェクト)には、そのオブジェクトに対する操作メニューを出したい
- その操作メニューをクリックするたび、表示/非表示を司るclassを切り替えたい
ちなみに僕はBuluma.jsを @nuxtjs/bulma
経由で使ってます。
以下、もしかするとちょっとコードが間違っている可能性があります。その際はほんとすいません!!
実際のコードを多少いじって書いているので・・・。もしおかしかったらご指摘ください。
1st step: 基本形
まずは雛形。こんな感じに定義したくなりますよね。
template部
<template>
<span>
<table v-if="resources.length > 0" class="table is-hoverable is-fullwidth">
<thead>
<tr>
(省略)
</tr>
</thead>
<tbody>
<tr v-for="r in resources" :key="r.id">
<td>{{ r.id }}</td>
<td>{{ r.name }}</td>
<td>{{ r.description }}</td>
<td> <!-- ここが操作メニュー部 -->
<div class="dropdown is-right">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon is-small">
<no-ssr><i class="fas fa-angle-down" aria-hidden="true"/></no-ssr>
</span>
</button>
</div>
<div id="dropdown-menu" class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item">Operation 1</a>
<a href="#" class="dropdown-item">Operation 2</a>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</span>
</template>
script部
<script>
import client from '~/clients' // resource系をaxiosで取ってくるメソッド群
export default {
data() {
return {
resources: [],
}
},
async mounted() {
try {
let res = await client.listResources()
this.resources = res.data
} catch (error) {
console.log('なんかだめぽ')
}
},
}
</script>
まずこんな状態を考えます。
しかし @nuxt.js/buluma
っていわゆるjavascript部分がほぼ欠落しています。
上記のようにdropdownを書いた場合、文法的には合っているのですが、クリックしてもトグルされないのですよね。
で、どうやらそれは仕様のようでして、 自分で補え!!
となっているわけです。
それがそもそもの問題の始まりだったりもするわけですよ・・・。
ということで、こんな形に書き換えてみたとします。
2nd step: arrayを用意してみる
リソース毎のメニュー表示/非表示を切り替えるためのarrayを用意してみました。
div.dropdownに .is-activeを足すとメニューが開くので、それを制御しようとしました。
template部
<template>
<span>
<table v-if="resources.length > 0" class="table is-hoverable is-fullwidth">
<thead>
<tr>
(省略)
</tr>
</thead>
<tbody>
<tr v-for="(r, index) in resources" :key="r.id">
<td>{{ r.id }}</td>
<td>{{ r.name }}</td>
<td>{{ r.description }}</td>
<td>
<!-- 操作メニュー部にv-bind:classを足した。 -->
<!-- .is-active が足されるとメニュー開くのがBulma仕様 -->
<div
class="dropdown is-right"
:class="{ 'is-active': dropDownActive[index] }"
>
<div class="dropdown-trigger">
<button
class="button"
aria-haspopup="true"
aria-controls="dropdown-menu"
@click="toggleDropDownActive(index)" // 改行されてるけど足したのはこれ
>
<span class="icon is-small">
<no-ssr><i class="fas fa-angle-down" aria-hidden="true"/></no-ssr>
</span>
</button>
</div>
<div id="dropdown-menu" class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item">Operation 1</a>
<a href="#" class="dropdown-item">Operation 2</a>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</span>
</template>
script部
<script>
import client from '~/clients' // resource系をaxiosで取ってくるメソッド群
export default {
data() {
return {
resources: [],
dropDownActive: [], // リソース毎のプルダウンがis-active classを保有するかどうかを規定するarray
}
},
async mounted() {
try {
let res = await client.listResources()
this.resources = res.data
for (let i = 0; i < this.resources.length: i++) {
this.dropDownActive.push(false) // 初期状態はis-active=falseな状態で初期化
}
} catch (error) {
console.log('なんかだめぽ')
}
},
methods: {
toggleDropDownActive: function(index) {
// ここで各リソース毎のメニュー状態をトグルする
this.dropDownActive[index] = !this.dropDownActive[index]
}
}
}
</script>
でもこれだとだめなんですよね。動作しません。
再評価がうまく行われない異様です。
3rd step: computedを用意してみる
次にcomputedなプロパティを定義するというのも考えました。
再評価されないならcomputedすればいいのかな・・・という素人な考え。
しかし、最初に書いておきますと、これは動作しないだけではなくエラーになります。
template部
<template>
<span>
<table v-if="resources.length > 0" class="table is-hoverable is-fullwidth">
<thead>
<tr>
(省略)
</tr>
</thead>
<tbody>
<tr v-for="(r, index) in resources" :key="r.id">
<td>{{ r.id }}</td>
<td>{{ r.name }}</td>
<td>{{ r.description }}</td>
<td>
<!-- 操作メニュー部にv-bind:classを足した。 -->
<!-- .is-active が足されるとメニュー開くのがBulma仕様 -->
<div
class="dropdown is-right"
:class="{ 'is-active': isDropDownActive(index) }" // computed化っぽい感じ(なだけ)
>
<div class="dropdown-trigger">
<button
class="button"
aria-haspopup="true"
aria-controls="dropdown-menu"
@click="toggleDropDownActive(index)"
>
<span class="icon is-small">
<no-ssr><i class="fas fa-angle-down" aria-hidden="true"/></no-ssr>
</span>
</button>
</div>
<div id="dropdown-menu" class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item">Operation 1</a>
<a href="#" class="dropdown-item">Operation 2</a>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</span>
</template>
script部
<script>
import client from '~/clients' // resource系をaxiosで取ってくるメソッド群
export default {
data() {
return {
resources: [],
dropDownActive: [], // リソース毎のプルダウンがis-active classを保有するかどうかを規定するarray
}
},
async mounted() {
try {
let res = await client.listResources()
this.resources = res.data
for (let i = 0; i < this.resources.length: i++) {
this.dropDownActive.push(false) // 初期状態はis-active=falseな状態で初期化
}
} catch (error) {
console.log('なんかだめぽ')
}
},
computed: {
isDropDownActive: function(index) { // arrayの内容を単に返すだけ
return dropDownActive[index]
}
}
methods: {
toggleDropDownActive: function(index) {
// ここで各リソース毎のメニュー状態をトグルする
this.dropDownActive[index] = !this.dropDownActive[index]
}
}
}
</script>
結局compuedなメソッドに参照時に引数を渡すというのがそもそもの問題。
これだとだめ。
ということでいろいろ探していたら、こういうサイトを見つけました。
[Vue.js/Nuxt.js] v-forで中の要素にcomputedを使いたいときはコンポーネント分割をしよう
ありがとうございます。。。ほんとに救われました。
step4: これを見てたどり着いた答え
- 一覧のtr配下を別コンポーネントとして切り出します
- それに対して引数でリソース(ループされる側)を渡します
- 受ける側はそれをprop(プロパティ)としてもたせます(というか、プロパティとして持たせているから外から渡せるというのが正しい)
- メニューのactive/inactiveは、コンポーネント内の単なるdataとして扱います
こういうイメージです。
親要素側(いままでみていた部分)
template部
<template>
<span>
<table v-if="resources.length > 0" class="table is-hoverable is-fullwidth">
<thead>
<tr>
(省略)
</tr>
</thead>
<tbody>
<tr v-for="(r, index) in resources" :key="r.id">
<td>{{ r.id }}</td>
<td>{{ r.name }}</td>
<td>{{ r.description }}</td>
<td>
<!-- 操作メニュー部を切り出す -->
<!-- ループ内のrをresourceプロパティ経由で子コンポーネントに渡す -->
<list-operation :resource="r" />
</td>
</tr>
</tbody>
</table>
</span>
</template>
script部
<script>
import client from '~/clients' // resource系をaxiosで取ってくるメソッド群
export default {
data() {
return {
resources: [],
}
},
async mounted() {
try {
let res = await client.listResources()
this.resources = res.data
} catch (error) {
console.log('なんかだめぽ')
}
},
}
</script>
最小限の状態に戻りました。
で、これとは別に、ListOperationという子コンポーネントを作ります。
子要素側
template部
<template>
<div class="dropdown is-right" :class="{ 'is-active': isActive }">
<div class="dropdown-trigger">
<button
class="button"
aria-haspopup="true"
aria-controls="dropdown-menu"
@click="toggleDropDownActive"
>
<span class="icon is-small">
<no-ssr><i class="fas fa-angle-down" aria-hidden="true"/></no-ssr>
</span>
</button>
</div>
<div id="dropdown-menu" class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item">Operation 1</a>
<a href="#" class="dropdown-item">Operation 2</a>
</div>
</div>
</div>
</template>
script部
<script>
export default {
props: {
resource: {
type: Object,
default: null,
},
},
data() {
return {
isActive: false,
}
},
methods: {
toggleDropDownActive: function() {
this.isActive = !this.isActive
},
},
}
</script>
ポイントは、
- 親要素側で子要素を記述する際に
:<プロパティ名>="<値>"
として、v-for内でループしているものを渡す - それを小要素側は
props
で受ける - あとはループを意識しないで子要素の処理に専念して書く
という形。
これで2時間位ハマったのですが、おかげでコードもスッキリして問題も解消しました。
何より勉強になりましたw
あと実は他にも解決方法があるかもですが、ぜひご存知のかたいらっしゃいましたら教えてください!!
(ちゃんとvue.jsの動作原理を理解していかないとなぁ。。。。)