[Vue入門] Componentsの基礎

コンポーネントの基礎

コンポーネントによって UI を独立した再利用可能なピースに分割し、それぞれのピースを切り離して考えることができるようになります。アプリケーションはネストされたコンポーネントのツリーによって構成されているのが一般的です:

これは、ネイティブの HTML 要素をネストする方法ととてもよく似ていますが、Vue は独自のコンポーネントモデルを実装しており、各コンポーネントのカスタムコンテンツとロジックをカプセル化することができます。 Vue はまた、ネイティブの Web コンポーネントとうまく連携しています。

コンポーネントの定義

ビルドステップを使用する場合は通常、各 Vue コンポーネントは専用のファイルで .vue 拡張子を使用して定義します。これは 単一ファイルコンポーネント(略して SFC)として知られています:

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

ビルドステップを使用しない場合、Vue コンポーネントは Vue 固有のオプションを含むプレーンな JavaScript オブジェクトとして定義することができます:

export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
}

テンプレートは、ここで JavaScript の文字列としてインライン化され、Vue がその場でコンパイルします。また、ID セレクターを使って要素を指定(通常はネイティブの <template> 要素)することもできます。 Vue はそのコンテンツをテンプレート・ソースとして使用します。

上記の例では 1 つのコンポーネントを定義し、それを .js ファイルのデフォルトエクスポートとしてエクスポートしていますが、名前付きエクスポートを使用すると、同じファイルから複数のコンポーネントをエクスポートすることができます。

コンポーネントの使用

TIP

このガイドの残りの部分では SFC 構文を使用します。コンポーネントに関するコンセプトは、ビルドステップを使用するかどうかに関係なく、同じものです。サンプルセクションでは、両方のシナリオでのコンポーネントの使い方をお見せしています。

子コンポーネントを使用するには、親コンポーネントでインポートする必要があります。カウントするコンポーネントを ButtonCounter.vue というファイル内に配置したとすると、このコンポーネントはそのファイルのデフォルトエクスポートとして公開されます:

<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

インポートしたコンポーネントをテンプレートに公開するには、components オプションでコンポーネントを登録する必要があります。これにより、そのコンポーネントは登録されたキーを使ってタグとして利用できるようになります。

また、コンポーネントをグローバル登録することで、インポートすることなくアプリケーション内のすべてのコンポーネントで利用できるようにすることもできます。グローバル登録とローカル登録のメリットとデメリットは、専用のコンポーネントの登録セクションで説明されています。

コンポーネントは好きなだけ、何度でも再利用可能です:

<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

プレイグラウンドで試す

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

SFC では、ネイティブの HTML 要素と区別するために、子コンポーネントに パスカルケース のタグ名を使用することが推奨されます。ネイティブの HTML のタグ名は大文字小文字を区別しませんが、Vue の SFC はコンパイルされたフォーマットなので、大文字小文字を区別するタグ名を使うことができます。また、タグを閉じるために /> を使用することができます。

テンプレートを DOM で直接作成する場合(例えば、ネイティブの <template> 要素のコンテンツとして)、テンプレートはブラウザのネイティブな HTML パース動作に従います。そのような場合には、ケバブケース を使用してコンポーネントにクロージングタグを明示する必要があります:

<!-- DOM の中にテンプレートが書かれた場合 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

Props の受け渡し

ブログを構築する場合、ブログの記事を表示するコンポーネントが必要になるかと思います。すべてのブログ記事が同じレイアウトで表示されるようにしたいのですが、コンテンツは異なっています。このようなコンポーネントは、表示したい特定の記事のタイトルや内容などのデータを渡すことができない限り役に立ちません。そこで props の出番です。

props はコンポーネントに登録できるカスタム属性のことです。ブログ記事コンポーネントにタイトルを渡すには、このコンポーネントが受け取る props のリスト内で props オプション を使って props プロパティの使用を宣言する必要があります:

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

props 属性に値が渡されると、その値はコンポーネントインスタンスのプロパティになります。プロパティの値は、他のコンポーネントプロパティと同様に、テンプレートの中やコンポーネントの this コンテキストでアクセスすることができます。

コンポーネントは好きなだけ props を持つことができ、デフォルトでどんな値でも、どの props にも渡すことができます。

props が登録されると、以下のようにカスタム属性としてデータを渡すことができるようになります:

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

しかしながら、一般的なアプリケーションでは親コンポーネントに投稿の配列があることが多いでしょう:

export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

このように各コンポーネントをレンダリングしたい場合は、v-for を使用します:

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

プレイグラウンドで試す

v-bind を使って、動的な props を渡すことができることに注目してください。これは、レンダリングするコンテンツを事前に正確に把握していない場合に特に役立ちます。

props については以上となりますが、このページを読み終え内容に慣れてきたら、後ほど Props の完全ガイドを読みにくることをおすすめします。

イベントのリッスン

<BlogPost> コンポーネントを開発していく中で、いくつかの機能については、親コンポーネントへの通信が必要になるかもしれません。例えば、ブログ記事のテキストを拡大し、ページの残りの部分はデフォルトのサイズのままにしておくアクセシビリティ機能を含めることにするかもしれません。

親コンポーネントの中では、postFontSize という data property を追加することで、この機能をサポートできます:

data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}

これは、テンプレート内で使用することができ、すべてのブログ記事のフォントサイズを制御することができます:

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

では、<BlogPost> コンポーネントのテンプレートにボタンを追加してみましょう:

