Skip to content
On this page

コンポーネントにスロットを使用する

商品をコンポーネント化したことで、Card コンポーネントに必要な情報を props で渡すだけとなり、コードが見やすくなりました。

vue
<template>
  <header class="header">
    <img
      src="/images/logo.svg"
      alt="">
    <h1>Vue.js ハンズオン</h1>
  </header>
  <main class="main">
    <template
      v-for="item in items"
      :key="item.id">
      <div
        v-if="!item.soldOut"
        class="item"
        :class="{ 'selected-item': item.selected }"
        @click="item.selected = !item.selected">
        <Card
          :id="item.id"
          :image="item.image"
          :name="item.name"
          :description="item.description"
          :price="item.price"/>
      </div>
    </template>
  </main>
</template>

ここで、商品ページを充実するために、drinksidemenu といった「タグ情報」であったり、商品詳細やアレルギー表示などの「リンク情報」といった機能を追加しようとした時はどうすればよいでしょうか。現在のコードでは、props に定義されておらず、情報を渡すことができないため、その度に props の修正を行う必要があります。そこで、Card コンポーネントを汎用的に利用できるように、スロットを使用してみましょう。

スロットについて

スロットとは

Vue.js のスロットでは、親コンポーネントから子コンポーネントにコンテンツを渡してレンダリングすることが可能です。スロットを使用すると、コンポーネントの props の修正に手を入れることなく、表示するコンテンツを変更できるため、コンポーネントの再利用性と柔軟性が高まります。スロットには、スロットコンテンツスロットアウトレットという仕組みがあるので、説明していきます。

スロットコンテンツ

スロットコンテンツとは、子コンポーネントへ渡すコンテンツのことを指します。

コンテンツを渡す方法は、親コンポーネントで子コンポーネントを呼び出し、子コンポーネントの要素へレンダリングしたいコンテンツを定義します。スロットコンテンツとして、プレーンテキスト、HTML 要素、他のコンポーネントなど、さまざまな種類を渡すことができます。

親コンポーネント

vue
<ChildComponent>
  スロットコンテンツ
</ChildComponent>

スロットアウトレット

親コンポーネントでスロットコンテンツを定義しましたが、子コンポーネント側では、スロットコンテンツを受け取るためのスロットアウトレットを用意する必要があります。

コンテンツを受け取る方法は、スロットコンテンツをレンダリングしたい場所に slot 要素を定義します。

子コンポーネント

vue
<div>
  <!-- 親要素のスロットコンテンツがslot要素へレンダリングされる -->
  <slot />
</div>

スロットコンテンツで紹介した親コンポーネントのコードと、スロットアウトレットの子コンポーネントを組み合わせると、最終的に表示されるコードは以下のようになります。

vue
<div>
  <!-- スロットコンテンツがスロットアウトレットにレンダリングされている -->
  スロットコンテンツ
</div>

名前付きスロット

コンポーネントに複数のスロットを渡したい場合や、コンテンツを識別しやすいように特定の名前を付けたい場合があります。このような場合、名前付きスロットを使用することができます。

名前付きスロットを定義するには、name 属性を指定する必要があります。

子コンポーネント

vue
<template>
  <div>
    <h2>Child Component</h2>
    <slot name="contents" />
    <slot name="footer" />
  </div>
</template>

上の例では、コンポーネントのテンプレートに、コンテンツ用とフッター用の 2 つの名前付きスロットを定義しています。親コンポーネント側では、スロットアウトレットの name 属性で定義した名前を、v-slot ディレクティブを使用して、スロットコンテンツに指定します。

親コンポーネント

vue
<template>
  <div>
    <ChildComponent>
      <template v-slot:contents>
        <p>コンテンツ</p>
      </template>
      <template v-slot:footer>
        <p>フッター</p>
      </template>
    </ChildComponent>
  </div>
</template>

スロットコンテンツには、DOM をそのまま記述することが可能なので、テキストを p 要素で囲って渡していることにも注目です。

また、v-slot には専用の省略表記があり、<template v-slot:contents><template #contents と書くことができます。

vue
<template>
  <div>
    <ChildComponent>
      <template #contents>
        <p>コンテンツ</p>
      </template>
      <template #footer>
        <p>フッター</p>
      </template>
    </ChildComponent>
  </div>
</template>

ヒント

name 属性がない <slot /> は暗黙的に default という name が設定されています。 つまり、 <slot name="default" /> と同じ意味になります。もちろん親コンポーネントでも <template #default> と記述することが可能です。

Cardコンポーネントでスロットを利用する

実際に Card コンポーネントにスロットを実装していきましょう。

description をスロットに置き換える

現在、Card コンポーネントは、idimagenamedescriptionprice、 5 つの props を受け付けています。今回は、props から description を削除し、汎用的に使用可能な body という名前付きスロットに置き換えていきます。まずは、親コンポーネントである App.vue を修正していきます。

