Vue3での中継用コンポーネントの作り方についてメモ

ただたんに、親コンポーネントから孫コンポーネントにプロパティを渡したり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>

ここはもう少しうまいことできないかなとは思う。

コメント

タイトルとURLをコピーしました