Vue.js のスロット機能を使ってみると意外に便利だったため、ざっくりまとめます。

スロット

Vue.js のスロットとは、簡単にいうと「 子コンポーネントの一部を親コンポーネントから差し込む 」ことができる機能です。

Vue.js > ガイド > コンポーネント > スロットによるコンテンツ配信
https://jp.vuejs.org/v2/guide/components.html#スロットによるコンテンツ配信

コードサンプル

スロットは、Bootstrap のようにタグ構成が決まっているコードのコンポーネント化と相性が良いと感じます。

さっそく例を見てみましょう。

確認環境

  • Node.js 7.7.x
  • mpn 5.3.x
  • Vue.js 2.4.x

Panel を単一スロットのコンポーネントにする

整形前のコード

<div class="panel panel-primary">
  <div class="panel-body">
    Panel content
  </div>
</div>

コンポーネント化

次はpanel-bodyの文字を<slot>タグでマークしただけのものです。

<template>
  <div class="panel panel-primary">
    <div class="panel-body">
      <slot>
        Panel content
      </slot>
    </div>
  </div>
</template>

このように無名の <slot> を一つ用意するものは、ドキュメントでいう単一スロットにあたります。


このスロットにコンテンツを流し込むときは次のようにします。

<template>
  <panel>
    <p>新しい朝が来た希望の朝だ!</p>
  </panel>
</template>

これを表示すると次のコードに置き換えられます。

<div class="panel panel-primary">
  <div class="panel-body">
    <p>新しい朝が来た希望の朝だ!</p>
  </div>
</div>

コンポーネントの中身がそのまま Slot に差し込まれるイメージです。
簡単ですね!

Panelを単一スロット+名前付きが混在したコンポーネントにする

Panel を Body だけで使うことって、多分あまりありませんよね?

次にヘッダーをつけた Panel のスロット活用を行います。

整形前のコード

<div class="panel panel-default">
  <div class="panel-heading">
    Pannel header
  </div>
  <div class="panel-body">
    Panel content
  </div>
</div>

コンポーネント化

<template>
  <div class="panel panel-default">
    <div class="panel-heading">
      <slot name="header">
        Pannel header
      </slot>
    </div>
    <div class="panel-body">
      <slot>
        Panel contentaaaba
      </slot>
    </div>
  </div>
</template>

<slot> タグが 2 つになりましたが、panel-heading 内の <slot> タグには name 属性で名前を与えています。
ドキュメントで言うところの名前付きスロットにあたります。


2 つのスロットにそれぞれコンテンツを差し込んでみましょう。

<template>
  <panel>
    <h4 slot="header">ラジオ体操の歌</h4>
    <p>新しい朝が来た 希望の朝だ!</p>
    <p>喜びに胸を開け 大空あおげ</p>
  </panel>
</template>

これを表示すると次のコードに置き換えられます。

<div class="panel panel-default">
  <div class="panel-heading">
    <h4>ラジオ体操の歌</h4>
  </div>
  <div class="panel-body">
    <p>新しい朝が来た 希望の朝だ!</p>
    <p>喜びに胸を開け 大空あおげ</p>
  </div>
</div>

ブラウザでチェックすると、、、問題なく表示されています。

Vue.js スロットによる実装の画面表示イメージ
Vue.js スロットによる実装の画面表示イメージ

名前付きスロットにコンテンツを差し込むには、ラップする要素に slot 属性で差し込む先を指定します。
今回のように名前あり/なしが混在している場合、 slot属性の指定のないものはすべてデフォルトスロットに差し込まれる という挙動です。

理屈はわかりますが、なんとなく気持ちが悪いですね。
「複数のスロットを用意する場合は全て名前付きにする」というようなルールにしてしまった方がわかりやすいかもしれません。


ちなみに 1 コンポーネントに名前なしスロットが複数ある場合は次のように怒られます。

[Vue warn]: Duplicate presence of slot "default" found in the same render tree - this will likely cause render errors.

フォームでスロットを活用する

整形前のコード

次はよく見る Bootstrap で組み立てたフォームです。

Bootstrap で実装したフォーム画面
Bootstrap で実装したフォーム画面

このフォームは素の Bootstrap にv-modelを加えて実装しております。

<form class="form-horizontal">
  <fieldset>
    <legend>書籍登録/変更</legend>

    <div class="form-group">
      <label for="inputTitle" class="col-md-3 control-label">タイトル</label>
      <div class="col-md-9">
        <input v-model="title" type="text" class="form-control" id="inputTitle" placeholder="書籍タイトル">
      </div>
    </div>

    <div class="form-group">
      <label for="inputTitleSub" class="col-md-3 control-label">サブタイトル</label>
      <div class="col-md-9">
        <input v-model="title_sub" type="text" class="form-control" id="inputTitleSub" placeholder="書籍サブタイトル">
      </div>
    </div>

  </fieldset>
</form>

ちょっと無理やりですが、ここも繰り返し部分をコンポーネントにしてみましょう。

コンポーネント化

<template>
  <div class="form-group">
    <label :for="target" class="col-md-3 control-label">{{ label }}</label>
    <div class="col-md-9">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    label: String,
    target: String
  },
}
</script>

先のコードを、この子コンポーネントを利用した形に書き換えたものが次です。

<form class="form-horizontal">
  <fieldset>
    <legend>書籍登録/変更</legend>

    <input-component label="タイトル" target="inputTitle">
      <input v-model="title" type="text" class="form-control" id="inputTitle" placeholder="書籍タイトル">
    </input-component>

    <input-component label="サブタイトル" target="inputTitleSub">
      <input v-model="title_sub" type="text" class="form-control" id="inputTitleSub" placeholder="書籍サブタイトル">
    </input-component>

  </fieldset>
</form>

かなり見通しが良くなりましたね!
こういうすっきりシンプルな実装、私は大好きです。


この実装で混乱するところは v-modelのスコープは親と子のどちらにあるか です。
答えは親です。

ドキュメントにも記載がある通り、Vue.js では「 親テンプレート内の全てのものは親のスコープ 」というルールがありますので覚えておくと良い感じです。

親テンプレート内の全てのものは親のスコープでコンパイルされ、子テンプレート内の全てものは子のスコープでコンパイルされる
https://jp.vuejs.org/v2/guide/components.html#コンパイルスコープ

おわりに

スロットをうまく活用し、再利用生の高いコンポーネントを用意しておくと開発スピードのアップが期待できますね。
どこをスロット化するか をしっかり設計するのがポイントだと感じました。


WEB+DB PRESS Vol.97

外村 和仁,小林 徹,古川 陽介,佐藤 歩,yoku0825,是澤 太志,一野瀬 翔吾,加藤 颯史,のざき ひろふみ,うらがみ,水嶋 淳貴,久田 真寛,久保 達彦,伊藤 直也,遠藤 雅伸,ひげぽん,海野 弘成,はまちや2,竹原,倉岡 洋義
出版社:技術評論社  発売日:2017-02-24

Amazonで詳細を見る

この記事の著者 Queue 8 Studio みちひこ