【Vue.js】超エキサイティングなslot

# はじめに

この記事は、[JSL(日本システム技研) Advent Calendar 2021](https://qiita.com/advent-calendar/2021/jsl) の記事です。
最近、Vueの機能の１つである `slot` を使う機会が多かったので、使い方をまとめてみようと思いました。
※「超エキサイティング」かどうかには個人差があります。
※ 速攻でタイトル詐欺疑惑 :thinking: 

# 環境
- vue: 2.6.14
- vue-property-decorator: 9.1.2
- typescript: 4.4.3

# 本編

## 1. そもそもslotってなに？

言葉では説明しづらいので、以下の例をご覧ください。

```Sample.vue
<template>
  <div class="sample">
    <div>このコンポーネントで固定の表示</div>
    <slot>
      <div>slotで置き換え可能な表示</div>
    </slot>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class Sample extends Vue {}
</script>

<style scoped>
.sample {
  outline-style: solid;
  padding: 10px;
  margin: 10px;
}
</style>
```

```Test.vue
<template>
  <div style="width: 400px">
    <sample></sample>
    <sample>
      <div style="color: red">slotで置き換えた表示</div>
    </sample>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

import Sample from "@/components/Sample.vue";

@Component({
  components: {
    Sample
  }
})
export default class Test extends Vue {}
</script>
```

見ての通り、`Test.vue` で `Sample` コンポーネントを2つ表示するだけの例ですが、これを実行するとこんな感じの表示になります。

![SS 2021-12-15 0.02.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/983e719f-5e3d-d8e1-579f-28c913d5c21e.png)

おや、2つ目の表示が1つ目と違っていますね？
これは2つ目の `<sample>` 要素の中に書かれている要素が、 `Sample` コンポーネントの `<slot>` 要素部分に差し込まれたからです。
こんな感じに、外からコンポーネントの一部を置き換えられるのが `slot` の機能です。

そういえば、1つ目の方は `<sample>` 要素に何も指定していませんが、 `<slot>` 要素内にあった要素がそのまま表示されていますね。
slotとして置き換える要素が何も指定されなければ、 `<slot>` 要素内の要素がデフォルト値として出力されるという親切設計です。

## 2. 名前付きslot

1章では基本的なslotの使い方を紹介しましたが、もしかしたらこんなことを思った人がいるかもしれません。

「slotって1箇所しか使えないの？」

いえいえ、そんなことはありません。slotは「名前を付ければ」コンポーネントにいくらでも用意することが可能です。
先ほどの例にちょっと書き加えたもので説明します。

```Sample.vue
<template>
  <div class="sample">
    <div>このコンポーネントで固定の表示</div>
    <slot>
      <div>slotで置き換え可能な表示</div>
    </slot>
    <slot name="slot-2">
      <div>slotで置き換え可能な表示2</div>
    </slot>
  </div>
</template>

...
```

```Test.vue
<template>
  <div style="width: 400px">
    <sample></sample>
    <sample>
      <div style="color: red">slotで置き換えた表示</div>
      <template #slot-2>
        <div style="color: blue">slotで置き換えた表示2</div>
      </template>
    </sample>
  </div>
</template>

...
```

上記のようにしてみるとこんな風に表示されます。

![SS 2021-12-15 0.25.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/8f60c9e3-6664-c2b3-3e29-420bac7f6e36.png)

2つ目の方に青い文字が表示されていますね。これが2つ目のslot部分です。
`Sample.vue` に `<slot name="slot-2">` が `Test.vue` に `<template #slot-2>` がそれぞれ追加されていますのでもうお分かりかと思いますが、 `<slot name="お名前">` で名付けて `<template #お名前>` の中に記述した要素を差し込むことができます。

これを「名前付きslot」と呼んでいます。めっちゃ簡単ですね。

ちなみに、「名前付きslot」に対して名無しのslotは「デフォルトslot」と呼ばれます。名前を指定する必要がないので `<template #お名前>` がなくてもOKだったわけです。
とはいえ、上のようなデフォルトslotの省略記法と名前付きslotの混在は公式が「非推奨」と言っています。なので、複数のslotが混在する場合は `<template #default>` で省略記法を使わない方が好ましそうです(記事書くために調べて初めて知りました :rolling_eyes: )

https://jp.vuejs.org/v2/guide/components-slots.html

> デフォルトスロットに対する省略記法は、名前付きスロットと混在させることが できない 点に注意してください。スコープの曖昧さにつながるためです


## 3. slotから値を受け取る

slotから値を受け取る？ :thinking: なんのこっちゃ？となると思いますので、（例によって）例で説明していきます。

今回は `Sample.vue` を「IDとその値を表示するコンポーネント」にしてみました。

```Sample.vue
<template>
  <div class="sample">
    <slot name="label">
      <div>{{ label }}</div>
    </slot>
    <slot name="value">
      <div>{{ value }}</div>
    </slot>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class Sample extends Vue {
  @Prop({ type: String, default: "" }) id!: string;
  @Prop({ type: String, default: "" }) value!: string;

  get label(): string {
    // NOTE: 4文字毎にハイフン区切りに整形
    return this.id.match(/.{1,4}/g)?.join("-") ?? "";
  }
}
</script>

<style scoped>
.sample {
  outline-style: solid;
  padding: 10px;
  margin: 10px;
}
</style>
```

```Test.vue
<template>
  <div style="width: 400px">
    <sample :id="item1.id" :value="item1.value"></sample>
    <sample :id="item2.id" :value="item2.value">
      <template #label>
        <div style="color: red">{{ item2.id }}</div>
      </template>
    </sample>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

import Sample from "@/components/Sample.vue";

type Item = {
  id: string;
  value: string;
};

@Component({
  components: {
    Sample
  }
})
export default class Test extends Vue {
  item1: Item = {
    id: "1234abcd5678efgh",
    value: "slot指定なし"
  };
  item2: Item = {
    id: "2345bcde6789fghi",
    value: "slot指定あり"
  };
}
</script>
```

注目なのは、 `Sample.vue` の `label` が「IDのハイフン区切りフォーマット」であることです。
とりあえず実行してみましょうか。

![SS 2021-12-15 9.31.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/476d2db2-189e-22db-8daf-18d9f12455a6.png)

`item2` のIDだけを赤色にしようとしていますが…なんか違和感が…
あ、こっちだけ「ハイフン区切り」になってないですやん！

そうすると `Test.vue` でもハイフン区切りにするコードを書かなきゃいけないの？は〜しちめんどくさか〜〜💢

はい、そこで登場するのが「slotから値を受け取る」機能でございます！

（胡散臭いテレフォンショッピングみたいになってきましたが、機能は胡散臭くありませんのでご安心ください）

やりたいこととしては、 `Sample.vue` でハイフン区切りフォーマット済みのID `label` を `Test.vue` で受け取りたい、ですね。
少々コードを修正してみましょう。

```Sample.vue
...
    <slot name="label" :label="label">
...
```

```Test.vue
...
      <template #label="{ label }">
        <div style="color: red">{{ label }}</div>
...
```

これで実行してみます。

![SS 2021-12-15 9.52.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/c9fca764-12c7-1c3d-ec78-122736e3e19a.png)

はい。たったこれだけでちゃんと「ハイフン区切りフォーマット済みのID」を受け取って表示することができました。
仕掛けはとってもシンプルで、 `<slot name="お名前">` に渡したpropsは `<template #お名前="props">` で受け取れるというだけです。

同じ実装をいろんなところでする必要がなくなりますし、関数のimportも必要ありません。コンポーネント依存の実装をコンポーネント内に閉じ込めることができて素敵ですね。

え？「色変えるだけならprops増やせばいいじゃん」ですか？……ごもっともでございます。
ただ、こういう「限定的な対応」の度にpropsが増えていくと考えると果てしなく気持ち悪いですよね…。
なので、こういう解法を用いたほうがいい場合もありますよ、という紹介でした :pray: 

## 4. 動的なslot

3章までは基本的な内容でしたが、ここから発展的な使い方を紹介していきます。
今まではslotの名前が固定でしたが、propsなどによって変化させたい場合もあるでしょう。
そんなときはどうすればいいのか、またまた例を使って説明していきます。

```Sample.vue
<template>
  <div class="sample">
    <slot name="label" :label="label">
      <div>{{ label }}</div>
    </slot>
    <div
      v-for="(v, index) in valueProps"
      :key="index"
      style="border-bottom: 1px solid"
    >
      <span>{{ v[0] }}: </span>
      <span>{{ v[1] }}</span>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class Sample extends Vue {
  @Prop({ type: String, default: "" }) id!: string;
  @Prop({ type: Object, default: "" }) value!: Partial<Record<string, string>>;

  get label(): string {
    // NOTE: 4文字毎にハイフン区切りに整形
    return this.id.match(/.{1,4}/g)?.join("-") ?? "";
  }
  get valueProps(): [string, string | undefined][] {
    return Object.entries(this.value);
  }
}
</script>

<style scoped>
.sample {
  outline-style: solid;
  padding: 10px;
  margin: 10px;
}
</style>
```

```Test.vue
<template>
  <div style="width: 400px">
    <sample :id="item.id" :value="item.value"></sample>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

import Sample from "@/components/Sample.vue";

type Item = {
  id: string;
  value: Partial<Record<string, string>>;
};

@Component({
  components: {
    Sample
  }
})
export default class Test extends Vue {
  item: Item = {
    id: "2345bcde6789fghi",
    value: {
      prop1: "prop1の値",
      prop2: "prop2の値",
      prop3: "prop3の値"
    }
  };
}
</script>
```

今回は、 `value` をオブジェクトにしてプロパティ名と値を並べて表示するようにしてみました。
実行してみるとこんな感じです。

![SS 2021-12-15 18.40.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/46bb148e-defb-7bbb-c007-138610095d93.png)

さて、ここからが本題なのですが、例えばprop2の値の色だけを変更したい場合はどうすればいいでしょうか？
`value` の型からもわかる通り、プロパティ名は `value` によって変わるため汎用性を考えると固定にはできません。つまり、slotの名前を動的に付けてあげる必要が出てきたわけです。

では、少し例のコードをいじってみましょう。

```Sample.vue
...
      <slot :name="`value.${v[0]}`" :propValue="v[1]">
        <span>{{ v[1] }}</span>
      </slot>
...
```

```Test.vue
...
    <sample :id="item.id" :value="item.value">
      <template #value.prop2="{ propValue }">
        <span style="color: red">{{ propValue }}</span>
      </template>
    </sample>
...
```

とりあえず実行してみましょ。

![SS 2021-12-15 18.55.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/8a8b13fd-6bfb-79b1-d304-6de8ba0b9a66.png)

はい、ちゃんとprop2の値だけslotで置き換えられていますね。
キモとなっているのは `Sample.vue` の ```<slot :name="`value.${v[0]}`"``` 部分です。
これによって、slotのお名前が `value. + プロパティ名` と動的に決めることができるので、後はいつも通り `<templete #お名前>` で指定できます。

さてお次は `<template #お名前>` で指定する側が動的になるパターンです。
プロパティの値が `"prop3の値"` と一致するものを赤色にしてみましょう。

```Test.vue
...
      <template #[`value.${redProp}`]="{ propValue }">
        <span style="color: red">{{ propValue }}</span>
      </template>
...

...
  get redProp(): string {
    return (
      Object.entries(this.item.value).find(v => v[1] === "prop3の値")?.[0] ?? ""
    );
  }
...
```

これで実行するとこんな感じになります。

![SS 2021-12-15 19.23.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/554977b4-2afc-3066-8ff5-54518fd76acf.png)

はい。意図した通りに、prop3が赤くなりましたね。
```<template #[`動的なお名前`]>``` とすることで、指定先も自由自在になります。
ちなみに、指定先のslotが存在しない場合はエラーにはならず、ただslotのデフォルト要素が表示されるので安心です。

では次が最後のパターンです。slotを持つコンポーネントを `v-for`で複数表示する場合を考えてみましょう。
例のコードを少しいじって…

```Test.vue
<template>
  <div style="width: 400px">
    <sample
      v-for="(item, index) in items"
      :id="item.id"
      :key="index"
      :value="item.value"
    >
    </sample>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

import Sample from "@/components/Sample.vue";

type Item = {
  id: string;
  value: Partial<Record<string, string>>;
};

@Component({
  components: {
    Sample
  }
})
export default class Test extends Vue {
  items: Item[] = [
    {
      id: "1234abcd5678efgh",
      value: {
        prop1: "prop1の値",
        prop2: "prop2の値",
        prop3: "prop3の値"
      }
    },
    {
      id: "2345bcde6789fghi",
      value: {
        prop4: "prop4の値",
        prop5: "prop5の値",
        prop6: "prop6の値"
      }
    }
  ];
}
</script>
```

itemを配列にして `v-for` で表示するようにしてみました。

![SS 2021-12-15 19.35.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/b0f72cc4-5cf9-a125-63c3-817919a21a03.png)

で、これのprop4の値だけを赤色にしてみましょう。

```Test.vue
...
    <sample
      v-for="(item, index) in items"
      :id="item.id"
      :key="index"
      :value="item.value"
    >
      <template #value.prop4="{ propValue }">
        <span style="color: red">{{ propValue }}</span>
      </template>
    </sample>
...
```

![SS 2021-12-15 19.40.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/7c736661-f9a6-3437-9999-03d145002e8a.png)

はい。めっちゃ簡単…というか1つだった時と特に変化ありませんでしたね。
コンポーネントが `v-for` で複数描画されるとしてもslotは変わらず健在です。

あれ、じゃあprop4が2つともに含まれていた時はどうなるの？
やってみましょう。

```Test.vue
...
      prop4: "prop1-4の値"
...

...
      prop4: "prop2-4の値"
...
```

![SS 2021-12-15 19.45.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252036/fd9ded9c-863b-cfaf-b56c-77685921d9a6.png)


うん、なんとなく分かってたけど大丈夫でしたね。ちゃんと両方に適用されています。
こういう項目を列挙する系のコンポーネントは割とあるので、活躍の場は多そうですね。

というわけで、あっけない最後で締まりませんがこれで本編終了です。お疲れ様でした :bow: 

# おわりに

まとめですが、slotを使うとコンポーネントの拡張性がめちゃくちゃ上がります。
自分はslotの存在を知るまでは、propsでhtmlを文字列で渡してv-htmlで（ｺﾞﾆｮｺﾞﾆｮ）とかやっていたので世界が広がった気すらしました。
あと、 [Vuetify](https://vuetifyjs.com/ja/) などのデザインフレームワークに用意されているコンポーネントはslotが効果的に用意されていて、便利に拡張して使うもよし、実装を参考にするもよし、割とみる目が変わりました。
slotを理解できるとVueの実装が更に楽に、楽しくなるのではないかなと思います。

皆様もslotを使って「超エキサイティング」なVueライフを！