コンポーネントの基本

最終更新日: 2019年7月22日

基本例

Vue コンポーネントの例を次に示します:

// button-counter と呼ばれる新しいコンポーネントを定義します
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

コンポーネントは名前付きの再利用可能な Vue インスタンスです。この例の場合、<button-counter>です。このコンポーネントを new Vue で作成されたルート Vue インスタンス内でカスタム要素として使用することができます。

<div id="components-demo">
  <button-counter></button-counter>
</div>
new Vue({ el: '#components-demo' })

コンポーネントは再利用可能な Vue インスタンスなので、datacomputedwatchmethods、ライフサイクルフックなどの new Vue と同じオプションを受け入れます。唯一の例外は el のようなルート固有のオプションです。

コンポーネントの再利用

コンポーネントは必要なだけ何度でも再利用できます:

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

ボタンをクリックすると、それぞれが独自の count を保持することに注意してください。これはコンポーネントを使用するたびに、新しいインスタンスが作成されるためです。

data は関数でなければなりません

<button-counter>コンポーネントを定義したとき、data が直接オブジェクトとして提供されていなかったことに気づいたかもしれません:

data: {
  count: 0
}

代わりに、コンポーネントの data オプションは関数でなければなりません。各インスタンスが返されるデータオブジェクトの独立したコピーを保持できるためです:

data: function () {
  return {
    count: 0
  }
}

Vue にこのルールがない場合、ボタンを1つクリックすると、以下のようにすべての他のインスタンスのデータに影響します:

コンポーネントの編成

アプリケーションがネストされたコンポーネントのツリーに編成されるのは一般的です:

Component Tree

例えば、ヘッダー、サイドバー、およびコンテンツ領域のコンポーネントがあり、それぞれには一般的にナビゲーションリンク、ブログ投稿などの他のコンポーネントが含まれています。

これらのコンポーネントをテンプレートで使用するには、Vue がそれらを認識できるように登録する必要があります。コンポーネント登録には、グローバルローカルの2種類があります。これまでは、Vue.component を使用してコンポーネントをグローバルに登録していただけです:

Vue.component('my-component-name', {
  // ... オプション ...
})

グローバルに登録されたコンポーネントは、その後に作成されたルート Vue インスタンス(new Vue)のテンプレートで使用できます。さらに、その Vue インスタンスのコンポーネントツリーのすべてのサブコンポーネント内でも使用できます。

今のところコンポーネント登録について知っておくべきことはこれですべてですが、このページを読んで内容が分かり次第、コンポーネント登録の全ガイドを読むことをお勧めします。

プロパティを使用した子コンポーネントへのデータの受け渡し

先程、ブログ投稿用のコンポーネントの作成についてふれました。問題は、表示する特定の投稿のタイトルやコンテンツなどのデータをコンポーネントに渡すことができない限り、そのコンポーネントは役に立たないということです。プロパティはここで役立ちます。

プロパティはコンポーネントに登録できるカスタム属性です。値がプロパティ属性に渡されると、そのコンポーネントインスタンスのプロパティになります。ブログ投稿コンポーネントにタイトルを渡すには、props オプションを使用して、このコンポーネントが受け入れるプロパティのリストにそれを含めることができます:

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

コンポーネントは必要に応じて多くのプロパティを持つことができます。デフォルトでは、任意の値を任意のプロパティに渡すことができます。上記のテンプレートでは、data と同様に、コンポーネントインスタンスでこの値にアクセスできることがわかります。

プロパティが登録されると、次のようにそれをカスタム属性としてデータを渡すことができます:

<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>

しかしながら、普通のアプリケーションでは、おそらく data に投稿の配列があります:

new Vue({
  el: '#blog-post-demo',
  data: {
    posts: [
      { id: 1, title: 'My journey with Vue' },
      { id: 2, title: 'Blogging with Vue' },
      { id: 3, title: 'Why Vue is so fun' }
    ]
  }
})

それぞれの投稿ごとにコンポーネントを描画します:

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

