2019.03.01

Vue.js中級編!?「スコープ付きスロット」を理解しよう


この記事では、Vue.js の機能のひとつであるスコープ付きスロットについて解説します。

スロットとは

スロットとは、子コンポーネントの描画内容を親コンポーネントが決める機能です。

たとえばモーダルウィンドウを表現する <modal> コンポーネントがあるとしましょう。

<modal text="This is modal window"></modal>

上記のようにモーダルウィンドウに表示する内容を props で表現してしまうと、自由度が低く使いまわしにくくなります。たとえば画像やリストやテーブルが入れられませんよね。

そこでスロットを利用します。

<modal>
  <template #title>
    This is modal window
  </template>
  <template #content>
    <img src="..." alt="" />
    <ul>
      <li>...</li>
    </ul>
  </template>
</modal>

<modal> コンポーネント(子コンポーネント)の描画内容を、<modal> コンポーネントを内包する親コンポーネントが決めています。

参考記事 👇

スロットと変数のスコープ

次に「普通の」スロット機能と変数のスコープについて説明します。スコープ付きスロットの理解に繋がるでしょう。

以下のテンプレートを持つ <Hello> コンポーネントを考えます。

<template>
  <span>
    Hello,
    <slot></slot>
  </span>
</template>

親コンポーネントでスロット部分に star 変数を差し込みます。

<template>
  <Hello>{{ star }}</Hello>
</template>

<script>
import Hello from './Hello.vue'

export default {
  components: { Hello },
  data() {
    return { star: 'Earth' }
  }
}
</script>

これは結果的に Hello, Earth とレンダリングされます。

つまりポイントは、テンプレートに記述された変数は、そのコンポーネント自身の変数として展開されるということです。この挙動は直感的なので当たり前に思われるかもしれませんが、上記の例では変数 star は親コンポーネントのデータ変数 star として扱われます。<Hello> コンポーネント側のデータ変数として扱われるわけではありません。

逆に言えば、親コンポーネントから子コンポーネント側の変数にはアクセスできません。子コンポーネントの変数に自由にアクセスできてしまうとコンポーネント間の結合も密になってしまいコンポーネント指向のメリットが活かせなくなります。

以上が Vue コンポーネントの基本的な挙動です。しかし特定のユースケースのために、親コンポーネントから子コンポーネント内のデータにスロットを通してアクセスする方法が用意されています。それがスコープ付きスロットです。

スコープ付きスロットとは

スコープ付きスロットとは、子コンポーネントのスロットに渡された props の値に、親コンポーネントからアクセスできる機能です。使いどころによっては普通のスロット機能よりも汎用性の高いコンポーネントを作成できます。

まずは簡単な例を見ていきましょう。以下の <HelloUser> コンポーネントを考えます。

<template>
  <div>
    Hello,
    <slot :user="currentUser">
      {{ currentUser.firstName }}
    </slot>
  </div> 
</template>

<script>
export default {
  computed {
    currentUser() {
      // Vuexストアからユーザー情報を取得
      return this.$store.state.user
    }
  }
}
</script>

<slot>user というプロパティを渡している点がポイントです。これがスコープ付きスロットの特徴です。スロットに渡されたプロパティはスロットプロパティと呼ばれます。

this.$store.state.user は以下の値を返すとしましょう。

{ firstName: 'John', lastName: 'Lennon' }

すると以下の記述は...

<HelloUser />

Hello, John と描画されます。ここまでは普通のスロットのお話です。

さて、フルネーム(firstNamelastName)を表示したい場合は以下のようにスコープ付きスロットの機能を用います。

<HelloUser>
  <template #default="slotProps">
    {{ slotProps.user.firstName }}
    {{ slotProps.user.lastName }}
  </template>
</HelloUser>

これは Hello, John Lennon と描画されます。

いろいろなルールがいっぺんに適用されているので箇条書きにしてみましょう。

  1. #defaultv-slot:default の省略記法です。
  2. v-slot 属性は <template> 要素にしか記述できません。
  3. default はスロットの名前を表します。
    明示的に名付けなければ暗黙的に default という名前になります。
  4. v-slot:*** の値には任意の変数名を記述します。
    ここで指定された変数にスロットプロパティを保持するオブジェクトが代入されます。
  5. slotProps.user.lastName のように v-slot に渡した変数(slotProps)を通して親コンポーネントから子コンポーネントが持っているデータにアクセスしています。これがスコープ付きスロットの機能です。

<HelloUser> コンポーネントではスロットに user プロパティを渡していました。

<slot :user="currentUser">

親コンポーネント側では slotProps と名付けられた変数にスロットプロパティがすべて入ります。その中から user を取り出して slotProps.user.lastName とアクセスしています。

スロットについては、Vue.js 2.6 の新記法を紹介したこちらの記事も参考にしてください。

スコープ付きスロットは Vue.js のなかでも比較的複雑な機能だと思います。この名前とここの変数名が紐づいて…というように、複数のコンポーネントにまたがって頭を働かせないといけないですね。どことどこが繋がっているのか把握することがポイントになります。

v-model も同様に複雑な機能だと思いますが、こちらは結構使いどころがあるので使っているうちに覚えられます。そこでスコープ付きスロットについても実用的な使用例を考えました。

使用例

何らかのデータの一覧を表現するための <list-group> コンポーネントです。

See the Pen Vue.js Scoped Slots by Masahiro Harada (@MasahiroHarada) on CodePen.

Bootstrap の List Group をイメージして作りました。「何らかの」というのがポイントで、<list-group> が受け持つのは一覧表現のみで中身は問いません。具体的に言えば、赤枠、白背景で四隅が角丸、内側に特定の余白があってコンテンツを内包する外見を提供するのがこのコンポーネントの役割です。そして、その中に何が入るかはスコープ付きスロットの機能を使って親コンポーネントで決める設計です。

こちらが <list-group> コンポーネントの定義です。

Vue.component('list-group', {
  template: `
    <ul class="list-group">
      <li v-for="item in items" :key="item.key">
        <slot :content="item"></slot>
      </li>
    </ul>
  `,
  props: ['items']
})

何かのデータの配列を items プロパティで受け取ります。そしてその items 配列の要素をループで出力します。ただし、一つ一つの要素をどのように表示するかはスロットによってこのコンポーネントを使う側、つまり親コンポーネントに委ねています。

1つめのリストは単なる文章のリストです。

<list-group :items="dummyTexts">
  <template #default="slotProps">
    {{ slotProps.content.text }}
  </template>
</list-group>

2つめのリストは写真とキャプションがセットになったカード風の部品のリストです。

<list-group :items="photos">
  <template #default="foo">
    <image-box
      :src="foo.content.url"
      :caption="foo.content.caption"
    ></image-box>
  </template>
</list-group>

どちらも要素の表示の仕方は異なりますが、<list-group> コンポーネントによって統一された一覧表現を得ています。普通のスロットの機能だけではループ要素の一つ分だけをスロットとして切り出すことはできないので、スコープ付きスロットの活用例と言えるでしょう。


以上、この記事では Vue.js のスコープ付きスロットとその使用例を紹介しました。
特徴を掴んでもらって、使ってみるきっかけになれば幸いです。