突然ですが、Vuetifyを使っているとSlotを活用してかゆいところに手を届かせることってよくあると思います。
しかし私は今まで、Slotという合言葉を指定するといい感じにVuetifyが動くなぁと言うだけの認識でした・・・。
(Vuetifyでしかslot使ったことなかったんです・・・)
そのSlotってどう使うのかが改めて気になったので、まとめてみたという記事です。
(以前社内ブログで書いたものを再掲してます。)
Slotってなんだよ
そもそもSlotとは。
Vue.jsが用意してくれている、コンポーネントをより便利にしてくれる機能の一つです。
何がスロットなのかと言うと、パチンコ屋さんにあるスロットとかではなくて
「差し込み口」という意味でのスロットになります。
具体的な機能としては、
コンポーネントに対してtemplateタグを差し込めるようにしてくれる機能です。
難しく言うと、親コンポーネントから子コンポーネントにコンテンツを渡して表示させることができる機能です。
こんな感じで使えます。
<!-- 適当に用意したスロットが用意されているコンポーネント -->
<template>
<div>
<p>今日の晩御飯は<slot>なし</slot>です</p>
</div>
</template>
<!-- スロットを持つコンポーネントを呼び出すコンポーネント -->
<template>
<div>
<SlotSample>たまごやき<SlotSample>
</div>
</template>
このParentSample.vueファイルを表示すると画面に
「今日の晩御飯はたまごやきです」
と表示されます。
SlotSampleのslotタグの場所が、ParentSampleでSlotSampleを呼び出すときに書いた内容で置き換えられているのがわかります。
このように、後付けでtemplateを差し込む余地が用意されていることで、コンポーネントをとても柔軟に使えるようにしてくれています。
そしてもしParentSampleで呼び出すときに「たまごやき」を書かない時、
つまり以下の例のような場合だと、「今日の晩御飯はなしです」というようにSlotSample内に元々書かれている値が表示されます。
<!-- スロットには何も差し込まずコンポーネントを呼び出す -->
<template>
<div>
<SlotSample></SlotSample>
</div>
</template>
<!--「今日の晩御飯はなしです」と表示される-->
まとめると、
とりあえず基本的にはSlotに差し込むのはtemplateだと押さえておけば大丈夫です。
名前付きSlotとかいう便利なやつ
Slotというやつには、親が子コンポーネントを呼び出すときにtemplateを差し込めることがなんとなく掴めたかなぁと思います。
そして今から説明する名前付きSlotという機能を理解していると、
Vuetifyが用意している便利機能はこうやってあれこれしてるんだなというのがわかります。(Vuetifyに限らずslotを持っているコンポーネント全般に言えますが・・・)
まず、名前付きSlotとは。
簡単に言うと、先ほどのtemplateの差し込み口に対して名前がついている機能のことです。
まぁそのまんまですね。
例としてはこんな感じです。
<!-- スロットにdinnerという名前をつけてみる -->
<template>
<div>
<p>今日の晩御飯は<slot name="dinner">なし</slot>です</p>
</div>
</template>
<!-- 名前付きスロットを呼び出すコンポーネント -->
<template>
<div>
<SlotSample2>
<template v-slot:dinner> たまごやき </template>
</SlotSample2>
</div>
</template>
Slotをもつ子コンポーネント側(SlotSample2)で、nameプロパティを指定します。
この時につける名前はなんでもいいです。
そして親コンポーネント(ParentSample2)ではtemplateタグ内でv-slotプロパティで、名前付きスロットの名前を指定します。
Slotに差し込むtemplateに対して、差し込み先の名前を教えてあげてる感じですね。
これで画面には「今日の晩御飯はたまごやきです」と表示されます。
そして名前付きスロットはいくつでもコンポーネント内で作っておけますので、こんなことになってきます。
<!-- 名前付きスロットを複数持つコンポーネント -->
<template>
<div>
<p>朝ごはんは<slot name="breakfast">抜き</slot>です</p>
<p>昼ごはんは<slot name="lunch">水だけ</slot>です</p>
<p>晩御飯は<slot name="dinner">なし</slot>です</p>
</div>
</template>
<!-- 名前付きスロットを呼び出すコンポーネント -->
<template>
<div>
<SlotSample2>
<template v-slot:breakfast> トースト </template>
<template v-slot:dinner> たまごやき </template>
</SlotSample2>
</div>
</template>
上記の例だと画面には
「朝ごはんはトーストです」「昼ごはんは水だけです」「晩御飯はたまごやきです」
と表示されます。
対応するSlotの名前の指定の方法と、指定しなかった時の動きがなんとなくわかるはずです。
(今回はlunchのスロットのみ指定していないので、デフォルトの値が表示されている)
そしてVuetifyには各コンポーネントにたくさん名前付きSlotが用意されているんですよね。
名前付きスロットを理解しておけば、それらを使いこなせるようになります。
公式ドキュメントのSlotの紹介欄(上記リンク参照)に、このv-data-tableで使える名前付きSlotが紹介されています。
実際にここに載っていたno-dataという名前付きSlotを使うと、
<!-- testDataが表示できるデータなら表示されるが、データがない時は「データがありませんと表示される -->
<template>
<div>
<v-data-table :items="testData">
<template #no-data> データがありません。 </template>
</v-data-table>
</div>
</template>
v-ifなどで場合分けせずとも、簡単にデータがない場合の処理が実装できます。 #
ちなみにv-slotは # で省略できる。
省略記法あります。突然この書き方されてもびっくりしないでください。
僕は初見時、何かわかりませんでした・・・
<!-- v-slotの箇所を # を使って省略する -->
<template>
<v-data-table :items="testData">
<template #no-data> データがありません。 </template>
</v-data-table>
</template>
デフォルトスロットについて一応補足
<template>
<div>
<p>今日の晩御飯は<slot>なし</slot>です</p>
</div>
</template>
今から上記のSlotSample3を呼び出す2つの例を書いてみます。
そしてその2つの内容は同じだという話をします。
<!-- スロットを持つコンポーネントを呼び出すコンポーネント -->
<template>
<div>
<SlotSample3>たまごやき</SlotSample3>
</div>
</template>
<template>
<div>
<SlotSample3>
<template v-slot:default> たまごやき </template>
</SlotSample3>
</div>
</template>
<!-- 実は実行時にVue.jsが色々補ってくれている -->
Slotはtemplateタグを差し込むことができる機能だという話を最初の方にしました。
この原則は変わりませんが、templateなどを省略できちゃう書き方があります。
まず名前をつけないSlotを用意した場合(今回だとSlotSample3.vue)、それにはVueが勝手にdefaultという名前をつけています。
そして呼び出し側では、Slotに対して何も名前を指定しない場合には勝手にdefaultという名前付きSlotを呼び出していると認識されます。
なんなら勝手にtemplateまで補ってくれるので、defaultという名前の省略とともにtemplateすら書かなくても大丈夫です。
もう一度さっきの例の省略具合を見比べてみてください。
一応以下のように名前付きSlotの呼び出しと絡めるとこんな感じになります。
<!-- 名前付きスロットを複数持つコンポーネント -->
<template>
<div>
<p>朝ごはんは<slot>抜き</slot>です</p>
<p>晩御飯は<slot name="dinner">なし</slot>です</p>
</div>
</template>
<!-- 名前付きスロットを呼び出すコンポーネント -->
<template>
<div>
<SlotSample3>
<template v-slot:dinner> たまごやき </template>
トースト
</SlotSample3>
</div>
</template>
以下のように補完されます。
(今回「トースト」の文字列をあえて下のほうに持ってきましたが、SlotSampleタグ内だとどこに書いても問題なく補完されます)
<!-- 一応こっちの補完され具合も書いておきます -->
<template>
<div>
<p>朝ごはんは<slot name="default">抜き</slot>です</p>
<p>晩御飯は<slot name="dinner">なし</slot>です</p>
</div>
</template>
<!-- いい感じにtemplateと名前を補完してくれる -->
<template>
<div>
<SlotSample3>
<template v-slot:dinner> たまごやき </template>
<template v-slot:default> トースト </template>
</SlotSample3>
</div>
</template>
省略した方がすっきり見えますが、
もちろん自分でtemplateタグを用意してdefaultと書いてあげた方が可読性が上がる場合もあると思いますので、いい感じに使い分けてください。
一応補足としてdefaultという名前付きスロットが省略可能なだけで、
(省略というより、Vueが識別するために仕方なく補ってるに近いかも)
基本的にどのSlotも名前付きSlotなんだよ、というややこしい話をしました。
スロットプロパティとかいうややこしいやつ
この機能はマニアックかなと思うので、あまり使う機会はないような気がしますが、
一応Slotが機能として持っているので紹介しておきます。
どんな機能かというと、子コンポーネントのデータをSlotを通じて親コンポーネントに渡せることができるというものです。
(基本的には$emitとかでやり取りすれば問題ないとは思うので、マニアックな状況で使うんだと思います)
そんな難しくはないです。
<!-- スロットに好きな名前でバインドし、渡したいデータを設定する。今回はtestと名付けました -->
<template>
<div>
<p>
子から親に渡されたデータは
<slot name="testSlot" :test="testData">なかった</slot>です
</p>
</div>
</template>
<script>
export default {
// とりあえず適当なデータを作ってます。
data() {
return {
testData: 'TestTest'
}
}
}
</script>
<!-- v-slotの属性に=を付け、好きな名前を指定するとそこに子からデータが飛んでくる -->
<template>
<div>
<SlotSample4>
<template v-slot:dinner="slotProps">
{{ slotProps }}
</template>
</SlotSample4>
</div>
</template>
<!-- 「子から親に渡されたデータはTestTestです」と表示されます -->
なんとなくわかりますかね・・・・
子のSlotから渡したいデータを、好きな名前をつけてバインドします。
(今回でいうところの:test。書き方を省略しなければv-bind:testです。バインドっていうのは実際の値をその箇所に紐づけるみたいな意味です。)
このようにバインドしておけば、その名前付きスロットのtemplateタグでv-slotの属性で受け取ることができます。
受け取るときの名前はなんでもいいので、好きに指定してあげてください。(今回はslotPropsと名付けてます)
少しややこしいですが、こんな感じで名前付きSlot単位でデータを渡せます。
使い方としては、こっちのSlotでは渡したデータを表示してもらって、こっちのSlotはデフォルトのデータを表示させたいみたいな時に使えそうではありますね。(想像力の限界・・・)
あとデフォルトSlotしかない時はこんな風に書けばオッケーです。
<!-- スロットに名前をつけずに値をバインドして、渡したいデータを設定する -->
<template>
<div>
<p>子から親に渡されたデータは<slot :test="testData">なかった</slot>です</p>
</div>
</template>
<script>
export default {
// とりあえず適当なデータを作ってます。
data() {
return {
testData: 'TestTest'
}
}
}
</script>
<!-- 名前付きスロットではないスロットからスロットプロパティを受け取る -->
<template>
<div>
<SlotSample4>
<template v-slot:default="slotProps">{{ slotProps }}</template>
</SlotSample4>
</div>
</template>
<!-- 「子から親に渡されたデータはTestTestです」と表示されます -->
まぁスロットに名前がないとdefaultと勝手に名付けられるから、それを指定してあげてねというだけの話です。
ちなみにデフォルトスロットしかない場合は、ParentSample.vueは以下のように省略できます。
<!-- templateタグを省略してコンポーネントのタグに直接プロパティを指定できる -->
<!-- ※この省略はデフォルトスロットしかない場合に限るので注意 -->
<template>
<div>
<SlotSample4 v-slot="slotProps">
{{ slotProps }}
</SlotSample4>
</div>
</template>
<!-- 「子から親に渡されたデータはTestTestです」と表示されます -->
<!-- 補足:v-slot:default="slotProps"は#default="slotProps"と書くとさらに省略できます -->
渡せる値もバインドを増やせばいくつでも親で受けられますが、受け口は一つです。
(子で値を3つバインドして親に渡しても親では、今回で言うslotPropsのところに全部入ってきます。)
あと、一応関数とかも渡せます。
終わり
以上でざっとVue.jsが用意してくれているスロットという機能について書いてみました。
Vuetifyでのslot利用や、汎用的なコンポーネントを手作りする場合などに役立ててください。
参考
・https://jp.vuejs.org/v2/guide/components-slots.html
・https://reffect.co.jp/vue/vue-js-slot-scoped-slot