# スロット

このページはすでに コンポーネントの基本 を読んでいる事を前提としています。コンポーネントをよく知らない方はそちらを先にお読みください。

# スロットコンテンツ

Vue には Web Components spec draft (opens new window) にヒントを得たコンテンツ配信 API が実装されており、 <slot> 要素をコンテンツ配信の受け渡し口として利用します。

これを使うことで次のようなコンポーネントを作成することが出来ます:

<todo-button>
  Add todo
</todo-button>
1
2
3

そして、 <todo-button> のテンプレートはこうなります:

<!-- todo-button コンポーネントのテンプレート -->
<button class="btn-primary">
  <slot></slot>
</button>
1
2
3
4

コンポーネントを描画する時、 <slot></slot> は「Add Todo」に置換されるでしょう:

<!-- 描画された HTML -->
<button class="btn-primary">
  Add todo
</button>
1
2
3
4

文字列はあくまで序の口です!スロットには、 HTML を含む任意のテンプレートコードを含めることも出来ます:

<todo-button>
  <!-- Font Awesome のアイコンを追加 -->
  <i class="fas fa-plus"></i>
  Add todo
</todo-button>
1
2
3
4
5

あるいは他のコンポーネントを入れることも出来ます:

<todo-button>
  <!-- コンポーネントを使ってアイコンを追加 -->
  <font-awesome-icon name="plus"></font-awesome-icon>
  Add todo
</todo-button>
1
2
3
4
5

もしも <todo-button> のテンプレートが <slot> 要素を含まない 場合、開始タグと終了タグの間にある任意のコンテンツは破棄されます。

<!-- todo-button コンポーネントのテンプレート -->

<button class="btn-primary">
  Create a new item
</button>
1
2
3
4
5
<todo-button>
  <!-- 次の行のテキストは描画されません -->
  Add todo
</todo-button>
1
2
3
4

# 描画スコープ

スロットの中でデータを扱いたい場合はこうします:

<todo-button>
  Delete a {{ item.name }}
</todo-button>
1
2
3

このスロットは、テンプレートの残りの部分と同じインスタンスのプロパティ (つまり、同じ “スコープ”) にアクセスできます。

スロットの説明図

<todo-button> のスコープにアクセスすることは できません。例えば、action へのアクセスは動作しないでしょう:

<todo-button action="delete">
  Clicking here will {{ action }} an item
  <!--
  `action` は undefined になります。なぜなら、このコンテンツは
  <todo-button> コンポーネント _の中で_ 定義されるのではなく、
  <todo-button> コンポーネント _に_ 渡されるからです
  -->
</todo-button>
1
2
3
4
5
6
7
8

ルールとしては、以下を覚えておいてください:

親のテンプレート内の全てのものは親のスコープでコンパイルされ、子のテンプレート内の全てのものは子のスコープでコンパイルされる。

# フォールバックコンテンツ

スロットに対して、コンテンツがない場合にだけ描画されるフォールバック (つまり、デフォルトの) コンテンツを指定すると便利な場合があります。例えば、<submit-button> コンポーネントにおいて:

<button type="submit">
  <slot></slot>
</button>
1
2
3

ほとんどの場合には <button> の中に「Submit」という文字を描画したいかもしれません。「Submit」をフォールバックコンテンツにするには、 <slot> タグの中に記述します。

<button type="submit">
  <slot>Submit</slot>
</button>
1
2
3

そして、親コンポーネントからスロットのコンテンツを指定せずに <submit-button> を使うと:

<submit-button></submit-button>
1

フォールバックコンテンツの「Submit」が描画されます:

<button type="submit">
  Submit
</button>
1
2
3

しかし、もしコンテンツを指定すると:

<submit-button>
  Save
</submit-button>
1
2
3

指定されたコンテンツが代わりに描画されます:

<button type="submit">
  Save
</button>
1
2
3

# 名前付きスロット

複数のスロットがあると便利なときもあります。例えば、 <base-layout> コンポーネントが下記のようなテンプレートだとしましょう:

<div class="container">
  <header>
    <!-- ここにヘッダコンテンツ -->
  </header>
  <main>
    <!-- ここにメインコンテンツ -->
  </main>
  <footer>
    <!-- ここにフッターコンテンツ -->
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

こういった場合のために、 <slot> 要素は name という特別な属性を持っていて、それぞれのスロットにユニークな ID を割り当てることによってコンテンツがどこに描画されるべきかを決定することができます:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

name のない <slot> 要素は、暗黙的に「default」という名前を持ちます。

