vue.jsではSlotを利用することで親コンポーネントから子コンポーネントの内側にコンテンツを挿入することができます。例えばボタンというコンポーネントを作成し、ボタン上に表示されるテキストのみ変更したい場合、親コンポーネントからSlotを使ってテキストを渡すことで子コンポーネントのボタン上に表示させるテキストを変更することができます。ボタン上のテキストを変更できることでボタンコンポーネントを再利用することが可能となります。

本文書ではSlotのみに特化して説明を行っているので本文書を読み終えるとSlot、Named Slot、データプロパティを使ったScoped Slot, メソッドを使ったScoped Slotの4つを理解することができます。

特に入門者の人はこの機会にSlotやNamed SlotだけではなくScoped Slotをしっかりと理解していきましょう。

Slotの使用方法の初級編

最もシンプルなSlotの使い方

最も簡単な例を使ってSlotがどのようなものか確認しましょう。vue createコマンドで任意のvueプロジェクトを作成し、App.vueファイルを利用し動作確認を行っています。

まず最初にプロジェクトディレクトリの直下にあるsrc¥componentsディレクトリに子コンポーネントUser.vueを作成します。親のコンポーネントから受け取るコンテンツを挿入したい場所に<slot></slot>タグを入れます。


<template>
  <p>この人の名前は<slot></slot>です。</p>
</template>

<script>
export default {
}
</script>

親のコンポーネントであるApp.vueでは作成したUser.vueをインポートしてコンポーネントタグuserの間にコンテンツを記述します。


<template>
  <user>John Doe</user>
</template>

<script>
import User from './components/User.vue'
export default {
 name: 'app',
 components: {
  User,
 },
}
</script>

ブラウザで確認すると親のコンポーネント側のuserタグの間に入れたコンテンツが子コンポーネントUser.vueの<slot></slot>に置き換わっていることが確認できます。

slotがJohn doeに置き換わる
slotがJohn doeに置き換わる

これが最も簡単なSlotの使用方法でslotタグを利用することで親コンポーネントから子コンポーネントにコンテンツが渡せることがわかりました。

Slotのデフォルト値を入れる

子コンポーネントのslotタグの間にデフォルト値を入れておくことで親コンポーネント側でuserタグの間にコンテンツを入れない場合にslotタグに入れたデフォルト値を表示させることができます。


<template>
  <p>この人の名前は<slot>名無しの権兵衛</slot>です。</p>
</template>

<script>
export default {
}
</script>

今回は、App.vue側のuserタグの間には何もコンテンツを入れません。


<template>
  <user></user>
</template>

何も親からコンテンツが渡されない場合は、そのままslotタグの間に入れたデフォルト値がブラウザ上に表示されます。

デフォルト値が表示
デフォルト値が表示

デフォルト値を設定した場合でも親側のuserタグの間にコンテンツを入れた場合は親側に入れたコンテンツが表示されます。


<template>
  <user>John Doe</user>
</template>
slotがJohn doeに置き換わる
userタグの間にJohn doeを入れている場合

1つのコンポーネントに複数のSlotが存在する場合(Named Slot)

先程の例ではUser.vueコンポーネントに1つだけSlotが存在しました。1つのコンポーネントに複数のSlotを設定した場合はどのように行えばいいか確認していきます。

複数のSlotを設定したい場合は各Slotに名前をつけることで識別可能となり、親コンポーネントもその名前を元に別々のコンテンツを渡すことができます。Slotに名前をつけるのでNamed Slotと呼ばれます。

User.vueに3つのslotを設定します。slotタグの2つにはname属性がついていますが、1つは何もついていません。


<template>
  <ul>
    <li>名前:<slot>名無しの権兵衛</slot></li>
    <li>年齢:<slot name="age">記入なし</slot></li>
    <li>性別:<slot name="sex">不明</slot></li>
  </ul>
</template>

