# コンポーネントの基本

# 基本例

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

// Vue アプリケーションを作成します
const app = Vue.createApp({})

// グローバルな button-counter というコンポーネントを定義します
app.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

INFO

ここでは単純な例を示していますが, 典型的な Vue アプリケーションでは文字列テンプレートではなく単一ファイルコンポーネントを使用します。 詳しくはこちらで解説しています。

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

<div id="components-demo">
  <button-counter></button-counter>
</div>
1
2
3
app.mount('#components-demo')
1

コンポーネントは再利用可能なインスタンスなので、datacomputedwatchmethods、そしてライフサイクルフックのようなルートインスタンスと同様のオプションが利用可能です。

# コンポーネントの再利用

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

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
1
2
3
4
5

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

# コンポーネントの構成

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

コンポーネントツリー

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

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

const app = Vue.createApp({})

app.component('my-component-name', {
  // ... オプション ...
})
1
2
3
4
5

グローバルに登録されたコンポーネントは、アプリケーション内のどのコンポーネントのテンプレートでも使うことができます。

とりあえずコンポーネント登録についてはこれで以上ですが、このページを読み終えて十分に理解できたら、後から戻ってきてコンポーネント登録の完全なガイドを読むことをお勧めします。

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

先ほど、ブログ投稿用のコンポーネントの作成について触れました。問題は、表示する特定の投稿のタイトルや内容のようなデータを作成したコンポーネントに渡せなければそのコンポーネントは役に立たないということです。プロパティはここで役立ちます。

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

const app = Vue.createApp({})

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

app.mount('#blog-post-demo')
1
2
3
4
5
6
7
8

プロパティ属性に値が渡されると、渡されたそのコンポーネントインスタンスのプロパティになります。そのプロパティの値は、他のコンポーネントのプロパティと同じように、テンプレート内でアクセスができます。

コンポーネントは必要に応じて多くのプロパティを持つことができ、デフォルトでは任意のプロパティに任意の値を渡すことができます。

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

<div id="blog-post-demo" class="demo">
  <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>
</div>
1
2
3
4
5

しかしながら、一般的なアプリケーションではおそらく data に投稿の配列を持っています:

