基礎からメモ: Vue.js CH5-p169 スロットを使ったコンポーネントのカスタマイズ

スロットという機能を使って、親コンポーネントから子コンポーネントにテンプレートの一部を差し込むことができる。 これによって子コンポーネントの一部分をカスタマイズしたりできる。

使い方は簡単。親コンポーネントで使う子コンポーネントのカスタムタグ内に埋め込まれたコンテンツが、子側のテンプレート内のslotタグで参照され、そのまま埋め込まれる。

コンポーネント

let myComponent = {
  template: `<div>
      <comp-child>
      ここがスロットコンテンツ
      </comp-child>
    </div>`
};

コンポーネント

Vue.component('comp-child', {
  template: `<div class="comp-child">
    スロットコンテンツを埋め込む→ <slot></slot>
  </div>`,
});

表示結果

<div id="app">
  <div>
    <div class="comp-child">
      スロットコンテンツを埋め込む→ ここがスロットコンテンツ
    </div>
  </div>
</div>

スロットのスコープとデフォルト値

スロットではコンテンツ定義側のスコープを維持する。親の側のデータを使っているなら、子側に同じデータ変数名があっても親側のデータが使われる。

Vue.component('comp-child', {
  template: `<div class="comp-child">
    <slot>{{ message }}</slot>
  </div>`,
  data: function() {
    return { message: '子のデータ' }
  },
});

let myComponent = {
  template: `<div>
    <comp-child>{{ message }}</comp-child>
    </div>`,
  data: function() {
    return { message: '親のデータ' }
  }
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

表示結果

<div class="comp-child">
  親のデータ
</div>

もし親側のテンプレートにスロットで読み込まれるデータが無かったら(空白なら)、子側のslotタグ内に何らかの内容があればデフォルト値になるので、この例の場合は子の messsage がデフォルトとして表示される。

名前付きスロット

スロットには名前を付けることができる。異なる名前を付けることで複数のスロットを使用できる。

ベースのHTML

<div id="app">
  <my-component></my-component>
</div>

親側のテンプレートでスロット領域内にslot="特定の名前"という属性のタグを置くと、

let myComponent = {
  template: `<comp-child>
      <header slot="header">Hello Vue.js!</header>
      Vue.jsはJavaScriptのフレームワークです。
      </comp-child>`,
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

子側のテンプレートの、その名前に対応するスロットの部分が親の内容で置き換わる。親の内容が無ければデフォルトの値が表示される。

Vue.component('comp-child', {
  template: `<section class="comp-child">
    <slot name="header">デフォルトタイトル</slot>
    <div class="content"><slot>デフォルトコンテンツ</slot></div>
    <slot name="footer"></slot>
  </section>`,
});

ここでは子側のテンプレートのfooterという名前のスロットには、親側も子側も内容が無いので全く表示されない。またcontentという属性名のタグ内は親側の残りのスロットコンテンツに置き換わる。

表示結果

<div id="app">
  <section class="comp-child">
    <header>Hello Vue.js!</header>
    <div class="content">
      Vue.jsはJavaScriptのフレームワークです。
    </div>
  </section>
</div>

親側テンプレートのfooterという名前のタグに内容があると、子側の対応するスロットが置き換わり表示される。(子側のテンプレートは変更なしで)

let myComponent = {
  template: `<comp-child>
      <header slot="header">Hello Vue.js!</header>
      Vue.jsはJavaScriptのフレームワークです。
      <footer slot="footer">フッターコンテンツ</footer>
      </comp-child>`,
};

ヘッダーとフッターにはさまれたコンテンツ部分は<div slot="content">というタグを親側に作ってもよいが、ここでは子側を何も変更しなくても、残り部分が子側のコンテンツ部分に入る。

表示結果

<div id="app">
  <section class="comp-child">
    <header>Hello Vue.js!</header>
    <div class="content">Vue.jsはJavaScriptのフレームワークです。</div>
    <footer>フッターコンテンツ</footer>
  </section>
</div>

<slot>タグにはv-ifv-forなどの一部のディレクティブを使うことができるが、class属性などを付与できない。属性が必要な場合は親側に付ける。

スロットコンテンツの定義にtemplateタグを使う

親側のテンプレートでスロットコンテンツを定義する際にタグが不要であれば、templateタグで定義できる。

<comp-child>
  <template slot="text1">テキスト1</template>
  <template slot="text2">テキスト2</template>
</comp-child>

子側のテンプレート

<section class="comp-child">
  <p><slot name="text1">デフォルトコンテンツ1</slot></p>
  <p><slot name="text2">デフォルトコンテンツ2</slot></p>
</section>

表示結果からはtemplateタグは消えている。

<div id="app">
  <section class="comp-child">
    <p>テキスト1</p>
    <p>テキスト2</p>
  </section>
</div>

スコープ付きスロット

特別な属性slot-scopeを使えば、スロットコンテンツの定義に必要なデータを子コンポーネントから受け取ることができる。

子側のテンプレートで渡すデータ名と内容をslotタグの属性で指定する。

Vue.component('comp-child', {
  template: `<div class="comp-child">
    <slot text="Hello Child Scope!"></slot>
  </div>`,
});

親側のテンプレートでは、slot-scope属性の値として子側の値を受け取る変数名を任意に決めておく。この変数名に.を挟んで子側で定義したデータ名を書くことで子側のデータが参照できる。受け取る変数名は親のテンプレート内で一貫しておればよく任意である。他の属性名とダブらない名前にしておけばよい。

let myComponent = {
  template: `<comp-child>
      <p slot-scope="chSlot">
        スロットから受け取ったテキスト→ {{ chSlot.text }}
      </p>
      </comp-child>`,
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

表示結果

<div id="app">
  <div class="comp-child">
    <p>スロットから受け取ったテキスト→ Hello Child Scope!</p>
  </div>
</div>

コンポーネントのリストデータを親が受け取る

子側のテンプレートでv-forを使って子側が持つリストデータを繰り返し処理し、親側で個別の要素を受け取るテンプレートを定義することもできる。

Vue.component('comp-child', {
  template: `<ul class="comp-child">
    <slot v-for="item in list" v-bind:item="item"></slot>
  </ul>`,
  data: function() {
    return { list: [
      { id: 1, name: 'slime', hp: 100 },
      { id: 2, name: 'goblin', hp: 200 },
      { id: 3, name: 'dragon', hp: 500 },
    ] };
  },
});

親側のコンポーネント

let myComponent = {
  template: `<comp-child>
      <li slot-scope="childScp">Name: {{ childScp.item.name }} / HP: {{ childScp.item.hp }}</li>  
    </comp-child>`,
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

表示結果

<div id="app">
  <ul class="comp-child">
    <li>Name: slime / HP: 100</li>
    <li>Name: goblin / HP: 200</li>
    <li>Name: dragon / HP: 500</li>
  </ul>
</div>

slot-scopeは属性なので何らかの要素に付ける必要があるが、タグが必要なければまた<template>タグを使えばよい。