Vueコンポーネントはvue.jsの中核をなす重要な機能です。コンポーネント間のデータの受け渡しや記述方法など最初は難しさを感じるかと思います。ぜひ本文書を参考にコンポーネントの理解を深めてください。

Vue3のComposition APIのsetup scriptを利用した場合の記事を別で公開しているので参考にしてください。

コンポーネントとは

企業HPやブログのサイトを見るとヘッダー、フッター、メイン、サイドバーというようにいくつかのパーツに別れて構成されています。特にヘッダーやフッターなどはすべてのページでも同じ内容を表示する場合が多くすべてのページで同じ内容を何度も何度も記述することは非効率です。そのためヘッダー用のパーツ、フッター用のパーツを個別に作成し、すべてのページからパーツを読み込んで共有化することが一般的です。可能なかぎり構成要素をパーツ化し共有化することができれば開発、メンテナンスの効率が格段に上がります。vue.jsではコンポーネントという機能を使ってパーツ化し再利用することで効率よく開発、メンテナンスを行うことができます。

コンポーネントの設定

コンポーネントの作成

コンポーネントを利用するためには、コンポーネントを定義して登録する必要があります。


Vue.component(コンポーネントの名前,{
  template : 'HTMLに表示させたい内容'
})	
Vue.componentには、templateプロパティ以外にもVueインスタンスと同様にdata, computed, methods等のプロパティを持つことができます。記述方法や使い方は、これから順次説明を行っていきます。

実際に使用する場合は下記のように記述します。hello-worldという名前のコンポーネントを作成しその中身にはh1タグで囲まれたHello Worldという文字列を持つ要素が含まれていることを意味します。


Vue.component('hello-world',{
  template : '<h1>Hello World</h1>'
})	

コンポーネントの登録を行うとVueインスタンス下にあるhtml内から直接コンポーネントを利用することができます。利用する場合は、コンポーネント作成時に設定した名前のタグ名hello-worldを使ってhtmlに組み込みます。

Vueインスタンス下というのは、Vueインスタンス作成時のnew Vueのelで指定しているidタグの中の要素のことを意味します。本文書では#appです。

<div id="app">
	<hello-world></hello-world>
</div>
コンポーネントの名前は任意のため、その中に記述する内容を要約した名前をつけることでメンテナンスも楽になります。

ブラウザで見ると登録したコンポーネントのtemplateプロパティのh1タグの内容が表示されることが確認できます。

コンポーネントでHello World
コンポーネントでHello World

Vueインスタンスを含めたコード全体は下記のようになります。cdnを使ってvue.jsを読み込んでいます。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Component</title>
</head>
<body>
	<div id="app">
		<hello-world></hello-world>
	</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

Vue.component('hello-world',{
  template : '<h1>Hello World</h1>'
})

var app = new Vue({
  el: '#app',
  data: {
  	
  }
})

</script>	
</body>
</html>

Vue.componentの定義は、new Vueよりも前に記述する必要があります。new Vueよりも後に作成した場合は、Unknown custom element: <hello-world> – did you register the component correctly?のメッセージがコンソールに表示され、ブラウザには何も表示されません。エラーメッセージを日本語にすると”コンポーネントは正しく登録されましたか?”という内容です。

本文書では、vue.jsが設定通りに動作しない場合はコンソール(console)を使ってエラーを確認します。上記のUnknown custom elementのエラーもコンソールを使って確認しています。Chromeの場合、開発ツール(デベロッパーツール)から確認することができます。

コンポーネントの再利用

コンポーネントは再利用することができ、hello-worldのタグを複数記述するだけで同じ内容を表示させることが可能です。コンポーネントを一度作成すると部品としてどこからでも再利用することができます。


<div id="app">
	<hello-world></hello-world>
	<hello-world></hello-world>
	<hello-world></hello-world>
</div>
コンポーネントの再利用
コンポーネントの再利用

ローカルへの登録方法

先ほどはVue.componentを利用してコンポーネントの登録を行いました。コンポーネントの登録には、グローバルとローカルの2つの方法があります。Vue.componentを使う場合はグローバルへの登録となり、別のコンポーネントで利用することができます。一方ある特定のコンポーネントだけで利用したい場合は、ローカルで登録を行い利用することができます。ローカルへの登録は、Vueインスタンスにcomponentsプロパティの追加をすることで行います。


