Vue.jsのslotあれこれ

Vue.jsのslot機能について勉強したので，備忘録的に書いてきます．
## はじめに
そもそもslotとは何か．
Vue.jsにおけるslotは，親コンポーネントから小コンポーネントにテンプレートを差し込むことができる機能です．
## 名前付きslot
例えば，タイトル，本文，ボタンなどを要素として持つ以下のようなarticleというコンポーネントをApp.vueから呼び出す場合を考えてみます．

```vuejs:article.vue
<template>
  <div style="width=500px;">
    <h1>タイトル</h1>
    <p>本文</p>
    <button>ボタン</button>
  </div>
</template>

<script>
export default {
  name: 'Article',
}
</script>
```
```vuejs:App.vue
<template>
  <Article />
</template>

<script>
import Article from './components/article.vue';

export default {
  name: 'App',
  components: {
    Article,
  }
}
</script>
```
これらのファイルは，現状で以下のように動作します．
![スクリーンショット 2021-06-22 15.51.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/0a0fbe59-665d-bcca-3807-0201845e7953.png)
しかし，article.vueを繰り返し使って複数の記事を作成したい場合，呼び出す際にタイトルや本文，ボタンの文字を指定できたら便利ですよね．
それを可能にするのがslotです．上記のファイルを以下のように書き換えてみましょう．

```vuejs:article.vue
<template>
  <div style="width=500px;">
    <h1>
      <slot name="title"><!-- ここにタイトルが差し込まれます --></slot>
    </h1>
    <p>
      <slot name="body"><!-- ここに本文が差し込まれます --></slot>
    </p>
    <button>
      <slot name="buttonText"><!-- ここにボタンテキストが差し込まれます --></slot>
    </button>
  </div>
</template>

<script>
export default {
  name: 'Article',
}
</script>
```
```vuejs:App.vue
<template>
  <Article>
    <template v-slot:title>
      スロットで差し込んだタイトルです
    </template>
    <template v-slot:body>
      スロットで差し込んだ本文です
    </template>
    <template #buttonText> <!-- v-slotは省略記法として#で置き換えることができます -->
      スロットで差し込んだボタンテキストです
    </template>
  </Article>
</template>

<script>
import Article from './components/article.vue';

export default {
  name: 'App',
  components: {
    Article,
  }
}
</script>
```

実行結果はこんな感じ
![スクリーンショット 2021-06-22 16.30.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/0b379bf6-45c5-68cf-e3a1-2a4d1ced4289.png)

このように，子コンポーネントで親からテンプレートを受け取りたい部分に`<slot name="hogehoge">`と書き，親コンポーネントで`<template v-slot:hogehoge>`と差し込みたいスロットの名前を指定してあげることで可読性も高く，汎用性の高いコンポーネントを作ることができます．
v-slotは，v-bindにおける：，v-onにおける@のように省略記法を持っており，#を使って省略することができます．

```vuejs
<template #buttonText> <!-- v-slotは省略記法として#で置き換えることができます -->
  スロットで差し込んだボタンテキストです
</template>
```
このように，差し込みたい箇所（slot）に名前をつけ，親コンポーネントからslotを指定されるslotを名前付きslotと言います．

## デフォルトスロット
名前付きslotに対して，名前のないslotも作ることができます．名前のないslotはdefault slotとも呼ばれます．先程の例では，テンプレートを差し込みたい箇所が複数ありましたが，一つで良い場合などにdefault slotを使います．
以下のような`<a>`タグをラップしたようなコンポーネント，navigation-link.vueを用意します．

```vuejs:navigation-link.vue
<template>
  <a :href="url">
    <slot><!-- slotが一つだけなのでname属性がいらない　--></slot>
  </a>
</template>

<script>
export default {
  name: 'NavigationLink',
  props: {
    url: String,
  },
}
</script>
```
```vuejs:App.vue
<template>
  <navigation-link url="https://www.google.com/">googleへのリンク</navigation-link>
</template>

<script>
import NavigationLink from './components/navigation-link.vue';

export default {
  name: 'App',
  components: {
    NavigationLink,
  },
}
</script>
```
実行結果はこんな感じ
![スクリーンショット 2021-06-22 19.52.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/bec47bc9-35ec-f8ec-aef1-54cfea91adf9.png)
navigation-link.vueのように，スロットが一つだけの場合，name属性をつけないことでdefault slotになります．また，name属性に明示的に`default`を指定することでも同様に，default slotにすることができます．親コンポーネント側でもslotの名前を指定する必要はありません．
また，名前付きslotがある場合にも，名前なしslotを利用することができます．

```vuejs:navigation-link.vue
<template>
  <div style="width=500px;">
    <h1>
      <slot name="title"></slot>
    </h1>
    <h2>
      <slot name="subTitle"></slot>
    </h2>
    <button>
      <slot name="buttonText"></slot>
    </button>
    <p style="color: red;">
      <slot><!-- デフォルトスロット --></slot><!-- slot name="default"でも同様 -->
    </p>
  </div>
</template>
```
## slotのデフォルトコンテンツ（フォールバックコンテンツ）
子コンポーネントに用意されたスロットに，何も渡されなかった場合に表示されるコンテンツを，子コンポーネントであらかじめ記述することができます（公式ドキュメントではこのようなコンテンツのことをフォールバックコンテンツと呼んでいます）．記述する方法は簡単で，小コンポーネントの`<slot></slot>`タグの間にあらかじめコンテンツを書いておくだけで良いです．

