vue.jsのSlotからScoped Slotまでを理解しよう
vue.jsではSlotを利用することで親コンポーネントから子コンポーネントにコンテンツを渡して表示させることができます。例えばボタンというコンポーネントを作成し、ボタン上に表示されるテキストのみ変更したい場合、親コンポーネントからSlotを使ってテキストを渡すことで子コンポーネントのボタン上に表示させるテキストを変更することができます。ボタン上のテキストを変更できることでボタンコンポーネントを再利用することが可能となります。
本文書ではSlotのみに特化して説明を行っているので本文書を読み終えるとSlot、Named Slot、データプロパティを使ったScoped Slot, メソッドを使ったScoped Slotの4つを理解することができます。
前半はSlot, Named Slotを使って親コンポーネントから子コンポーネントにコンテンツを渡す方法を確認し、後半ではScoped Slotを中心に子コンポーネントのデータプロパティとメソッドをSlotを使って親コンポーネントに渡す方法を確認していきます。特にVue.jsの入門者の人はこの機会に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コンポーネント(<user></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の使用方法で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>
1つのコンポーネントに複数のSlotが存在する場合(Named Slot)
先程の例ではUser.vueコンポーネントに1つだけSlotが存在しました。1つのコンポーネントに複数のSlotを設定したい場合はどのように行えばいいか確認していきます。
複数のSlotを設定したい場合は各Slotに名前をつけることで識別可能となり、親コンポーネントもその名前を元に別々のコンテンツを渡すことができます。Slotに名前をつけるのでNamed Slot(名前付き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>
ブラウザで確認すると下記のように表示されます。
もし子コンポーネントの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>
【以前の記述を使用した場合】
<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の利用方法は少し複雑になってくるため説明しているこちら側も混乱してしまいそうですができるだけシンプルなコードを利用して説明していきます。
ここからのSlotの使い方を理解していない人はVuetifyなどのパッケージや他の人が記述したコードでSlotを見つけた時に何を行っているかわからない可能性が高いのでそのような時にも混乱しなようにしっかり理解しておきましょう。
Scoped Slot(スコープ付きSlot)
Scoped Slotを利用すると子コンポーネント側で使用しているデータプロパティを親コンポーネントでも使用できるようになります。これまではSlotは親コンポーネントから子コンポーネントにコンテンツを渡すというものでした。これまでの逆で子コンポーネントから親コンポーネントにデータプロパティを渡すことができます。
これまでの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が表示されます。
子コンポーネントにデータプロパティを追加しデフォルトのSlot値をデータプロパティを利用して設定したという違いがありますが、これまで説明してきたSlotの使い方と同じなのでここまでは理解もできるかと思います。
ここで親コンポーネントから子コンポーネントのuserプロパティにアクセスしたいという要望があったとします。その場合にどのように設定を行なっていくかを説明していきます。
ここで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に設定していたので親からのコンテンツで上書きされています。
slotPropsがなにか気になると思うので確認していきます。
slotPropsという名前を使っていますがこの名前は任意なので変更を行いその値全体を表示させてましょう。
<template>
<user v-slot:default="dataFromChild">
{{ dataFromChild }}
</user>
</template>
dataFromChildに入っている値は下記のようになります。
子コンポーネントからデータを渡すため直接userを設定すればいいのではと考えてしまいますがdataFromChildのような任意の名前の変数を一つ挟む必要があります。
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>
別の記述方法({ })
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も確認することができます。
<template>
<span>
<slot :user="user" :message="message">
{{ user.lastName }}
</slot>
</span>
</template>
ここまでの説明で子コンポーネントのデータプロパティを親コンポーネントに渡したい時はScoped Slotが使えるということがわかりました。
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タグを追加しクリックイベントを設定します。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>
先程と見た目は変わりませんが、親コンポーネントで設定したボタンを使ってリストの表示・非表示を切り替えることができます。
データプロパティだけではなくメソッドも渡せることがわかりました。
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等を利用してサーバなどの外部リソースから取得します。
<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を利用することで親側で記述したテーブル情報が表示されます。
もし何も変更が必要ない場合は、親側のtemplateタグを削除し、headerを元に戻せばテーブルが子コンポーネントのSlotタグの中に記述したデフォルト設定のまま表示されます。
親側のコンポーネントで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の情報が表示されます。
デフォルトのSlotの値は、this.$slots.default[0].textで取得できることがわかります。もしApp.vueの親コンポーネントのSlotに何も入れない場合は、this.$slotsの値は空のオブジェクト{}になります。
まとめ
ここまで読み進めることができればかなりSlotの理解も深まっているのではないでしょうか。特にScoped Slot機会があればぜひ使ってみてください。