ただたんに、親コンポーネントから孫コンポーネントにプロパティを渡したりslotの機能を利用するような、中継用の子コンポーネントがほしくなることがよくあります。
なのに、なかなか書き方を覚えられないからメモ。
結論からいうと、「TheContainer」というコンポーネントがあるとして、最もシンプルに作るなら下記のようになると思います。
<script setup lang="ts"> import TheContainer from "./TheContainer.vue"; </script> <template> <TheContainer msg=""> <template v-for="(slot, name) in $slots" #[name]="data"> <slot :name="name" v-bind="data"></slot> </template> </TheContainer> </template>
これで、「TheContainer」の機能を親コンポーネントを利用できます。
Vue2の時は、イベントはルートまでたどらないようになっていないため、「v-on=”$listeners”」という指定も必要だったと思いますが、Vue3では必要ないようです。
試しに作ってみた孫コンポーネントと親コンポーネントは下記
孫コンポーネント(TheContainer)
<script setup lang="ts"> import { computed } from "vue"; const props = defineProps<{ msg: string; headerMsg?: string; footerMsg?: string; }>(); const defaultMsg = computed(() => { return { header: props.headerMsg ?? "ヘッダー", msg: props.msg, footer: props.footerMsg ?? "フッター", }; }); </script> <template> <div class="container"> <header @click="$emit('header-click')"> <slot name="header" :msg="defaultMsg">{{ defaultMsg.header }}</slot> </header> <main @click="$emit('main-click')"> <slot :msg="defaultMsg">{{ defaultMsg.msg }}</slot> </main> <footer @click="$emit('footer-click')"> <slot name="footer" :msg="defaultMsg">{{ defaultMsg.footer }}</slot> </footer> </div> </template> <style scoped> .container { display: flex; flex-direction: column; height: 100vh; } .container header { height: 50px; background-color: aqua; } .container main { flex: 1; background-color: burlywood; } .container footer { height: 100px; background-color: darkseagreen; } </style>
親コンポーネント
<script setup lang="ts"> import TheRelay from "../components/relay/TheRelay.vue"; const footerClick = () => { console.log("Footer Click"); }; </script> <template> <TheRelay msg="メイン" @footer-click="footerClick"> <template #header>タイトル</template> <template #default="{ msg }">メインメッセージ:{{ msg.msg }}</template> </TheRelay> </template>
フッターをクリックすると、コンソールに「Footer Click」と表示されました。
プロパティの指定を明示的にしたい場合は、中継用コンポーネントから参照する孫コンポーネントに「v-bind=”$attrs”」と指定して、setupをつけないscriptタグ内で「inheritAttrs: false」を返すようにします。その場合、「v-bind=”$attrs”」より後にプロパティを指定すると、値がその値で上書きされます。
中継用コンポーネント
<script lang="ts"> export default { inheritAttrs: false, }; </script> <script setup lang="ts"> import TheContainer from "./TheContainer.vue"; </script> <template> <TheContainer v-bind="$attrs" msg="aiueo"> <template v-for="(slot, name) in $slots" #[name]="data"> <slot :name="name" v-bind="data"></slot> </template> </TheContainer> </template>
slotも中継用コンポーネントでしたものを上書きしたいとなることがあると思いますが、ただたんに下記のようにslotの名前のtemplateタグを指定しても上書きされないようです(先に指定してもダメ)。
<template> <TheContainer v-bind="$attrs" msg="aiueo"> <template v-for="(slot, name) in $slots" #[name]="data"> <slot :name="name" v-bind="data"></slot> </template> <template #header><button>ログアウト</button></template> </TheContainer> </template>
slotを上書きしたい場合、slotsを書き換える必要がありそう。いろいろ試してみたら、下記のようslotsからheader以外を配列に入れて、その配列をv-forで回すことでできました。
<script lang="ts"> export default { inheritAttrs: false, }; </script> <script setup lang="ts"> import { computed, useSlots } from "vue"; import TheContainer from "./TheContainer.vue"; const slots = useSlots(); const relaySlot = computed(() => { const slotNames = []; for (const slotName in slots) { if (slotName !== "header") { slotNames.push(slotName); } } console.log("slotNames", slotNames); return slotNames; }); </script> <template> <TheContainer v-bind="$attrs" msg="aiueo"> <template v-for="name in relaySlot" #[name]="data"> <slot :name="name" v-bind="data"></slot> </template> <template #header="data"> <slot name="header" v-bind="data"></slot> <button>ログアウト</button> </template> </TheContainer> </template>
ここはもう少しうまいことできないかなとは思う。
コメント