親側のコンポーネントを確認します。templateタグの中でv-slotを使ってslotの名前を指定すると子コンポーネント側で設定したname属性の値に対応させることができます。templateタグに入っていないJohn Doeがname属性がなかったslotに対応します。


<template>
  <user>
    John doe 
    <template v-slot:age>25</template>
    <template v-slot:sex>男性</template>
  </user>
</template>

<script>
import User from './components/User.vue'
export default {
 name: 'app',
 components: {
  User,
 },
}
</script>

ブラウザで確認すると下記のように表示されます。

slotに名前をつけた場合
slotに名前をつけた場合

もし子コンポーネントのname属性のないslotを親コンポーネントから明示的に指定したい場合は、defaultを下記のように使うことで対応可能です。


<template>
  <user>
    <template v-slot:default>John doe</template> 
    <template v-slot:age>25</template>
    <template v-slot:sex>男性</template>
  </user>
</template>
v-slotはvue.jsの2.6+から使用できる記述方法です。それ以前のバージョンではslot=”age”のようにslot属性を利用します。2.6+のバージョンではこの記述方法を使用することも可能です。

【以前の記述を使用した場合】


<template>
  <user>
    <template slot="default">John doe</template> 
    <template slot="age">25</template>
    <template slot="sex">男性</template>
  </user>
</template>