var app = new Vue({
  el: '#app',
  data: {
  },
  components: {
    'hello-world': {
      template: '<h1>Hello World</h1>'
    }
  }
})	

別ファイルからimport

コンポーネントは別ファイルとして分けることができます。通常はcomponentsフォルダの下などにファイルを作成しますがここではindex.htmlファイルと同じフォルダ内にHelloWorld.jsファイルを作成します。index.htmlファイル内でimportを行うのでexport defaultでオブジェクトとしてexportしています。


export default {
  template: '<h1>Hello World</h1>',
};

index.htmlファイルでimportによりHelloWorldコンポーネントとして読み込みます。ローカル登録の場合は下記のように行います。


<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Component</title>
</head>
<body>
  <div id="app">
    <hello-world></hello-world>
  </div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="module">

import HelloWorld from "./HelloWorld.js"

const app = new Vue({
  el: '#app',
  data: {
    
  },
  components: {
    'hello-world': HelloWorld
  }
})

</script> 
</body>
</html>

export, importを利用する場合はscriptタグにtype=”module”を設定する必要があります。もし設定していない場合には”Uncaught SyntaxError: Cannot use import statement outside a module”のエラーが発生します。

コンポーネントを別ファイルに分けて読み込む場合にはVSCodeのLive Serverを利用しています。index.htmlファイルをそのままブラウザで読み込んだ場合にはCORSのエラーが発生するので開発サーバなど(php -S localhost:8080)を利用してください。

グローバル登録の場合は下記のように記述することができます。


<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="module">
  import HelloWorld from './HelloWorld.js';

  Vue.component('hello-world', HelloWorld);

  const app = new Vue({
    el: '#app',
    data: {},
  });
</script>

グローバルとローカル登録の違い

Vue.componentを使ったグローバル登録とVueインスタンスの中で行うローカル登録の違いは下記の例を使って理解することができます。

1つのページに2つのVueインスタンスを作成し、hello-worldのコンポーネントはグローバル登録、hello-viewのコンポーネントは2つめのapp2インスタンスのみに登録します。

vue.jsでは、id=”app”とid=”app2″を要素として2つのVueインスタンスを1つのページ内で作成することができます。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Component</title>
</head>
<body>
	<div id="app">
		<hello-world></hello-world>
		<hello-view></hello-view>
	</div>	

	<div id="app2">
		<hello-world></hello-world>
		<hello-view></hello-view>
	</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

Vue.component('hello-world',{
  template : '<h1>Hello World</h1>'
})

var app = new Vue({
  el: '#app',
})

var app2 = new Vue({
  el: '#app2',
  components: {
    'hello-view': {
      template: '<h1>Hello View</h1>'
    }
  }
})
</script>	
</body>
</html>

下記のように1つ目のappのVueインスタンスではapp2にローカル登録したhello-viewコンポーネントが使用できないためHello Viewの文字列が表示されませんが、ローカル登録したapp2のVueインスタンス側には表示されます。

グローバルとローカル登録の違い
グローバルとローカル登録の違い

コンソールを見るとUnknown custom element: – did you register the component correctly? 。不明のカスタムエレメント、正しくコンポーネントを登録しましたか?のエラーが表示されます。

templateに複数のタグ

Vue 3ではこの制限はなくなっています。

templateに複数のタグを入れる場合に注意点があるので確認しておきます。以下のようにtemplateの中にpタグを2つ挿入します。


Vue.component('hello-world',{
  template: '<p>{{ message }}</p><p>Hello World</p>',
  data: function(){
    return {
      message : 'Hello Vue'
    }
  }
})	

ブラウザで確認すると2つ目のpタグのHello Worldが画面には表示されません。コンソールのログには、”Component template should contain exactly one root element.” とエラーメッセージが表示されます。エラーの内容は”コンポーネントテンプレートは1つのルート要素を持たなければならない”というエラーです。上記ではpタグが2つ挿入されているため、1つのルート要素にはなっていません(2つのルート要素になっている)。そのため、この2つのpタグを含む上位のタグをつける必要があります。ここではdivタグをつけます。divをつけることでdivが1つのルート要素になり、2つのpタグはdivの子要素となりエラーメッセージも消えてブラウザにはHello Vue Hello Worldが表示されます。


