コンポーネントの基礎
コンポーネントによって UI を独立した再利用可能なピースに分割し、それぞれのピースを切り離して考えることができるようになります。アプリケーションはネストされたコンポーネントのツリーによって構成されているのが一般的です:
これは、ネイティブの HTML 要素をネストする方法ととてもよく似ていますが、Vue は独自のコンポーネントモデルを実装しており、各コンポーネントのカスタムコンテンツとロジックをカプセル化することができます。 Vue はまた、ネイティブの Web コンポーネントとうまく連携しています。 Vue コンポーネントとネイティブの Web コンポーネントの関係について興味があるようでしたらこちらを参照してください。
コンポーネントの定義
ビルドステップを使用する場合は通常、各 Vue コンポーネントは専用のファイルで .vue
拡張子を使用して定義します。これは 単一ファイルコンポーネント(略して SFC)として知られています:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
ビルドステップを使用しない場合、Vue コンポーネントは Vue 固有のオプションを含むプレーンな JavaScript オブジェクトとして定義することができます:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// DOM 内テンプレートをターゲットにすることもできます
// template: '#my-template-element'
}
テンプレートは、ここで JavaScript の文字列としてインライン化され、Vue がその場でコンパイルします。また、ID セレクターを使って要素を指定(通常はネイティブの <template>
要素)することもできます。 Vue はそのコンテンツをテンプレート・ソースとして使用します。
上記の例では 1 つのコンポーネントを定義し、それを .js
ファイルのデフォルトエクスポートとしてエクスポートしていますが、名前付きエクスポートを使用すると、同じファイルから複数のコンポーネントをエクスポートすることができます。
コンポーネントの使用
TIP
このガイドの残りの部分では SFC 構文を使用します。コンポーネントに関するコンセプトは、ビルドステップを使用するかどうかに関係なく、同じものです。サンプルセクションでは、両方のシナリオでのコンポーネントの使い方をお見せしています。
子コンポーネントを使用するには、親コンポーネントでインポートする必要があります。カウントするコンポーネントを ButtonCounter.vue
というファイル内に配置したとすると、このコンポーネントはそのファイルのデフォルトエクスポートとして公開されます:
vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
<script setup>
と共に使用すると、インポートしたコンポーネントは自動的にテンプレートで使用できるようになります。
また、コンポーネントをグローバル登録することで、インポートすることなくアプリケーション内のすべてのコンポーネントで利用できるようにすることもできます。グローバル登録とローカル登録のメリットとデメリットは、専用のコンポーネントの登録セクションで説明されています。
コンポーネントは好きなだけ、何度でも再利用可能です:
template
<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
ボタンをクリックすると、それぞれが別の count
を維持することに注意してください。これは、コンポーネントを使用するたびに、新しいインスタンスが作成されるからです。
SFC では、ネイティブの HTML 要素と区別するために、子コンポーネントに パスカルケース
のタグ名を使用することが推奨されます。ネイティブの HTML のタグ名は大文字小文字を区別しませんが、Vue の SFC はコンパイルされたフォーマットなので、大文字小文字を区別するタグ名を使うことができます。また、タグを閉じるために />
を使用することができます。
テンプレートを DOM で直接作成する場合(例えば、ネイティブの <template>
要素のコンテンツとして)、テンプレートはブラウザのネイティブな HTML パース動作に従います。そのような場合には、ケバブケース
を使用してコンポーネントにクロージングタグを明示する必要があります:
template
<!-- DOM の中にテンプレートが書かれた場合 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
詳細は DOM 内テンプレート解析の注意点を参照ください。
props の受け渡し
ブログを構築する場合、ブログの記事を表示するコンポーネントが必要になるかと思います。すべてのブログ記事が同じレイアウトで表示されるようにしたいのですが、コンテンツは異なっています。このようなコンポーネントは、表示したい特定の記事のタイトルや内容などのデータを渡すことができない限り役に立ちません。そこで props の出番です。
props はコンポーネントに登録できるカスタム属性のことです。ブログ記事コンポーネントにタイトルを渡すには、defineProps
マクロを使って、このコンポーネントが受け取る props のリストを宣言する必要があります:
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
defineProps
はコンパイル時のマクロで <script setup>
内でのみ利用可能であり、明示的にインポートする必要はありません。宣言された props は、自動的にテンプレートに公開されます。また、defineProps
はコンポーネントに渡されたすべての props を含むオブジェクトを返すので、必要に応じて JavaScript 内でアクセスすることができます:
js
const props = defineProps(['title'])
console.log(props.title)
<script setup>
を使わない場合、props は props
オプションで宣言する必要があり、props オブジェクトは setup()
の第 1 引数として渡されます:
js
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
コンポーネントは好きなだけ props を持つことができ、デフォルトでどんな値でも、どの props にも渡すことができます。
props が登録されると、以下のようにカスタム属性としてデータを渡すことができるようになります:
template
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
しかしながら、一般的なアプリケーションでは親コンポーネントに投稿の配列があることが多いでしょう:
js
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])
このように各コンポーネントをレンダリングしたい場合は、v-for
を使用します:
template
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
動的な props の値を渡すために v-bind
がどう使用されているかに注目してください。これは、レンダリングするコンテンツを事前に正確に把握していない場合に特に役立ちます。
props については以上となりますが、このページを読み終え内容に慣れてきたら、後ほどpropsの完全ガイドを読みにくることをおすすめします。
イベントの購読
<BlogPost>
コンポーネントを開発していく中で、いくつかの機能については、親コンポーネントへの通信が必要になるかもしれません。例えば、ブログ記事のテキストを拡大し、ページの残りの部分はデフォルトのサイズのままにしておくアクセシビリティー機能を含めることにするかもしれません。
親コンポーネントの中では、postFontSize
という ref を追加することで、この機能をサポートできます:
js
const posts = ref([
/* ... */
])
const postFontSize = ref(1)
これは、テンプレート内で使用することができ、すべてのブログ記事のフォントサイズを制御することができます:
template
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>
では、<BlogPost>
コンポーネントのテンプレートにボタンを追加してみましょう:
vue
<!-- BlogPost.vue, omitting <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>
ボタンはまだ何もしません。クリックするとすべての投稿のテキストを拡大表示するように親に伝達したいです。この問題を解決するために、コンポーネントはカスタムイベントシステムを提供します。親は子コンポーネントインスタンス上の任意のイベントを、ちょうどネイティブの DOM イベントのように v-on
または @
で購読できます:
template
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
そして、子コンポーネントは組み込みの $emit
メソッドを呼び出し、イベント名を渡すことによって自身のイベントを発行することができます:
vue
<!-- 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
の値を更新することができます。
オプションとして defineEmits
マクロを使って発行するイベントを宣言することができます:
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
コンポーネントが発行する全てのイベントをドキュメント化することで、必要に応じてそれらをバリデーションしています。また、これは Vue が暗黙的に子コンポーネントのルート要素にイベントをネイティブリスナーとして適用するのを避けることにもなります。
defineProps
と同様に defineEmits
も <script setup>
内でのみ使用することができ、インポートする必要はありません。これは、$emit
メソッドと同等の emit
関数を返します。これは、コンポーネントの <script setup>
セクション($emit
に直接アクセスできない)で、イベントを発行させるために使用します:
vue
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
<script setup>
を使用していない時は emits
オプションを使用してイベント発行を宣言することができます。setup コンテキスト (setup()
に第 2 引数として渡されます) のプロパティとして emit
関数にアクセスすることができます:
js
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
カスタムコンポーネントについては以上となりますが、このページを読み終え内容に慣れてきたら、後ほどカスタムイベントの完全ガイドを読みにくることをおすすめします。
スロットを使ったコンテンツ配信
HTML 要素と同じように、以下のようにコンポーネントにコンテンツを渡すことができると便利なことがよくあります:
template
<AlertBox>
Something bad happened.
</AlertBox>
これは以下のようなレンダリングがされるかもしれません:
これはデモ目的のエラーです
何らかのエラーが発生しました。
これは Vue のカスタム要素 <slot>
を用いて実現することができます:
vue
<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
で実現されています:
template
<!-- currentTab 変更時にコンポーネントが変わります -->
<component :is="tabs[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
イベント名は、すべてケバブケース(ハイフン区切り)を使用する必要があるということになります:
js
// JavaScript 内ではキャメルケース
const BlogPost = {
props: ['postTitle'],
emits: ['updatePost'],
template: `
<h3>{{ postTitle }}</h3>
`
}
template
<!-- HTML 内ではケバブケース -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
自己クロージングタグ
これまでのコードサンプルでは、コンポーネントに自己クロージング (self-closing) タグを使用していました:
template
<MyComponent />
これは、Vue のテンプレートパーサーが />
を、タグの種類に関係なく任意のタグを終了する指示として尊重するためです。
しかし、DOM 内テンプレートでは必ず明示的なクロージングタグを入れる必要があります:
template
<my-component></my-component>
これは HTML の仕様では、いくつかの特定の要素でのみ自己クロージングタグの省略が認められているからです。最も一般的なのは <input>
と <img>
です。他のすべての要素では、自己クロージングタグを省略すると、ネイティブの HTML パーサーは開始タグを終了させなかったと判断します。例えば、次のようなスニペットです:
template
<my-component /> <!-- ここがクロージングタグのつもりです -->
<span>hello</span>
このようにパースされます:
template
<my-component>
<span>hello</span>
</my-component> <!-- ですが、ブラウザーはここでクローズします -->
要素の配置制限
<ul>
、 <ol>
、 <table>
、 <select>
など、一部の HTML 要素には内部に表示できる要素に制限があります。例えば <li>
などの一部の要素には、 <tr>
、および <option>
は特定の要素内にのみ表示できます。
このような制限のある要素でコンポーネントを使用する場合に問題が発生します。例えば:
template
<table>
<blog-post-row></blog-post-row>
</table>
カスタムコンポーネント <blog-post-row>
は無効なコンテンツとして巻き上げられ、最終的なレンダリング出力でエラーが発生します。回避策として、特別な is
属性 を使用することができます:
template
<table>
<tr is="vue:blog-post-row"></tr>
</table>
TIP
ネイティブの HTML 要素で使用する場合、Vue コンポーネントとして解釈されるためには is
の値の前に vue:
を付けなければなりません。これはネイティブの組み込みのカスタマイズ要素との混同を避けるために必要となります。
DOM 内テンプレート解析の注意点については、以上で終わりです。そして実は、Vue の Essentials(エッセンス集)はこれで終わりです。おめでとうございます!まだ学ぶことはありますが、ひとまずは休みをいれて、あなた自身が Vue で遊び、何か楽しいものを作ってみることをおすすめします。もしくは、サンプル集をまだ見ていないようであれば、チェックしてください。
いま受けたダイジェストの知識に慣れてきたと感じたら、ガイドを進めてコンポーネントについてより深く学んでみましょう。