v-slotの別の記述方法(#)

Slotでは以前のバージョンや記述方法がいくつかあるので慣れるまでは混乱するかもしれません。v-slotは#に置き換えることも可能です。


<template>
  <user>
    John doe
    <template #age>25</template>
    <template #sex>男性</template>
  </user>
</template>

Slotの使用方法の中級編

ここまでのSlotの利用方法であれば理解するのに難しい箇所はなかったと思います。ここからは少し複雑になってきますができるだけシンプルなコードを利用して説明していきます。ここからのSlotの使い方を理解していない人はVuetifyなどのパッケージや他の人が記述したコードでSlotを見つけた時に混乱する可能せいが高いのでそのような時にも混乱しなようにしっかり理解しておきましょう。

Scoped Slot(スコープ付きSlot)

Scoped Slotを利用すると子コンポーネント側で使用しているデータプロパティを親コンポーネントでも使用できるようになります。

これまでの説明では一度も子コンポーネント側のデータプロパティの話は出ていないので上記の文章の意味は理解できないかと思います。動作を確認すれば記述している意味がわかると思いますのでvue.jsの公式ドキュメントの例を参考に説明していきます。

まず、子コンポーネントUser.vueで以下のようにデータプロパティdataにuserプロパティを追加してslotのデフォルト値をuser.lastNameとします。


<template>
  <span>
    <slot>{{ user.lastName }}</slot>
  </span>
</template>

<script>
export default {
  data() {
    return{
      user:{
        firstName: 'John',
        lastName: 'Doe',
        age: '25',
        sex: '男性',
      },
    }
  }
}
</script>

先程までと同様に親コンポーネントからUser.vueをインポートします。親コンポーネントではuserタグの間にはなにもコンテンツを入れません。


<template>
  <user></user>
</template>

<script>
import User from './components/User.vue'
export default {
 name: 'app',
 components: {
  User,
 },
}
</script>

ブラウザで確認するとuserのlastNameが表示されます。

userプロパティのlastNameが表示
userプロパティのlastNameが表示

子コンポーネントでデータプロパティを追加しデフォルトのSlot値を利用しましたという違いがありますが、これまで説明してきたSlotの使い方と同じなのでここまでは問題ないかと思います。

ここで親コンポーネントから子コンポーネントのuserにアクセスしたいという要望があったとします。その場合にどのように設定を行なっていくかを説明していきます。

何も根拠はないかもしれませんが、親コンポーネントで下記のようにそのまま{{ }}を使えば子コンポーネントのuserの情報が表示されるのではないかと思った人もいるかもしれませんが下記のように記述しても何も表示されません。


<template>
  <user>
    {{ user.firstName }}
  </user>
</template>

この問題を解決するためにScoped Slotを利用します。子コンポーネントではv-bind、親コンポーネントではv-slotを利用することで親コンポーネントでuserにアクセスすることができます。

子コンポーネントUser.vueではslotタグの中でv-bindでデータプロパティuserをバインドします。


<template>
  <span>
    <slot v-bind:user="user">
    {{ user.lastName }}
    </slot>
  </span>
</template>

親コンポーネント側ではv-slot:defaultを利用してデータを受け取ります。


<template>
  <user v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </user>
</template>

ブラウザで確認すると子コンポーネントで定義したuserのfirstNameが表示されることが確認できます。子コンポーネントではSlotのデフォルト値をuserのlastNameに設定していたので親からのコンテンツで上書きされています。

子コンポーネントのuserのfirstName
子コンポーネントのuserのfirstName

slotPropsがなにか気になると思うので確認していきます。

slotPropsという名前を使っていますがこの名前は任意なので変更を行いその値全体を表示させてましょう。


<template>
  <user v-slot:default="dataFromChild">
    {{ dataFromChild }}
  </user>
</template>

dataFromChildに入っている値は下記のようになります。

子コンポーネントからのデータを表示
子コンポーネントからのデータを表示

子コンポーネントからデータを渡すため直接userを設定すればいいのではと思いますがdataFromChildのような任意の名前の変数を一つ挟む必要があります。

複数のデータプロパティを渡したい場合はこの1つの変数にすべてのデータが入って渡されます。後ほど説明を行います。

dataFromChildにはuserデータ全体が入っているのでageにもsexにも親コンポーネントからアクセスすることが可能になります。


<template>
  <user v-slot:default="dataFromChild">
    <p>{{ dataFromChild.user.firstName }} {{ dataFromChild.user.lastName }}</p>
    <p>{{ dataFromChild.user.age}}/{{ dataFromChild.user.sex}}</p>
  </user>
</template>
user情報を表示
user情報を表示
v-slot:default=””については子側のSlotに名前がついていないものと対応する場合は、v-slot=”任意の名前”とdefaultを抜いて記述することも可能です。

別の記述方法({ })

dataFromChildを使っていましたが、dataFromChildを省略して以下のように記述することも可能です。渡される変数の名前がわかっている場合はこのほうが短いコードで記述することが可能になります。


<template>
  <user v-slot:default="{ user }">
    <p>{{ user.firstName }} {{ user.lastName }}</p>
    <p>{{ user.age}}/{{ user.sex}}</p>
  </user>
</template>

複数のデータプロパティがある場合

子コンポーネント側で複数のデータプロパティをバインドした場合はどうなるか見てみましょう。新しくデータプロパティmessageを追加し、slotタグでmessageもv-bindしています。


<template>
  <span>
    <slot v-bind:user="user" v-bind:message="message">
    {{ user.lastName }}
    </slot>
  </span>
</template>

<script>
export default {
  data() {
    return{
      user:{
        firstName: 'John',
        lastName: 'Doe',
        age: '25',
        sex: '男性',
      },
      message:'Hello Scoped Slot',
    }
  }
}
</script>

親コンポーネント側ではuserもmessageもdataFromChildで受け取ることが可能です。


<template>
  <user v-slot="dataFromChild">
    <p>{{ dataFromChild }}</p>
  </user>
</template>

dataFromChildの中にuserもmessageも確認することができます。

userとmessageを確認
userとmessageを確認
v-slot:default=”{ user,message }”でも子コンポーネントから値を受け取ることができます。
Scoped Slotの説明と直接関係ありませんが、v-bindも下記のように省略して記述することができます。

<template>
  <span>
    <slot :user="user" :message="message">
    {{ user.lastName }}
    </slot>
  </span>
</template>

Scoped Slotではmethods(関数)も渡せる

ここまではScoped Slotを利用して子コンポーネントのデータプロパティのみを親に渡してきました。Scoped Slotはデータプロパティだけではなくmethods(関数)も渡すことができます。

ボタンをクリックするとリストが表示/非表示と切り替わるコンポーネントMenu.vueファイルを作成します。ボタンにクリックイベントを設定し、ボタンをクリックすることでデータプロパティdisplayがtrue, falseと切り替わりv-ifで表示/非表示と変わります。


<template>
  <div>
    <button @click="on">Click</button>
    <slot v-if="display">
    </slot>
  </div>
</template>

<script>
export default {
  data() {
    return{
      display: true
    }
  },
  methods: {
    on(){
      return this.display = !this.display;
    }
  }
}
</script>

slotに入るリストのコンテンツはslotを利用して親のコンポーネントから渡します。


<template>
  <Menu>
    <ul>
      <li>List1</li>
      <li>List2</li>
      <li>List3</li>
    </ul>
  </Menu>
</template>

<script>
import Menu from './components/Menu.vue'
export default {
 name: 'app',
 components: {
  Menu,
 },
}
</script>

ブラウザで確認すると下記のような画面になります。

リストが表示
リストが表示

クリックボタンを押すと表示されていたリストが非表示になります。再度ボタンを押すとリストが表示されます。

クリックでリストが非表示
クリックでリストが非表示

ここまでは子コンポーネントに設定したボタンとメソッドを利用して表示・非表示を切り替えているので理解ができるかと思います。次は、Scoped Slotを利用して”親側”から表示・非表示を切り替えれるように変更します。

Menu.vueファイルのbuttonタグを削除して、slotタグを追加します。slotタグにはname属性を設定しその値はactivatorにします。さらにslotタグの中ではv-bindでonメソッドを指定します。


<template>
  <div>
    <slot name="activator" v-bind:on="on">
    </slot>
    <slot v-if="display">
    </slot>
  </div>
</template>

App.vueではtemplateタグを追加して、v-slotでMenuで追加したslotのactivatorを指定します。buttonタグも追加したクリックイベントを設定し、実行するメソッドはScoped Slotを経由して渡されたonメソッドです。


<template>
  <Menu>
    <template v-slot:activator="{ on }">
      <button v-on:click="on">
        click it
      </button>
    </template>
    <ul>
      <li>List</li>
      <li>List2</li>
      <li>List3</li>
    </ul>
  </Menu>
</template>
Vuetifyを使ったことがある人ならv-menuでドロップダウンメニューを行いたい時に使用するv-slot:activatorの形に似ていることがわかるかと思います。実際にVuetifyのコードを読んでいるわけではありませんので見た目だけ注目しています。

先程と見た目は変わりませんが、親コンポーネントで設定したボタンを使ってリストの表示・非表示を切り替えることができます。

リストが表示
親コンポーネントのボタンに変更

データプロパティだけではなくメソッドも渡せることがわかりました。

Scoped Slotを利用してテーブルをカスタマイズ

Scoped Slotの利用方法を確認するためにシンプルなコードでテーブルをカスタマイズする方法を確認していきます。

再利用可能な子コンポーネントTable.vueを作成します。親のコンポーネントからheaders, items情報の入った配列をpropsを受け取りテーブルとして表示させます。


<template>
  <table border="1">
    <thead>
      <tr>
        <th v-for="row_name in header" :key="row_name">{{ row_name }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item,index) in items" :key="index">
          <td v-for="(value,index) in item" :key="index">{{ value }}</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'Table',
  props:{
    header:{
      type: Array,
      required: true,
    },
    items:{
      type: Array,
    }
  },
}
</script>