```vuejs
<slot name="hogehoge">デフォルトコンテンツ</slot>
```
こうすることでhogehogeスロットにコンテンツが渡されなかった場合には”デフォルトコンテンツ”という文字が表示され，親コンポーネントで何かしらのコンテンツをhogehogeスロットに渡すとそのコンテンツでslot内を書き換えます．


## スコープ付きスロット
親コンポーネントで小コンポーネントへ渡すテンプレートを記述する際に，子コンポーネントが持っているデータを用いて記述したい場合もあると思います．article.vueを以下のように書き換えてみます．

```vuejs:article.vue
<template>
  <div style="width=500px;">
    <h1>
      <slot name="title"></slot>
    </h1>
    <h2>
      <slot name="subTitle"></slot>
    </h2>
    <button>
      <slot name="buttonText"></slot>
    </button>
      <p>
        <slot name="updatedAt">
          {{ updatedAt.year }}
        </slot>
      </p>
  </div>
</template>

<script>
export default {
  name: 'Article',
  data() {
    return {
      updatedAt: {
        year: '2021',
        month: '06',
        date: '22',
      },
    }
  }
}
</script>
```
更新日時を差し込むスロットupdatedAtを用意しました．また，データとして子コンポーネントであるarticle.vueに更新日時のデータを持たせています．updatedAtでは，デフォルトコンテンツとして`{{ updatedAt.year }}`と記述しているので，親コンポーネントであるApp.vueからupdatedAtスロットにコンテンツを指定しなければ2021という数字が表示されます．


```vuejs:App.vue
<template>
  <Article>
    <template v-slot:title>
      タイトルです
    </template>
    <template v-slot:body>
      サブタイトルです
    </template>
    <template #buttonText>
      次へ
    </template>
  </Article>
</template>

<script>
import Article from './components/article.vue';

export default {
  name: 'App',
  
  components: {
    Article,
  },
}
</script>
```
![スクリーンショット 2021-06-23 16.34.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/5211d854-45a7-5789-db7d-d6b2c45d6f4e.png)
ここで，更新日時のうち，年ではなく，月を表示させたい場合はどのようにすれば良いのでしょうか．実は，スコープ付きスロットを用いることでこれを実現できます．article.vueを以下のように書き換えてみます．

```vuejs:article.vue
<template>
  <div style="width=500px;">
    <h1>
      <slot name="title"></slot>
    </h1>
    <h2>
      <slot name="subTitle"></slot>
    </h2>
    <button>
      <slot name="buttonText"></slot>
    </button>
      <p>
        <slot name="updatedAt" v-bind:slotProps="updatedAt"><!-- スコープ付きスロット -->
          {{ updatedAt.year }}
        </slot>
      </p>
  </div>
</template>

<script>
export default {
  name: 'Article',
  data() {
    return {
      updatedAt: {
        year: '2021',
        month: '06',
        date: '22',
      },
    }
  }
}
</script>
```
このように，子コンポーネントでデータを親コンポーネントに渡したい時にv-bindで変数をバインドしてあげます．ここではupdatedAtというオブジェクトをslotPropsという変数でバインドしています（slotPropsは任意の変数名で可）．
App.vueは次のように書き換えます．

```vuejs:App.vue
<template>
  <Article>
    <template v-slot:title>
      タイトルです
    </template>
    <template v-slot:body>
      サブタイトルです
    </template>
    <template #buttonText>
      次へ
    </template>
    <template v-slot:updatedAt="{ slotProps }"><!-- スコープ付きスロットから変数（オブジェクト）を取得 -->
      {{ slotProps.month }}<!-- デフォルトコンテンツをmonthを表示するように上書き -->
    </template>
  </Article>
</template>

<script>
import Article from './components/article.vue';

export default {
  name: 'App',
  
  components: {
    Article,
  },
}
</script>
```
このように，呼び出し側でv-slotの値として変数を受け取ることでそのスコープ内で値を使ってコンテンツを記述することができます．当然ですが，他のスロットなどではこの変数は機能しません．
実行結果はこんな感じ．
![スクリーンショット 2021-06-23 16.59.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287071/a75f7272-7edf-ad85-f19c-8c7dbcbc5375.png)

## おわりに
Vue.jsのslotについて解説しました．今回紹介した方法以外にも，記述のバリエーションがいくつかあるので，この記事と少し状況が違うなという方は公式の[ドキュメント](https://jp.vuejs.org/v2/guide/components-slots.html)を参照してみてください．
### 参考
- Vue.js公式ドキュメント　-　Slot：https://jp.vuejs.org/v2/guide/components-slots.html