const App = {
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

const app = Vue.createApp(App)

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

app.mount('#blog-posts-demo')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

そしてコンポーネントをそれぞれ描画します:

<div id="blog-posts-demo">
  <blog-post
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
  ></blog-post>
</div>
1
2
3
4
5
6
7

上記では、 v-bind を用いて動的にプロパティを渡すことができると分かります。これは描画する内容が事前に分からない場合に特に便利です。

とりあえずプロパティについてはこれで以上ですが、 このページを読み終えて十分に理解できたら、後から戻ってきてプロパティの完全なガイドを読むことをお勧めします。

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

<blog-post> コンポーネントを開発する中で、いくつかの機能で親コンポーネントとの通信が必要になるかもしれません。例えば、ページの他の部分の大きさはそのままで、ブログ記事のテキストを拡大するアクセシビリティ機能を実装することを決めるかもしれません。

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

const App = {
  data() {
    return {
      posts: [
        /* ... */
      ],
      postFontSize: 1
    }
  }
}
1
2
3
4
5
6
7
8
9
10

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

<div id="blog-posts-events-demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
    ></blog-post>
  </div>
</div>
1
2
3
4
5
6
7
8
9

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

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button>
        Enlarge text
      </button>
    </div>
  `
})
1
2
3
4
5
6
7
8
9
10
11

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

<button>
  Enlarge text
</button>
1
2
3

ボタンをクリックすると、全ての投稿のテキストを拡大する必要があることを親に伝える必要があります。この問題を解決するために、コンポーネントインスタンスはカスタムイベントの仕組みを提供しています。親は、ネイティブ DOM イベントでの場合と同様に、 v-on@ を用いて子コンポーネントのインスタンスでのイベントを購読することができます:

<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
1

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

<button @click="$emit('enlargeText')">
  Enlarge text
</button>
1
2
3

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

コンポーネントの emits オプションにより発行されたイベントを一覧することができます:

app.component('blog-post', {
  props: ['title'],
  emits: ['enlargeText']
})
1
2
3
4

これにより、コンポーネントが排出する全てのイベントをチェックし、オプションでそれらを検証することができます。

# イベントと値を発行する

イベントを特定の値と一緒に発行すると便利な場合があります。例えば、テキストをどれだけ大きく表示するかを <blog-post> コンポーネントの責務とさせたいかもしれません。そのような場合、 $emit の第二引数を使ってこの値を渡すことができます:

<button @click="$emit('enlargeText', 0.1)">
  Enlarge text
</button>
1
2
3

そして親でこのイベントを購読すると、 $event を用いて排出されたイベントの値にアクセスすることができます:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
1

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

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
1

値はそのメソッドの第一引数として渡されます:

methods: {
  onEnlargeText(enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}
1
2
3
4
5

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

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

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

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

<input :value="searchText" @input="searchText = $event.target.value" />
1

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

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>
1
2
3
4

WARNING

ここでは in-DOM テンプレートを使用しているため、 model-value をケバブケースで表記していることに注意してください。ケバブケースの属性とキャメルケースの属性に関してはDOM テンプレートの構文解析の注意点の章で詳しく解説されています。

これが実際に機能するためには、テンプレート内の <input> は以下でなければなりません:

  • value 属性を modelValue プロパティにバインドする
  • input では、 update:modelValue イベントを新しい値と共に発行する

以下のようになります:

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})
1
2
3
4
5
6
7
8
9
10

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

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

このコンポーネント内で v-model を実装するもう一つの方法は computed プロパティの機能を使ってゲッターとセッターを定義することです。 get メソッドは modelValue プロパティを返して、 set メソッドは対応するイベントを発行する必要があります。

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

とりあえずカスタムコンポーネントイベントについてはこれで以上ですが、このページを読み終えて十分に理解できたら、後から戻ってきてカスタムイベントの完全なガイドを読むことをお勧めします。

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

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

<alert-box>
  Something bad happened.
</alert-box>
1
2
3

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

これは、 Vue のカスタム <slot> 要素で達成できます:

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})
1
2
3
4
5
6
7
8

上で見た通り、コンテンツを配置したいところにプレースホルダとして <slot> を使います。それだけです。終わりです!

とりあえずスロットについてはこれで以上ですが、このページを読み終えて十分に理解できたら、後から戻ってきてスロットの完全なガイドを読むことをお勧めします。

# 動的なコンポーネント

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

上記は Vue の <component> 要素に特別な is 属性を持たせることで実現しています:

<!-- コンポーネントは currentTabComponent に変更があったときに変更されます -->
<component :is="currentTabComponent"></component>
1
2

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

  • 登録されたコンポーネントの名前、または
  • コンポーネントのオプションオブジェクト

完全なコードを試すには この例 (opens new window)、登録された名前ではなくコンポーネントのオプションオブジェクトをバインドしている例はこちらのバージョン (opens new window)を参照してください。

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

とりあえず動的なコンポーネントについてはこれで以上ですが、このページを読み終えて十分に理解できたら、後から戻ってきて動的 & 非同期コンポーネントの完全なガイドを読むことをお勧めします。

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

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

このような制限を持つ属性を含むコンポーネントを使用すると問題が発生することがあります。例えば:

<table>
  <blog-post-row></blog-post-row>
</table>
1
2
3

このカスタムコンポーネント <blog-post-row> は無効なコンテンツとして巻き取られ、最終的にレンダリングされた出力でエラーが発生します。回避策として特別な v-is ディレクティブを使うことができます:

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>
1
2
3

WARNING

v-is の値は JavaScript の式として扱われるので、コンポーネント名を引用符で囲む必要があります:

<!-- 間違い、何も出力されません-->
<tr v-is="blog-post-row"></tr>

<!-- 正解 -->
<tr v-is="'blog-post-row'"></tr>
1
2
3
4
5

また、 HTML の属性名は大文字小文字を区別しないので、ブラウザは全ての大文字を小文字として解釈します。つまり、 in-DOM テンプレートを使用している場合、キャメルケースのプロパティ名やイベントハンドラのパラメータはそれと同等のケバブケース(ハイフンで区切られた記法)を使用する必要があります:

// JavaScript ではキャメルケース

app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
1
2
3
4
5
6
7
8
<!-- HTML ではケバブケース -->

<blog-post post-title="hello!"></blog-post>
1
2
3

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

とりあえず DOM テンプレートパース時の警告についてはこれで以上です。そして実は、Vue の 本質 の最後となります。おめでとうございます! まだまだ学ぶべきことはありますが、まずは一休みして自分で Vue を実際に使って楽しいものを作ってみることをお勧めします。

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

Deployed on Netlify.
最終更新日: 2021-04-27, 15:13:06 UTC