# コンポーネントの基本

# 基本例

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

See the Pen Component basics by Vue (@Vue) on CodePen.

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

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

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

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

See the Pen Component basics: reusing components by Vue (@Vue) on CodePen.

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

# コンポーネントの構成

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

コンポーネントツリー

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

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

const app = Vue.createApp({})

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

グローバルに登録されたコンポーネントはその後作成された app インスタンスのテンプレートで使用することができます。さらに、ルートインスタンスのコンポーネントツリーの全てのサブコンポーネント内でも使用することが出来ます。

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

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

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

プロパティはコンポーネントに登録できるカスタム属性です。値がプロパティ属性に渡されると、そのコンポーネントインスタンスのプロパティになります。先ほどのブログ投稿用のコンポーネントにタイトルを渡すためには、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

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

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

<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

See the Pen Component basics: passing props by Vue (@Vue) on CodePen.

しかしながら、一般的なアプリケーションではおそらく 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

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

See the Pen Component basics: emitting events by Vue (@Vue) on CodePen.

コンポーネントの 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'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})
1
2
3
4
5
6
7
8
9

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

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

カスタムコンポーネント内で v-model を使うもう一つの方法は computed プロパティを利用してゲッターとセッターを定義することです。

以下の例では、computed プロパティを用いて custom-input コンポーネントをリファクタリングします。

注意して欲しいのは、 get メソッドは modelValue 属性を返し、バインディングに使用しているプロパティがどれであるかに関わらず、 set メソッドはそのプロパティに対応する $emit を送出しなければならないということです。

app.component('custom-input', {
  props: ['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

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

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

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

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

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

See the Pen Component basics: slots by Vue (@Vue) on CodePen.

幸いにも、この作業は 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

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

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

# 動的なコンポーネント

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

See the Pen Component basics: dynamic components by Vue (@Vue) on CodePen.

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

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

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

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

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

この属性は通常の HTML 要素で使用することができますが、それらはコンポーネントとして扱われ、すべての属性は DOM 属性としてバインドされることを覚えておいてください。 value のようないくつかのプロパティが期待通りに動作するためには、 .prop 修飾子を用いてバインドする必要があります。

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

# 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.
最終更新日: 3/21/2021, 12:21:43 AM