App.vue / template

vue
<template>
  <header class="header">
    <img
      src="/images/logo.svg"
      alt="">
    <h1>Vue.js ハンズオン</h1>
  </header>
  <main class="main">
    <template
      v-for="item in items"
      :key="item.id">
      <div
        v-if="!item.soldOut"
        class="item"
        :class="{ 'selected-item': item.selected }"
        @click="item.selected = !item.selected">
        <Card
          :id="item.id"
          :image="item.image"
          :name="item.name"
          :price="item.price">
          <template #body>
            <p>{{ item.description }}</p>
          </template>
        </Card>
      </div>
    </template>
  </main>
</template>

スロットを利用し、コンテンツを表示する

App.vue の修正が終わったら、次は Card コンポーネントを修正します。

スロットを利用し、テキストを挿入する

Card コンポーネントでは、name 属性に body を指定したスロットアウトレットを定義します。同時に、props から description を削除しておきます。

Card.vue / template

vue
<template>
  <div class="thumbnail">
    <img
      :src="image"
      alt="">
  </div>
  <div class="description">
    <h2>{{ name }}</h2>
    <slot name="body" />
    <span>¥<span class="price">{{ pricePrefix(price) }}</span></span>
  </div>
</template>

Card.vue / script

vue
<script setup>
defineProps({
  name: {
    type: String,
    default: '',
    required: false
  },
  price: {
    type: Number,
    default: null,
    required: false
  },
  image: {
    type: String,
    default: '',
    required: false
  },
});
//省略
</script>

以上で、スロットの置き換えが完了です。見た目上の変化はありませんが、body のスロットコンテンツが表示されているかと思います。

+1 チャレンジ
ここまでの学習が完了した人は、以下の内容にも挑戦してみましょう。

スロットにコンポーネントを定義する

スロットに複数要素を定義する

カードに表示する内容で、商品詳細ページのリンクを追加したいと考えたとします。slot には複数の HTML 要素を定義することもできるので、a 要素を追加してリンクを設定してみましょう。

商品ごとにリンクが異なっていたり、そもそも商品詳細ページがない商品もあるかもしれません。そのような時でも、slot を使用して柔軟に対応が可能です。まず、商品リストの定義に詳細ページのリンクを追加します。

App.vue / script

vue
<script setup>
import { ref } from 'vue'
import Card from './components/Card.vue'

const items = ref([
  {
    id: 1,
    name: 'アボカドディップバケット',
    description:
      '刻んだ野菜をアボカドと混ぜてディップに。こんがり焼いたバゲットとお召し上がりください。',
    price: 480,
    image: '/images/item1.jpg',
    soldOut: false,
    selected: false,
    link: 'https://handson.vuejs-jp.org/'
  },
  //省略
])
</script>

今回はダミーでハンズオンのリンクを設定しています。次に、スロットコンテンツの修正をします。

App.vue / template

vue
<template>
<!-- 省略-->
        <Card
          :id="item.id"
          :image="item.image"
          :name="item.name"
          :price="item.price">
          <template #body>
            <p>{{ item.description }}</p>
            <a v-if="item.link" :href="item.link">リンク</a>
          </template>
        </Card>
<!-- 省略-->
</template>

a 要素へ商品詳細ページのリンクを設定していますが、v-if を使用して、詳細ページのリンク情報が設定されていない場合は表示されないようにしています。

スロットコンテンツをコンポーネント化する

スロットコンテンツへは、HTML 要素だけでなく、コンポーネントも定義することが可能です。スロットコンテンツをコンポーネント化して、表示されるか確認してみましょう。

まずは components 配下に CardBody.vue を新規で作成して、props の定義も行います。props では descriptionlink を受け付けます。

CardBody.vue

vue
<script setup>
defineProps({
  description: {
    type: String,
    default: '',
    required: false
  },
  link: {
    type: String,
    default: '',
    required: false
  },
});
</script>

<template>
  <p>{{ description }}</p>
  <a v-if="link" :href="link">リンク</a>
</template>

コンポーネントの作成ができたので、スロットコンテンツを置き換えます。また、CardBody コンポーネントを import することを忘れないようにしましょう。

App.vue

vue
<script setup>
import { ref } from 'vue'
import Card from './components/Card.vue'
import CardBody from './components/CardBody.vue';

//省略
</script>

<template>
<!-- 省略-->
        <Card
          :id="item.id"
          :image="item.image"
          :name="item.name"
          :description="item.description"
          :price="item.price">
          <template #body>
            <CardBody
              :description="item.description"
              :link="item.link"/>
          </template>
        </Card>
<!-- 省略-->
</template>

以上で、スロットコンテンツへのコンポーネントの定義が完了です。

+1 チャレンジでは、スロットコンテンツへの複数要素の定義と、コンポーネントの定義を行い、レンダリングされることが確認できたと思います。