親側では、子コンポーネントにわたすheaders, usersを記述しておきます。通常であればusersはaxios等を利用して取得します。Slotは利用していません。


<template>
  <Table :header="header" :items="items"></Table>
</template>

<script>
import Table from './components/Table.vue'
export default {
  name: 'app',
  components: {
    Table,
  },
  data(){
    return {
      header:['ID','FIRSTNAME','LASTNAME','EMAIL'],
      items: [
        {
          id: 1,
          firstName: 'John',
          lastName: 'Doe',
          email: 'john@example.com'
        },        
        {
          id: 2,
          firstName: 'Kevin',
          lastName: 'Wood',
          email: 'kevin@example.com'
        },       
        {
          id: 3,
          firstName: 'Harry',
          lastName: 'Bosch',
          email: 'harry@test.com'
        },
      ],
    }
  }
}
</script>

ブラウザで確認するとユーザ一覧が表示されます。

ユーザ一覧を表示
ユーザ一覧を表示

もしここでFIRSTNAMEとLASTNAMEをNAMEとして表示させたいという要望があった場合どのように対応しますか?

Table.vueを直接書き換えることで対応可能ですが、もしこのTable.vueを他のコンポーネントでも利用していたらそのテーブルの表示にも影響を与えてしまいます。

ここでScoped Slotを利用します。