名前付きスロットにコンテンツを渡すには、<template> に対して v-slot ディレクティブを使い、スロット名をその引数として与える必要があります:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

これにより、<template> 要素の中身はすべて対応するスロットに渡されます。

描画される HTML は以下のようになります:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11
12

**v-slot は(一つの例外 を除き)<template> にしか指定できないことに注意してください。

# スコープ付きスロット

スロットコンテンツから、子コンポーネントの中だけで利用可能なデータにアクセスできると便利なことがあります。コンポーネントがアイテムの配列を描画するためにつかわれていて、レンダリングされた各アイテムをカスタマイズできるようにしたい、などは典型的な例です。

例えば、以下のようなコンポーネントがあり、 todo アイテムのリストを内部に持ってます。

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        {{ item }}
      </li>
    </ul>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

親コンポーネントでこれをカスタマイズするために、{{ item }}<slot> に置き換えたい場合があります:

<todo-list>
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}<span>
</todo-list>
1
2
3
4

しかし、これは動作しません。というのも、 item にアクセスすることができるのは <todo-list> コンポーネントだけですが、ここで指定しているコンテンツは親コンポーネントで描画されるからです。

親コンポーネント内でスロットコンテンツとして item を使えるようにするためには、これを <slot> 要素の属性として束縛します:

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item"></slot>
  </li>
</ul>
1
2
3
4
5

必要な数の属性を slot に束縛できます:

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
  </li>
</ul>
1
2
3
4
5

<slot> 要素に束縛された属性は、 スロットプロパティ と呼ばれます。これで、親スコープ内で v-slot に値を指定して、渡されたスロットプロパティの名前を定義できます:

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>
1
2
3
4
5
6
スコープ付きスロットの説明図

この例では、すべてのスロットプロパティを持つオブジェクトの名前を slotProps にしましたが、あなたの好きな名前を使うことができます。

# デフォルトスロットしかない場合の省略記法

上の例のようにデフォルトスロット だけの 場合は、コンポーネントのタグをスロットのテンプレートとして使うことができます。つまり、コンポーネントに対して v-slot を直接使えます:

<todo-list v-slot:default="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>
1
2
3
4

さらに短くすることもできます。未指定のコンテンツがデフォルトスロットのものとみなされるのと同様に、引数のない v-slot もデフォルトコンテンツを参照しているとみなされます:

<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>
</todo-list>
1
2
3
4

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

<!-- 不正。警告が出ます -->
<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>

  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</todo-list>
1
2
3
4
5
6
7
8
9

複数のスロットがある場合は常に すべての スロットに対して <template> ベースの構文を使用してください:

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</todo-list>
1
2
3
4
5
6
7
8
9
10

# スロットプロパティの分割代入

内部的には、スコープ付きスロットはスロットコンテンツを単一引数の関数で囲むことで動作させています:

function (slotProps) {
  // ... スロットコンテンツ ...
}
1
2
3

これは、v-slot の値が関数定義の引数部分で有効な任意の JavaScript 式を受け付けることを意味します。そのため、特定のスロットプロパティを取得するために ES2015 の分割代入 (opens new window) を使うこともできます:

<todo-list v-slot="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}<span>
</todo-list>
1
2
3
4

こうするとテンプレートはよりきれいになります。特に、スロットが多くのプロパティを提供している場合はそうです。また、プロパティをリネームする (例えば、item から todo) など別の可能性も開けます:

<todo-list v-slot="{ item: todo }">
  <i class="fas fa-check"></i>
  <span class="green">{{ todo }}</span>
</todo-list>
1
2
3
4

スロットプロパティが未定義だった場合のフォールバックを定義することもできます:

<todo-list v-slot="{ item = 'Placeholder' }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}<span>
</todo-list>
1
2
3
4

# 動的なスロット名

ディレクティブの動的引数v-slot でも動作し、動的なスロット名の定義が可能です:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>
1
2
3
4
5

# 名前付きスロットの省略記法

v-onv-bind と同様に v-slot にも省略記法があり、引数の前のすべての部分 (v-slot:) を特別な記号 # で置き換えます。例えば、v-slot:header#header に書き換えることができます:

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

しかし、ほかのディレクティブと同様に、省略記法は引数がある場合にのみ利用できます。これは、次のような構文が不正ということを意味します:

<!-- 警告が出ます -->

<todo-list #="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}<span>
</todo-list>
1
2
3
4
5
6

代わりに、省略記法を使いたい場合には、常にスロット名を指定する必要があります:

<todo-list #default="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}<span>
</todo-list>
1
2
3
4