上記では、v-bind を使って動的にプロパティを渡すことができることがわかります。これは、API から投稿を取得するときのように、前もって描画する正確なコンテンツがわからない場合に特に便利です。

これがプロパティについて今のところ知っておくべきことですが、このページを読んで内容が分かり次第、後でプロパティの全ガイドを読むことをお勧めします。

単一のルート要素

<blog-post>コンポーネントを構築するとき、テンプレートには最終的にタイトル以上のものが含まれます:

<h3>{{ title }}</h3>

最低でも、投稿の内容を含めたいでしょう:

<h3>{{ title }}</h3>
<div v-html="content"></div>

テンプレートで試してみると、Vue はすべてのコンポーネントに単一のルート要素が必要ということを示すエラーを表示します。このエラーは、次のようにテンプレートを親要素でラップすることで修正できます:

<div class="blog-post">
  <h3>{{ title }}</h3>
  <div v-html="content"></div>
</div>

コンポーネントが大きくなると、タイトルや投稿内容だけでなく、公開日やコメントなども必要になってくるかもしれません。しかし、それぞれの情報ごとにプロパティを定義してしまうと、とてもうるさいものになります:

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

そうなったら、<blog-post> コンポーネントをリファクタする好機かもしれません。代わりに、単一の post プロパティを受け入れる形にするのです:

<blog-post
  v-for="post in posts"
  v-bind:key="post.id"
  v-bind:post="post"
></blog-post>
Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <div v-html="post.content"></div>
    </div>
  `
})

上記の例や後に出てくる例では、JavaScript のテンプレート文字列を使用して、複数行にわたるテンプレートをより読みやすくします。これらはインターネットエクスプローラー(IE)ではサポートされていないので、IE をサポートして、かつトランスパイル(例: Babel もしくは TypeScript を使用した)を行わない場合、代わりに改行エスケープを使用してください。

これで、新しいプロパティが post オブジェクトに追加される際にはいつでも、<blog-post> 内で自動的に利用可能になるのです。

子コンポーネントのイベントを購読する

<blog-post> コンポーネントを開発する際、親コンポーネントとやり取りする機能が必要になるかもしれません。例えば、ブログの投稿のテキストを拡大するためのアクセシビリティ機能を追加し、他のページのデフォルトのサイズにすることができます。

親コンポーネントでは、postFontSize データプロパティを追加することでこの機能をサポートすることができます:

new Vue({
  el: '#blog-posts-events-demo',
  data: {
    posts: [/* ... */],
    postFontSize: 1
  }
})

すべてのブログ投稿のフォントサイズを制御するためにテンプレート内で使用できます:

<div id="blog-posts-events-demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      v-bind:key="post.id"
      v-bind:post="post"
    ></blog-post>
  </div>
</div>

それでは、すべての投稿の内容の前にテキストを拡大するボタンを追加します:

Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button>
        Enlarge text
      </button>
      <div v-html="post.content"></div>
    </div>
  `
})

問題は、このボタンがなにもしないことです:

<button>
  Enlarge text
</button>

ボタンをクリックすると、すべての投稿のテキストを拡大する必要があることを親コンポーネントに伝える必要があります。幸いにも、Vue インスタンスはこの問題を解決するカスタムイベントシステムを提供しています。親コンポーネントは、ネイティブの DOM イベントと同じように、v-on を使って子コンポーネントで起きた任意のイベントを購読することができます:

<blog-post
  ...
  v-on:enlarge-text="postFontSize += 0.1"
></blog-post>

そして、子コンポーネントでは、ビルトインの $emit メソッド にイベントの名前を渡して呼び出すことで、イベントを送出することができます:

<button v-on:click="$emit('enlarge-text')">
  Enlarge text
</button>

親コンポーネントは、v-on:enlarge-text="postFontSize += 0.1" リスナのおかげで、このイベントを受け取って postFontSize の値を更新することができます。

イベントと値を送出する