<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Enlarge text</button>
  </div>
</template>

ボタンは今のところ何もしませんが、クリックするとすべての投稿のテキストを拡大表示するように親に伝達したいです。この問題を解決するために、コンポーネントインスタンスはカスタムイベントシステムを提供します。親は子コンポーネントインスタンス上の任意のイベントを、ちょうどネィティブの DOM イベントのように v-on または @ で、リッスンするよう選択できます:

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

そして、子コンポーネントは組み込みの $emit メソッドを呼び出し、イベント名を渡すことによって自身のイベントを発行することができます:

<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

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

プレイグラウンドで試す

オプションとして emits オプションを使って emit イベントを宣言することができます:

<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>

コンポーネントが発行する全てのイベントをドキュメント化することで、必要に応じてそれらをバリデーションしています。また、これは Vue が暗黙的に子コンポーネントのルート要素にイベントをネイティブリスナーとして適用するのを避けることにもなります。

カスタムコンポーネントについては以上となりますが、このページを読み終え内容に慣れてきたら、後ほどカスタムイベントの完全ガイドを読みにくることをおすすめします。

スロットを使ったコンテンツ配信

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

<AlertBox>
  Something bad happened.
</AlertBox>

これは以下のようなレンダリングがされるかもしれません:

これはデモ目的のエラーです

何らかのエラーが発生しました。

これは Vue のカスタム要素 <slot> を用いて実現することができます:

<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

上で見たように、コンテンツを配置するプレースホルダーとして <slot> を使う – それだけです。これで完了です!

プレイグラウンドで試す

スロットについては以上となりますが、このページを読み終え内容に慣れてきたら、後ほどスロットの完全ガイドを読みにくることをおすすめします。

動的コンポーネント

ときどきタブ付きインターフェイスのような、コンポーネントを動的な切り替えが役立つ時があります:

プレイグラウンドのサンプルを開く

上記は Vue の <component> 要素の特別な属性 is で実現されています:

<!-- currentTab 変更時にコンポーネントが変わります -->
<component :is="currentTab"></component>

上の例では、:is に渡される値に以下のいずれかを含めることができます:

  • 登録されたコンポーネントの文字列、もしくは
  • 実際にインポートされたコンポーネントオブジェクト

また、is 属性を使って、通常の HTML 要素を作成することもできます。

複数のコンポーネントを <component :is="..."> で切り替えた場合、切り変えられたコンポーネントがアンマウントされます。組み込みの <KeepAlive> コンポーネント を使用すれば、アクティブでないコンポーネントを強制的に “生きて” いる状態にすることができます。

DOM テンプレート解析の注意点

Vue のテンプレートを DOM に直接記述する場合、Vue は DOM からテンプレート文字列を取得する必要があります。これはブラウザのネイティブな HTML パースのふるまいに、いくつかの注意点をもたらします。

TIP

以下で説明する制限事項は、DOM に直接テンプレートを記述する場合にのみ適用されます。以下のソースからの文字列テンプレートを使用する場合は適用されません:

  • 単一ファイルコンポーネント
  • インラインのテンプレート文字列(例: template: '...'
  • <script type="text/x-template">

大文字小文字の区別

HTML タグや属性名は大文字と小文字を区別しないので、ブラウザーはどの大文字も小文字として解釈します。つまり、DOM 内テンプレートを使用する場合、パスカルケースのコンポーネント名、キャメルケースの props 名、v-on イベント名は、すべてケバブケース(ハイフン区切り)を使用する必要があるということになります:

// JavaScript 内ではキャメルケース
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
<!-- HTML 内ではケバブケース -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

自己クロージングタグ

これまでのコードサンプルでは、コンポーネントに自己クロージング (self-closing) タグを使用していました:

<MyComponent />

これは、Vue のテンプレートパーサーが /> を、タグの種類に関係なく任意のタグを終了する指示として尊重するためです。

しかし、DOM テンプレートでは必ず明示的なクロージングタグを入れる必要があります:

<my-component></my-component>

これは HTML の仕様では、いくつかの特定の要素でのみ自己クロージングタグの省略が認められているからです。最も一般的なのは <input> と <img> です。他のすべての要素では、自己クロージングタグを省略すると、ネイティブの HTML パーサーは開始タグを終了させなかったと判断します。例えば、次のようなスニペットです:

<my-component /> <!-- ここがクロージングタグのつもりです -->
<span>hello</span>

このようにパースされます:

<my-component>
  <span>hello</span>
</my-component> <!-- ですが、ブラウザーはここでクローズします -->

要素の配置制限

<ul> 、 <ol> 、 <table> 、 <select> など、一部の HTML 要素には内部に表示できる要素に制限があります。例えば <li> などの一部の要素には、 <tr> 、および <option> は特定の要素内にのみ表示できます。

このような制限のある要素でコンポーネントを使用する場合に問題が発生します。例えば:

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

カスタムコンポーネント <blog-post-row> は無効なコンテンツとして巻き上げられ、最終的なレンダリング出力でエラーが発生します。回避策として、特別な is 属性 を使用することができます:

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

TIP

ネイティブの HTML 要素で使用する場合、Vue コンポーネントとして解釈されるためには is の値の前に vue: を付けなければなりません。これはネイティブの組み込みのカスタマイズ要素との混同を避けるために必要となります。

DOM テンプレート解析の注意点については、以上で終わりです。