Table.vue側でslotタグを追加しv-bindでitemを指定します。slotタグの中にはデフォルト値としてそのままtdタグを残します。


<template>
  <table border="1">
    <thead>
      <tr>
        <th v-for="row_name in header" :key="row_name">{{ row_name }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item,index) in items" :key="index">
        <slot v-bind:item="item">
          <td v-for="(value,index) in item" :key="index">{{ value }}</td>
        </slot>
      </tr>
    </tbody>
  </table>
</template>

親側ではv-slotを利用して子コンポーネントからitemを受け取ります。受け取ったitemをtemplateタグの中で展開します。


<template>
  <Table :header="header" :items="items">
    <template v-slot:default="{ item }">
      <td>{{ item.id }}</td>
      <td>{{ item.firstName }} {{ item.lastName }}</td>
      <td>{{ item.email }}</td>
    </template>
  </Table>
</template>

テーブルのヘッダー情報も忘れずに更新しておきます。


data(){
  return {
    // header:['ID','FIRSTNAME','LASTNAME','EMAIL'],
    header:['ID','NAME','EMAIL'],

ブラウザで確認するとSlotを利用することで親側で記述したテーブル情報が表示されます。

Slotを利用してテーブルを表示
Slotを利用してテーブルを表示

もし何も変更が必要ない場合は、親側のtemplateタグを削除し、headerを元に戻せばテーブルが子コンポーネントのSlotタグの中に記述したデフォルト設定のまま表示されます。

親側のコンポーネントでSlotで渡すコンテンツを変更することでカスタマイズ可能なテーブルコンポーネントを作成することができました。

this.$slotsからSlotの値を取得

this.$slotsを利用するとSlotに関する情報を取得することができます。

BlogPost.vueファイルを作成します。computedプロパティを使ってthis.$slotsに入っている値を取得します。


<template>
  <div>
    <h1><slot name="title">Title</slot></h1>
    <p><slot></slot></p>
    <p>Slot:{{ slotValue }}</p>
  </div>
</template>
<script>
  export default{
    computed:{
      slotValue(){
        return this.$slots;
      }
    }
  }
</script>

App.vueファイルではSlotにコンテンツを入れます。


<template>
  <div>
  <BlogPost>
    <template #title>vue.js</template>
    I'd like to know about $vm.slots.
  </BlogPost>
</div>
</template>

<script>
import BlogPost from './components/BlogPost.vue'
export default {
  name: 'app',
  components: {
    BlogPost,
  },
}
</script>

ブラウザで確認すると下記のようにSlotの情報が表示されます。

vm.$slotsから取得できる情報
vm.$slotsから取得できる情報

デフォルトのSlotの値は、this.$slots.default[0].textで取得できることがわかります。もしApp.vueの親コンポーネントのSlotに何も入れない場合は、this.$slotsの値は空のオブジェクト{}になります。