イベントを特定の値付きで送出すると便利なことがあります。例えば、<blog-post> コンポーネントにテキストをどれだけ拡大するかを責務とさせたいかもしれません。そのような場合、$emit の2番目のパラメータを使ってこの値を提供することができます:

<button v-on:click="$emit('enlarge-text', 0.1)">
  Enlarge text
</button>

親コンポーネントでイベントをリッスンすると、送出されたイベントの値に $event でアクセスできます:

<blog-post
  ...
  v-on:enlarge-text="postFontSize += $event"
></blog-post>

または、イベントハンドラがメソッドの場合:

<blog-post
  ...
  v-on:enlarge-text="onEnlargeText"
></blog-post>

値は、そのメソッドの最初のパラメータとして渡されます:

methods: {
  onEnlargeText: function (enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}

コンポーネントで v-model を使う

カスタムイベントは v-model で動作するカスタム入力を作成することもできます。このことを覚えておいてください:

<input v-model="searchText">

これは以下と同じことです:

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

コンポーネントで使用する場合、v-model は代わりにこれを行います:

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

これを実際に動作させるためには、コンポーネント内の <input> は以下でなければなりません:

こうなります:

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})

v-model はこのコンポーネントで完璧に動作するはずです:

<custom-input v-model="searchText"></custom-input>

これがカスタムコンポーネントについて今のところ知っておくべきことですが、このページを読んで内容が分かり次第、後でカスタムイベントの全ガイドを読むことをお勧めします。

スロットによるコンテンツ配信

HTML 要素と同様に、コンポーネントにコンテンツを渡すことができると便利なことがよくあります。例えば以下です:

<alert-box>
  Something bad happened.
</alert-box>

これは以下のように描画されるでしょう:

Something bad happened.

幸いにも、この作業は Vue のカスタム <slot> 要素によって非常に簡単になります:

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

上で見たように、ただ渡したいところにスロットを追加するだけです。それだけです。終わりです!

これがスロットについて今のところ知っておくべきことですが、このページを読んで内容が分かり次第、後でスロットの全ガイドを読むことをお勧めします。

動的なコンポーネント

タブ付きのインターフェイスのように、コンポーネント間を動的に切り替えると便利なことがあります:

上記は、Vue の <component> 要素と 特別な属性の is で可能になりました:

<!-- currentTabComponent が変更されたとき、コンポーネントを変更します -->
<component v-bind:is="currentTabComponent"></component>

上記の例では、currentTabComponent は次のいずれかを含むことができます:

完全なコードを試してみるにはこの fiddle、もしくは登録された名前の代わりにコンポーネントのオプションオブジェクトをバインディングしている例となるこのバージョンを参照してください。

これが動的なコンポーネントについて今のところ知っておくべきことですが、このページを読んで内容が分かり次第、後で 動的 & 非同期コンポーネントの全ガイドを読むことをお勧めします。

DOM テンプレートパース時の警告

<ul><ol><table><select>のようないくつかの HTML 要素には、それらの要素の中でどの要素が現れるかに制限があり、 <li><tr><option> は他の特定の要素の中にしか現れません。

このような制限がある要素を持つコンポーネントを使用すると、問題が発生することがあります。例:

<table>
  <blog-post-row></blog-post-row>
</table>

カスタムコンポーネント <blog-post-row> は無効なコンテンツとしてつまみ出され、最終的に描画された出力にエラーが発生します。幸いにも、特別な属性の is は回避策を提供します:

<table>
  <tr is="blog-post-row"></tr>
</table>

次のソースのいずれかの文字列テンプレートを使用している場合、この制限は適用されないことに注意してください:

これがDOM テンプレートパース時の警告について今のところ知っておくべきことです。そして、実際には Vue の 本質 の最後となります。おめでとうございます!まだまだ学ぶことはありますが、最初に Vue を自身で遊ぶために休憩をとり、何か面白いものを作ってみることをお勧めします。

理解したばかりの知識に慣れたら、動的 & 非同期コンポーネントの全ガイドとサイドバーにある他のコンポーネントの詳細セクションを読むことをお勧めします。