Vue.component('hello-world',{
  template: '<div><p>{{ message }}</p><p>Hello World</p></div>',
  data: function(){
    return {
      message : 'Hello Vue'
    }
  }
})	
1つのルートタグ忘れはよく発生しがちなミスなのでこのルールはしっかりと記憶しておきましょう。

コンポーネントでdataを使う

templateを追加してh1タグでブラウザに表示を行いました。コンポーネントでは、templateにhtmlタグを挿入するだけではなくdataも使用することができます。dataの設定方法はVueインスンス作成時と同じですが、dataは関数にする必要があります。


Vue.component('hello-world',{
  template: '<h1>{{ message }}</h1>',
  data: function(){
    return {
      message : 'Hello Vue'
    }
  }
})	

複数のdataを使いたい場合は下記のようになります。


  data: function(){
    return {
      message : 'Hello Vue',
      message2 : 'Hello World'
    }
  }	

以下はVueインスタンスの場合のdataの記述方法です。違いをしっかりと覚えておいて記述間違いをしないように注意しましょう。

dataを関数にすることでdataを持つ複数のコンポーネントを使った場合にコンポーネント毎にdataの独立の値を保持することができます。まずは次の”メソッドを追加する”の例を通してコンポーネント毎に独立した値を持つという意味を理解してください。

  data: {
    message: 'Hello Vue',
    message2: 'Hello World',
  }	

メソッドを追加する

コンポーネントはdataだけではなくメソッドも使うことができます。メソッドの追加はVueインスタンス作成時と同じ方法でmethodsプロパティで行います。

例を使ってコンポーネントのメソッドを理解していきます。コンポーネントの名前をbutton-counterとして、dataにcount変数を持たせ、countUp、countDownの2つのメソッドを追加します。ボタンにはv-onディレクティブでclickイベントを設定して、Upボタンを押すとcountが1増え、Downボタンを押すとcountが1減るプログラムを作成します。


Vue.component('button-counter',{
  template: '<p>カウント:{{ count }} <button v-on:click="countUp">Up</button><button v-on:click="countDown">Down</button></p>',
  data: function(){
    return {
      count : 0
    }
  },
  methods: {
    countUp: function(){
      this.count++
    },    
    countDown: function(){
      this.count--
    },
  }
})	

htmlにはbutton-counterタグを追加します。


<div id="app">
	<button-counter></button-counter>
</div>

Upボタンを押すとカウントが増え、Downボタンを押すとカウントが減ります。コンポーネントを使ってカウンターを作成することができました。

コンポーネントでカウンター作成
コンポーネントでカウンター作成

コンポーネントは再利用できることを先ほど学んだので、button-counterタグを複数設定してみましょう。


<div id="app">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

コンポーネントを複数作成した場合それぞれが独立したものとして作成されるので、Up、Downボタンはそのコンポーネント内のbutton-counterのみ影響を与えることができるため、countの値もそれぞれのコンポーネントで別々の値を保持することできます。

複数のカウンターを表示
複数のカウンターを表示

template内のhtmlを改行する

下記のようにtemplateに指定するタグが長い場合に途中で改行したい場合があります。シングルクオテーションで囲まれている場合は途中で改行を行うと正常に動作しません。


