🐡

Vue3: slot から VNode を取得してパンくずリストを実装する

に公開

イメージ

こう使いたい

        <MyBreadcrumb>
          <a href="#">
            <span class="icon-[eva--home-outline]"> </span>
            ホーム
          </a>
          <a href="#"> カテゴリ </a>
          <a href="#"> コンテンツ </a>
        </MyBreadcrumb>

要するに、<slot>の各要素の間に区切りを差し込みたいだけ。

結論

slot から 各要素(VNode) の配列を取り出す。

VNodeの配列は、useSlotsから$slots.default() 関数で取り出せる。
defaultはスロットの名前。<slot name="separator">から取り出すときは$slots.separator()を使う。

<script setup lang="ts">
import { useSlots } from 'vue';

const $slots = useSlots();

// slot から VNode[] を取り出す
const children = $slots.default?.() ?? [];
</script>

<template>
  <ul>
    <template v-for="(child, i) in children" :key="i">
      <!-- useSlots を使わず、 v-for="(child, i) in $slots.default?.() ?? []" と書いてもOK -->
      <li class="inline-flex items-center">
        <!-- VNode をそのまま描画 -->
        <component :is="child" />
      </li>

      <!-- 区切り(最後以外) -->
      <li v-if="i !== children.length - 1" class="inline-flex items-center">
        <span class="icon-[weui--arrow-filled]"></span>
      </li>
    </template>
  </ul>
</template>

何が嬉しいか

子要素の情報をオブジェクトの配列としてプロパティで受け入れても、似たようなことはできる。

しかし、プロパティによる実装では、利用する場面の拡大によりコンポーネント内外の実装が複雑になりがちだ。

<script setup lang="ts">
defineProps<{
  children: {
    icon?: string;
    href: string;
    label: string;
  }[];
}>();
</script>

<template>
  <ul>
    <template v-for="(child, i) in children" :key="i">
      <li class="inline-flex items-center">
        <a :href="child.href">
          <span v-if="child.icon" :class="child.icon"> </span>
          {{ child.label }}
        </a>
      </li>

      <!-- 区切り(最後以外) -->
      <li v-if="i !== children.length - 1" class="inline-flex items-center">
        <span class="icon-[weui--arrow-filled]"></span>
      </li>
    </template>
  </ul>
</template>
  • child.label にスタイルをあてたい場面が出てきた
    • => child.label を<span>で囲み、プロパティを追加する
  • リンク(href) を持たない要素も入れたくなってきた
    • => <a> タグに v-if="child.href"条件を追加して分岐を作る
  • 最後の要素だけスタイルを変えたい
    • => また分岐...

コンポーネントを作った当時は考えていなかったユースケースに対応するため、コンポーネント内の分岐が増えてしまう。渡せるプロパティも増えてコンポーネント外も複雑化し、コンポーネントを使っている側のコードを見ても何が起きているかわからなくなってしまう。

つまり、どんなにシンプルに作っても、いずれ融通が利かない場面に遭遇する

対して、 slot をそのまま使えば、コンポーネントの仕事が「各要素の間に区切りを差し込む」という役割から逸脱することがない

いや、それって融通が利かないんじゃないの?と聞こえる話だけど、コンポーネントを使う側に任されている、という意味で融通が利く。

落とし穴:slot 内で v-for/v-if を使うと要素数は予想通りにならない

slot内でv-forが使われている場合、$slots.default() で取れる要素は1つになる。
v-ifも条件を満たしていなくても、配列としては1つになる。

下の例だと、list配列の要素数がどれだけ増えても減っても、
子コンポーネントの$slots.default?.()で取れる配列の要素数は常に2つになる。

        <MyBreadcrumb>
          <a v-for="(item, i) of list" :key="i" href="#">
            {{ item }}
          </a>
          <div v-if="list.length >= 3">3つ以上</div>
        </MyBreadcrumb>

つまり、子要素が動的な内容であれば、プロパティによる実装しか取れない。

参考

React MUI の Breadcrumbsがかっこよかったので、vueでもできないかとChatGPTに聞いてみた。

Discussion