導入
今日の話題は昨日の続きで子エレメントがv-for
で動的に渡される場合にその子エレメントの外側を別のエレメントでラップする方法についてです。
※ 私が株式会社愛宕 Advent Calendar 2023に書く記事は主に社内向けに共有しておきたいけど勉強会をするまでもないちょっとしたTipsにしたいと思います。
もう一度、目的の構造を示します。
<div class="group">
<div class="group-item">
<div>hoge</div>
</div>
<div class="group-item">
<div>fuga</div>
</div>
<div class="group-item">
<div>piyo</div>
</div>
</div>
そして、次のようにExampleGroup
を使用して目的の構造が出力されるようにする方法は昨日投稿した通りです。
app.ts
<template>
<ExampleGroup>
<div>hoge</div>
<div>fuga</div>
<div>piyo</div>
</ExampleGroup>
</template>
今回は、こんな感じの場合どうしたらいいかという話です。
こうとか
<template>
<ExampleGroup>
<div v-for="item in items">{{ item.name }}</div>
</ExampleGroup>
</template>
こんなんとか
<template>
<ExampleGroup>
<div>hoge</div>
<div v-for="item in items">{{ item.name }}</div>
</ExampleGroup>
</template>
解決
$slots.default()
から直接vnodeを取得せず一旦必要なvnodeだけを取り出し、それをもとに<component>
で描画するようにします。
<div v-for="item in items">{{ item.name }}</div>
で作成したエレメントはvnodeそのものではなくvnode.children
に含まれているのがキモです。
ExampleGroup.vue
<script setup lang="ts">
// SFCでComposition APIを使うためにscript setupを使って書いています
import { isVNode } from 'vue'
// template内で書くときの$slotsと同じ
const slots = useSlots()
const vnodes = computed(() => {
if (!slots.default) {
// 念の為、slotが空の場合があってもいいように
return []
}
const result = []
for (let vnode of slots.default()) {
if (Array.isArray(vnode.children)) {
// template内でv-forでループさせた各エレメントはvnode.childrenに入っている
for (let child of vnode.children) {
if (isVNode(child)) {
// 念の為、vnodeがどうか確認してから描画対象に追加する
result.push(child)
}
}
} else {
// vnodeにchildrenがない場合は普通のvnodeなのでそのまま追加
result.push(vnode)
}
}
return result
})
</script>
<template>
<div class="group">
<div class="group-item" v-for="vnode in vnodes">
<component :is="vnode"></component>
</div>
</div>
</template>