Vue.component('button-counter',{
  template: '<p>カウント:{{ count }} <button v-on:click="countUp">Up</button><button v-on:click="countDown">Down</button></p>',

templateで設定するhtmlが長い場合は、`(バッククォート/バックティック)を使用してください。下記は意図的にタグごとに改行していますが、改行を行なってもプログラムは正常に動作します。この記述はテンプレートリテラルと呼ばれます。Vueの機能ではなくJavaScriptの機能です。


Vue.component('button-counter',{
  template: `<p>
  カウント:{{ count }} 
  <button v-on:click="countUp">
  Up
  </button>
  <button v-on:click="countDown">
  Down
  </button>
  </p>`,

データの受け渡し(親→子)

ここまでの説明でコンポーネント単独での利用方法がわかりました。次はコンポーネント間でのデータの受け渡しの説明を行います。コンポーネント間のデータの受け渡しができれば、別々に作成されたコンポーネントを連携して利用することができます。

親コンポーネントから子コンポーネント、子コンポーネントから親コンポーネントと異なる方法でデータの受け渡しを行うので、混乱しないように注意する必要があります。

親と子について

本文書の最初に使ったHello Worldの例では、親コンポーネントは、hello-worldのコンポーネントを利用したVueインスタンスです。子コンポーネントはVue.componentで作成したhello-worldに対応します。

ここでは行いませんが子コンポーネント(hello-world)のtemplateプロパティでは、別のコンポーネントを利用することができます。その場合はhello-worldが親のコンポーネントになり、利用された別のコンポーネントが子コンポーネントになります。親と子の関係もしっかり理解してください。

親から子へのデータの受け渡し

親コンポーネントから子コンポーネントへのデータの受け渡しには、propsを使用します。


Vue.component('hello-world',{
  template: '<h1>Hello World and {{ message }}</h1>',
  props: ['message']
})	
ここでのデータはVueインスタンス内にあるdataプロパティのデータではありません。後ほど説明しますが、dataプロパティの場合はv-bindを利用して渡します。

親コンポーネントはmessage属性としてその値を渡します。


<div id="app">
  <hello-world message="Hello Vue"></hello-world>
</div>

ブラウザで確認するとmessage属性の値が親コンポーネントから子コンポーネントに渡され表示することが確認できます。

propsを使った処理
propsを使った処理

全体のコードです。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Component</title>
</head>
<body>
	<div id="app">

    <hello-world message="Hello Vue"></hello-world>

	</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

Vue.component('hello-world',{
  template: '<h1>Hello World and {{ message }}</h1>',
  props: ['message']
})

var app = new Vue({
  el: '#app',
  data: {
  	
  }
})

</script>	
</body>
</html>

親のdataを子へ

v-bindを使うことで親コンポーネントのdataを子コンポーネントに渡すことができます。

inputTextを親コンポーネントのdataに追加します。


var app = new Vue({
  el: '#app',
  data: {
    inputText: ''
  }
})	

Vue.componentは先ほど同様にmessage属性を使って親からデータを受け取ります。


Vue.component('hello-world',{
  template: '<h1>Hello World and {{ message }}</h1>',
  props: ['message']
})	

message属性をv-bindし、その値はdataプロパティのinputTextを設定します。これだけでinputTextの値は子コンポーネントに渡されますが親で変更されたinputTextが即座に反映されるのか確認するためにinput要素を追加して、v-modelでinputTextをバイディングします。


<div id="app">
  <input type="text" v-model="inputText">
  <hello-world v-bind:message="inputText"></hello-world>
</div>

フォームに入力した内容が即座にブラウザに反映されることが確認できます。

v-bind, v-modelを使用したコンポーネント
v-bind, v-modelを使用したコンポーネント

リストデータを子に渡す

v-forディレクティブを利用して、リストデータを子コンポーネントに渡すこともできます。dataにリストデータの追加を行います。


var app = new Vue({
  el: '#app',
  data: {
    posts: [
      {'id':0, 'title': 'vue.jsの基礎', 'content': 'about vue.js...'},
      {'id':1, 'title': 'componentの基礎', 'content': 'about component...'},
      {'id':2, 'title': 'Vue CLIの基礎', 'content': 'about Vue CLIの基礎...'},
    ]
  }
})	

v-forを使用し、値を渡す時にpostオブジェクトとして渡すことができます。


<div id="app">
  <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></blog-post>
</div>
v-forでv-bind:keyを使用せずに実行するとコンソールにはcomponent lists rendered with v-for should have explicit keysのメッセージが表示されます。v-forではkeysを設定して実行することを進めるメッセージです。ブラウザ側にはエラーが表示されませんが、コンソールにはメッセージがでているのでkeyを設定するようにしてください。

keyを設定する理由は知りたい人は下記が参考になります。

子コンポーネントはpostをオブジェクトとして受け取り、templateの中でpostの展開を行います。


Vue.component('blog-post',{
  template: '<div><h2>{{ post.title }}</h2><p>{{ post.content }}</p></div>',
  props: ['post']
})	

ブラウザでもpost毎にリストが展開されて表示されることを確認することができます。

v-forを使ってリスト表示
v-forを使ってリスト表示

先程はpostオブジェクトを渡して子コンポーネントで展開しましたが、オブジェクトではなく変数として渡すこともできます。


<div id="app">
  <blog-post v-for="post in posts" v-bind:key="post.id" v-bind:title="post.title" v-bind:content="post.content"></blog-post>
</div>

dataを受け取る側の子コンポーネントのpropsで受け取るそれぞれの属性名を設定します。


Vue.component('blog-post',{
  template: '<div><h2>{{ title }}</h2><p>{{ content }}</p></div>',
  props: ['title','content']
})	

データの受け渡し(子→親)

親から子へdataを渡したい場合はv-bindとpropsを使いました。子から親にdataを渡したい場合は、$emitというメソッドとイベントを利用して行います。propsでのデータ渡しに比較して複雑で最初は難しいと感じるかもしれませんが慣れていきましょう。

読み進めるためには、v-onディレクトリを使ったイベントの処理は理解しておく必要があります。

$emitを使って子から親へ

子から親へのデータの受け渡しは少し複雑ですが、一度自分の手を動かして設定を行えば比較的楽に理解することができます。

$emitを使った子コンポーネントから親コンポーネントへのデータの受け渡しの流れです。

  1. 子コンポーネント側でボタンに対してクリックイベントを設定します。
  2. クリックイベントが発生したら、子コンポーネントはそのイベントの処理(clickEvent)で$emitメソッドを実行し、親に渡したいイベント(from-child)を発生させます
  3. 親は子からくるイベントに対するalertMessageメソッドを設定しfrom-childイベントを待ちます。
  4. 親は子が発生させたイベント(from-child)を受け取ってalertMessageメソッドを実行します。

1.子コンポーネント側でボタンに対してクリックイベントを設定します


Vue.component('emit-event',{
 template: '<button v-on:click="clickEvent">ボタン</button>',	

2.クリックイベントが発生したら、子コンポーネントはそのイベントの処理(clickEvent)で$emitメソッドを実行し、親に渡したいイベント(from-child)を発生させます


  methods: {
    clickEvent: function(){
      this.$emit('from-child')
    }
  }	

3.親は子からくるイベントに対するalertMessageメソッドを設定しfrom-childイベントを待ちます。


<div id="app">
  <emit-event v-on:from-child="alertMessage"></emit-event>
</div>

4.親は子が発生させたイベント(from-child)を受け取ってalertMessageメソッドを実行します。


var app = new Vue({
  el: '#app',
  methods:{
    alertMessage: function(){
      alert('子からイベント受け取ったよ')
    }
  }
})	
子から親へイベント
子から親へイベント

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>Component Emit</title>
</head>
<body>
  <div id="app">
    <emit-event v-on:from-child="alertMessage"></emit-event>
  </div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>

Vue.component('emit-event',{
  template: '<button v-on:click="clickEvent">ボタン</button>',
  methods: {
    clickEvent: function(){
      this.$emit('from-child')
    }
  }
})

var app = new Vue({
  el: '#app',
  methods:{
    alertMessage: function(){
      alert('子からイベント受け取ったよ')
    }
  }
})

</script>	
</body>
</html>

子コンポーネントでv-on:clickの値にそのまま$emitを記述することもできます。直接記述をすることでclickEventメソッドも必要ではなくなります。


template: `<button v-on:click="$emit('from-child')">ボタン</button>`,

子から親へdataを送る

先程の例では、子で発生させたイベントを使って親に伝えることはできました。次は、子コンポーネント側のdataを親側に送ってみましょう。

子コンポーネントに入力フォームを作成し、入力後送信ボタンを押すとイベントが発生し、$emitメソッドを利用して、入力した内容を親に送ります。


Vue.component('emit-event',{
  template: `<div>
  <input type="text" v-model="inputText">
  <button v-on:click="clickEvent">送信ボタン</button>
  </div>`,
  data: function(){
    return {
      inputText : ''
    }
  },
  methods: {
    clickEvent: function(){
      this.$emit('from-child',this.inputText)
    }
  },
})	
templateの文字が長くなったので改行を行っています。1行の場合はシングルクオート(‘)でしたが、複数の場合は、バッククオート(`)を使います。バッククオートはキーボードの@の場所にあります。

$emitは引数を取ることができ、この引数を利用してデータを送ります。複数のデータを渡す場合はオブジェクトを利用して渡すことができます。


this.$emit('from-child',this.inputText,XXX)	

親側で子からのイベントの設定を行います。


<div id="app">
  <p>{{ message }}</p>
  <emit-event v-on:from-child="receiveMessage"></emit-event>
</div>

receiveMessageメソッドは受け取ったmessageをdataのmessageに設定します。


var app = new Vue({
  el: '#app',
  data: {
    message: ''
  },
  methods:{
    receiveMessage: function(message){
      this.message = message;
    }
  }
})

子コンポーネントのフォームに入力したテキストが親コンポーネントに渡され、ブラウザに表示することを確認できます。

子からデータを受け取った
子からデータを受け取った

コンポーネントとSlot(スロット)

slotの機能を利用すると親側でコンポーネントタグの間に挿入した内容を子コンポーネント側で表示することができます。

Slotについてはvue.js2.6+で記述方法に変更もありました。Slotについては下記の文書を読むことをおすすめします。

slotによる挿入

親側で下記のように子コンポーネントタグの間にコンテンツを挿入します。


<div id="app">
  <slot-test>スロットによる挿入</slot-test>
</div>

子側のコンポーネントでは親側で挿入されたコンテンツを表示させる場所にslotタグを入れておきます。


Vue.component('slot-test',{
  template: '<p><slot></slot></p>',
}),

ブラウザで確認すると親側で子コンポーネントタグの間に挿入したコンテンツが下記のように表示されます。

slotを使用して表示
slotを使用して表示

slotの初期値の設定

slotタグの間に初期値を設定することで親側からのSlotの挿入がない場合は設定した初期値を表示させることができます。


Vue.component('slot-test',{
  template: '<p><slot>デフォルト</slot></p>',
})

親側で挿入するコンテンツがあるものとないもの2つのコンポーネントタグを記述します。


<div id="app">
  <slot-test>スロットによる挿入</slot-test>
 <slot-test></slot-test>
</div>

挿入がない場合は、初期値であるデフォルトが表示されることが確認できます。

slotに初期値がある場合
slotに初期値がある場合

slotに名前をつける

先程まではslotに挿入できる内容は1つでしたが、複数ある場合は表示させる場所を識別するためslotに名前をつけることができます。slotタグにname属性をつけその値にslot名を設定することで識別します。


Vue.component('slot-test',{
  template: `<div>
  <h1><slot name="title">タイトル</slot></h1>
  <p><slot name="content">内容</slot></p>
  </div>`,
})

slotを挿入する親側ではタグにslot属性を追加しその値に子コンポーネントで定義されているslotの名前を入力してその間に渡したいコンテンツを挿入します。


<div id="app">
  <slot-test>
    <div slot="title">vue.jsの基礎</div>
    <p slot="content">vue.jsについて</p>
  </slot-test>
</div>

ブラウザで確認すると指定したslot名の通りにコンテンツが表示されることが確認できます。

slotに名前をつける
slotに名前をつける

HTMLのソースを確認するとslot属性を設定したdivタグとpタグも一緒に表示されます。

名前をつけたslotのタグの状態
名前をつけたslotのタグの状態

div, pタグを表示させないため、templateタグに変更を行います。templateタグを利用すると先ほどまでコンソールに表示されていたdivタグとpタグが表示されなくなり期待通りのタグ設定になります。


<div id="app">
  <slot-test>
    <template slot="title">vue.jsの基礎</template>
    <template slot="content">vue.jsについて</template>
  </slot-test>
</div>
templateタグを使用した結果
templateタグを使用した結果