コンポーネントシステム

コンポーネントの使用

Vue.js では Web Components と類似した概念を持つ再利用可能なコンポーネントとして、polyfill 無しで、拡張された Vue サブクラスを扱うことができます。コンポーネントを作るためには、 Vue.extend() を用いて Vue のサブクラスコンストラクタを生成します:

1
2
3
4
// 再利用可能なコンストラクタを取得するために Vue を拡張します
var MyComponent = Vue.extend({
template: '<p>A custom component!</p>'
})

Vue のコンストラクタに渡すことができるほとんどのオプションは Vue.extend() で利用可能です。しかし、 datael は例外ケースです。各 Vue インスタンスは $data$el をそれぞれが持つべきであるため、 Vue.extend() に渡され、コンストラクタを通じて作られる全てのインスタンスを横断して共有されることは好ましくありません。したがって、コンポーネントを定義する際にデフォルトの dataelement を初期化したい場合は、代わりに関数を渡しましょう:

1
2
3
4
5
6
7
var ComponentWithDefaultData = Vue.extend({
data: function () {
return {
title: 'Hello!'
}
}
})

次に、 Vue.component() を使ってコンストラクタを登録しましょう:

1
2
// my-component という id でコンストラクタを登録する
Vue.component('my-component', MyComponent)

より物事を簡単にするために、コンストラクタの代わりにオプションのオブジェクトを直接渡すこともできます。Vue.component() はオブジェクトを受け取った場合、暗黙的に Vue.extend() を呼び出します:

1
2
3
4
5
// Note: この関数はグローバルな Vue を返し、
// 登録されたコンストラクタを返すものではありません。
Vue.component('my-component', {
template: '<p>A custom component!</p>'
})

また、登録したコンポーネントを親インスタンスのテンプレート内で使用することもできます(ルートの Vue インスタンスを初期化する前にそのコンポーネントが登録されていることを確認してください):

1
2
<!-- 親テンプレートの内部 -->
<my-component></my-component>

レンダリング内容:

1
<p>A custom component!</p>

毎回グローバルなコンポーネントを登録する必要はありません。components オプションでそれを渡すことによって、別のコンポーネントへのコンポーネントの可用性とその子孫を制限することができます (このカプセル化は、このようなディレクティブやフィルタなどのその他のアセットに適用されます):

1
2
3
4
5
6
7
var Parent = Vue.extend({
components: {
child: {
// 子は親と親の子孫コンポーネントだけ利用できる
}
}
})

Vue.extend()Vue.component() の違いを理解することは重要です。Vue 自身はコンストラクタであるため、Vue.extend()クラス継承メソッドです。そのタスクは Vue のサブクラスを生成して、そのコンストラクタを返すものです。一方、 Vue.component()アセット登録メソッドであり、Vue.directive()Vue.filter() と類似しています。そのタスクは与えられたコンストラクタに文字列のIDを関連付けて、 Vue.js がそれをテンプレートの中で利用できるようにするものです。直接 Vue.component() にオプションを渡した時は、内部的に Vue.extend() が呼ばれます。

Vue.js はコンポーネントの使い方として二つの異なる API スタイルをサポートしています: コンストラクタベースの命令的な API とテンプレートベースの API です。もし混同してしまう場合は、image エレメントを new Image() で作るか、 <img> タグで作るかということを考えてみてください。どちらもそれ自体で有効的であり、Vue.js は最大限の柔軟性のためにどちらの方式も提供しています。

table 要素は、要素がその内部に表示できるものに制限があるため、カスタム要素が押し上げられてしまい正しくレンダリングされません。これらのケースではコンポーネントディレクティブシンタックスを使うことができます: <tr v-component="my-component"></tr>

データの流れ

Props による伝達

デフォルトでは、コンポーネントは隔離されたスコープ (isolated scope) を持ちます。これが意味するところは、子コンポーネントのテンプレートの中で親データの参照ができないということです。データを隔離されたスコープで子コンポーネントに渡すためには、props を利用する必要があります。

“prop” は、親コンポーネントから受信されることを期待されるコンポーネントデータ上のフィールドです。子コンポーネントは、props オプションを利用して受信することを期待するために、明示的に宣言する必要があります:

1
2
3
4
5
6
7
Vue.component('child', {
// props を宣言
props: ['msg'],
// prop は内部テンプレートで利用でき、
// そして `this.msg` として設定される
template: '<span>{{msg}}</span>'
})

そのとき、以下のようにデータを渡すことができます:

1
<child msg="hello!"></child>

結果:

キャメルケース vs ハイフン付き

HTML の属性は大文字と小文字を区別しません。キャメルケースされた prop 名を属性として使用するとき、それらハイフン付き相当語句として使用する必要があります:

1
2
3
4
Vue.component('child', {
props: ['myMessage'],
template: '<span>{{myMessage}}</span>'
})
1
2
<!-- 重要: ハイフン付きの名前を使用! -->
<child my-message="hello!"></child>

動的な Props

親から動的なデータを受け取ることができます。例えば:

1
2
3
4
5
<div>
<input v-model="parentMsg">
<br>
<child my-message="{{parentMsg}}"></child>
</div>

結果:


prop として $data を公開することも可能です。渡される値は、オブジェクトでなければならず、コンポーネントをデフォルト $data に置き換えます。

Props のバインディングタイプ

デフォルトで、全ての props は子プロパティと親プロパティとの間で one way down バインディングです。親プロパティが更新するとき子と同期されますが、その逆はありません。このデフォルトは、子コンポーネントが誤ってアプリのデータフローが推理しづらい親の状態の変更しないように防ぐためです。しかしながら、明示的に two-way または one-time バインディングを強いることも可能です:

シンタックスの比較:

1
2
3
4
5
6
<!-- デフォルトは one-way-down バインディング -->
<child msg="{{parentMsg}}"></child>
<!-- 明示的な two-way バインディング -->
<child msg="{{@ parentMsg}}"></child>
<!-- 明示的な one-time バインディング -->
<child msg="{{* parentMsg}}"></child>

two-way バインディングは子の msg プロパティの変更を親の parentMsg プロパティに戻して同期します。one-time バインディングは、一度セットアップし、親と子との間では、先の変更は同期しません。

もし、渡される prop がオブジェクトまたは配列ならば、それは参照で渡されることに注意してください。オブジェクトの変更または配列は、使用しているバインディングのタイプに関係なく、子の内部それ自身は、親の状態に影響を与えます。

Prop 仕様

コンポーネントは受け取る props に対する必要条件を指定することができます。これは他の人に使用されるために目的とされたコンポーネントを編集するときに便利で、これらの prop 検証要件は本質的にはコンポーネントの API を構成するものとして、ユーザーがコンポーネントを正しく使用しているということを保証します。文字列として定義している props の代わりに、検証要件を含んだオブジェクトを使用できます:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.component('example', {
props: {
// 基本な型チェック (`null` はどんな型でも受け付ける)
onSomeEvent: Function,
// 存在チェック
requiredProp: {
type: String,
required: true
},
// デフォルト値
propWithDefault: {
type: Number,
default: 100
},
// オブジェクト/配列のデフォルトはファクトリ関数から返されるべきです
propWithObjectDefault: {
type: Object,
default: function () {
return { msg: 'hello' }
}
},
// two-way prop は、もしバインディングの型が一致しない場合は警告を投げます
twoWayProp: {
twoWay: true
},
// カスタムバリデータ関数
greaterThanTen: {
validator: function (value) {
return value > 10
}
}
}
})

type は次のネイティブなコンストラクタのいずれかになります:

加えて、type はカスタムコンストラクタ、そして assertion は instanceof チェック もできます。

prop 検証が失敗するとき、Vue は値を子コンポーネントへのセットを拒否し、そしてもし開発ビルドを使用している場合は警告します。

Props としてのコールバックの伝達

メソッドや、子コンポーネントへのコールバックなどのステートメントを渡すのも可能です。これは宣言、切り離された親子間のコミュニケーションを可能にします:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('parent', {
// ...
methods: {
onChildLoaded: function (msg) {
console.log(msg)
}
}
})

Vue.component('child', {
// ...
props: ['onLoad'],
ready: function () {
this.onLoad('message from child!')
}
})
1
2
<!-- 親のテンプレート -->
<child on-load="{{onChildLoaded}}"></child>

親スコープの継承

もし必要な場合は inherit: true オプションを使用して子コンポーネントに対して、親の全てのプロパティをプロトタイプ継承させることができます:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var parent = new Vue({
data: {
a: 1
}
})
// $addChild() はインスタンスメソッドで
// プログラムで子インスタンスを生成することができます
var child = parent.$addChild({
inherit: true,
data: {
b: 2
}
})
console.log(child.a) // -> 1
console.log(child.b) // -> 2
parent.a = 3
console.log(child.a) // -> 3

注意点: Vue インスタンスにおける各 data プロパティは getter / setter であるため、child.a = 2 とセットすることは、親のプロパティをコピーして子に新規プロパティを作成する代わりに、 parent.a を変更します:

1
2
3
child.a = 4
console.log(parent.a) // -> 4
console.log(child.hasOwnProperty('a')) // -> false

スコープに関する注釈

コンポーネントが親テンプレートの中で使用される時、e.g.:

1
2
<!-- 親テンプレート -->
<my-component v-show="active" v-on="click:onClick"></my-component>

このディレクティブ (v-showv-on) は親のスコープでコンパイルされます。そのため、 active という値と onClick は親で解決されます。子テンプレート内のいかなるディレクティブや挿入句は子のスコープでコンパイルされます。これによって、親と子のコンポーネント間のクリーンな住み分けが実現できます。

詳細についてはコンポーネントのスコープを読んでください。

コンポーネントライフサイクル

全てのコンポーネントや Vue インスタンスは自身のライフサイクルを持ちます: created、compiled、attached、detached、と最後に destroyed です。それぞれのキーとなるタイミングでインスタンスは対応したイベントを emit します。また、インスタンスの生成やコンポーネント定義の際に、それぞれのイベントに反応するためのライフサイクル hook 関数を渡すことができます。例えば:

1
2
3
4
5
var MyComponent = Vue.extend({
created: function () {
console.log('An instance of MyComponent has been created!')
}
})

ライフサイクルで利用可能な API リファレンスを確認してください。

動的コンポーネント

予約された component 要素を使って、”ページをスワップ” を成し遂げるためにコンポーネントを動的に切り替える仕組みがあります:

1
2
3
4
5
6
7
8
9
10
11
new Vue({
el: 'body',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
1
2
3
<component is="{{currentView}}">
<!-- vm.currentview が変更されると、中身が変更されます! -->
</component>

状態を保持したりや再レンダリングを避けたりするために、もし切り替えられたコンポーネントを活性化された状態で保持したい場合は、ディレクティブのパラメータ keep-alive を追加することができます:

1
2
3
<component is="{{currentView}}" keep-alive>
<!-- 非活性になったコンポーネントをキャッシュします! -->
</component>

トランジション操作

2つの追加の param 属性により、コンポーネントがレンダリングまたはトランジションされるべきかの高度な操作が可能になります。

wait-for

DOM と切り替えられる前に、挿入される子コンポーネントを待つためのイベント名です。トランジションの開始そして空のコンテンツ表示を回避する前に非同期なデータのロードを待つことが可能になります。

この属性は、静的そして動的コンポーネント上の両方で使用できます。動的コンポーネントでは、全てのコンポーネントが潜在的に待機イベントを $emit する必要があるためにレンダリングされ、それ以外の場合は、それらは挿入されることはないことに注意してください。

例:

1
2
3
4
5
<!-- 静的 -->
<my-component wait-for="data-loaded"></my-component>

<!-- 動的 -->
<component is="{{view}}" wait-for="data-loaded"></component>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// コンポーネントの定義
{
// compiled フックの中で非同期にデータを取得してイベントを発火します。
// 例として jQuery を使っています。
compiled: function () {
var self = this
$.ajax({
// ...
success: function (data) {
self.$data = data
self.$emit('data-loaded')
}
})
}
}

transition-mode

transition-mode パラメータ属性はどうやって2つの動的コンポーネント間でトランジションが実行されるべきかどうか指定できます。

デフォルトでは、入ってくるコンポーネントと出て行くコンポーネントのトランジションが同時に起こります。この属性によって、2つの他のモードを設定することができます:

1
2
3
4
5
<!-- 先にフェードアウトし, その後フェードインします -->
<component is="{{view}}"
v-transition="fade"
transition-mode="out-in">

</component>

リストとコンポーネント

オブジェクトの配列に対して、コンポーネントと v-repeat を併用することができます。その場合、配列の中にあるそれぞれのオブジェクトに対して、そのオブジェクトを $data として、また、指定されたコンポーネントをコンストラクタとして扱う子コンポーネントが生成されます。

1
2
3
<ul id="list-example">
<user-profile v-repeat="users"></user-profile>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var parent2 = new Vue({
el: '#list-example',
data: {
users: [
{
name: 'Chuck Norris',
email: 'chuck@norris.com'
},
{
name: 'Bruce Lee',
email: 'bruce@lee.com'
}
]
},
components: {
'user-profile': {
template: '<li>{{name}} {{email}}</li>'
}
}
})

結果:

エイリアスによるコンポーネントの反復処理

エイリアスシンタックスはコンポーネントを使用しているときも動作し、繰り返されるデータは、キーとしてエイリアスを使用するコンポーネントのプロパティとして設定されます:

1
2
3
4
<ul id="list-example">
<!-- データは `this.user` として内部コンポーネントで利用できます -->
<user-profile v-repeat="user in users"></user-profile>
</ul>

v-repeat で一度コンポーネントを利用すると、同じスコーピングルールは、コンポーネントコンテナ要素上の他のディレクティブに適用されることに注意してください。結果として、親テンプレートの $index にアクセスすることはできません。コンポーネントの独自テンプレート内部だけで利用できるようになります。

別な方法としては、中間スコープを作るために <template> ブロックを繰り返し使用することができますが、ほとんどの場合は、コンポーネント内部の $index を使用することをお勧めします。

子の参照

時々、JavaScript でネストした子コンポーネントへのアクセスが必要になる場合があります。それを実現するためには v-ref を用いて子コンポーネントに対して参照 ID を割り当てる必要があります。例えば:

1
2
3
<div id="parent">
<user-profile v-ref="profile"></user-profile>
</div>
1
2
3
var parent = new Vue({ el: '#parent' })
// 子コンポーネントへのアクセス
var child = parent.$.profile

v-refv-repeat と共に使用された時は、得られる値はそのデータの配列をミラーリングした子コンポーネントが格納されている配列になります。

イベントシステム

Vue インスタンスの子や親に直接アクセスすることもできますが、コンポーネント間通信のためのビルトインのイベントシステムを使用した方が便利です。また、この仕組みによってコードの依存性を減らし、メンテナンスし易くなります。一度親子の関係が確立されれば、それぞれのコンポーネントのイベントを使ったイベントのディスパッチやトリガが可能になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var parent = new Vue({
template: '<div><child></child></div>',
created: function () {
this.$on('child-created', function (child) {
console.log('new child created: ')
console.log(child)
})
},
components: {
child: {
created: function () {
this.$dispatch('child-created', this)
}
}
}
}).$mount()

プライベートアセット

時々、ディレクティブ、フィルタ、子コンポーネントなどのアセットをコンポーネントが使う必要がでてきます。しかし、コンポーネント自体を他のところでも再利用できるように、カプセル化されたそれらのアセットを保持したいと思うかもしれません。それはインスタンス化時にプライベートアセットのオプションを使用することによって実現できます。プライベートアセットは所有者であるコンポーネント、それから継承するコンポーネント、そして view 階層 にある子コンポーネントのインスタンスからのみアクセス可能なものになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 全5種類のアセット
var MyComponent = Vue.extend({
directives: {
// id : グローバルメソッドと同じ定義のペア
'private-directive': function () {
// ...
}
},
filters: {
// ...
},
components: {
// ...
},
partials: {
// ...
},
transitions: {
// ...
}
})

Vue.config.strict = true を設定することによって、子コンポーネントが親コンポーネントのプライベートアセットへのアクセスするのを禁止できます。

別の方法として、グローバルなアセットの登録メソッドと類似したチェーンする API を使用して、プライベートアセットを既存のコンポーネントのコンストラクタに追加することもできます:

1
2
3
4
5
MyComponent
.directive('...', {})
.filter('...', function () {})
.component('...', {})
// ...

アセットの命名規則

コンポーネントやディレクティブのようなあるアセットは、HTML 属性または HTML カスタムタグの形でテンプレートに表示されます。HTML 属性名とタグ名は大文字と小文字を区別しないため、私達はしばしばキャメルケースの代わりにダッシュケースを使用して私達のアセットに名前をつける必要があります。0.12.11 以降では、キャメルケース (camelCase) またはパスカルケース (PascalCase) を使用してアセットに名前をつけるのをサポートし、テンプレートでダッシュケースでそれらを使用します。

1
2
3
4
5
// コンポーネント定義
components: {
// キャメルケースを使用して登録
myComponent: { /*... */ }
}
1
2
<!-- テンプレートではダッシュケースを使用 -->
<my-component></my-component>

これは ES6 object literal shorthand でうまく動作します:

1
2
3
4
5
6
7
8
9
10
11
// PascalCase
import TextBox from './components/text-box';
import DropdownMenu from './components/dropdown-menu';

export default {
components: {
// <text-box> そして <dropdown-menu> としてテンプレートで使用
TextBox
DropdownMenu
}
}

コンテンツ挿入

再利用可能なコンポーネントを作るときに、コンポーネントの一部ではないホストしている要素 (Angular の “transclusion” の概念に類似したものです。) の中にある元のコンテンツへのアクセスや再利用がしばしば必要です。Vue.js は現在の Web Components の仕様ドラフトと互換性のあるコンテンツ挿入の仕組みを実装しています。元のコンテンツに対する挿入ポイントとして機能する特別な <content> 要素を使用します。

重要: “transcluded” されたコンテンツは子のスコープではなく、親コンポーネントのスコープの中でコンパイルされます。

単独の挿入位置

何も属性の無い一つの <content> タグしか存在しない時は、元のコンテンツ全体が DOM の中のその位置に挿入され、置換します。元々の <content> タグの内側のものは全てフォールバックコンテンツ (fallback content) として解釈されます。フォールバックコンテンツはホストしている要素が空で挿入されるべきコンテンツがない時にだけ表示されます。

my-component のテンプレート:

1
2
3
4
<div>
<h1>This is my component!</h1>
<content>This will only be displayed if no content is inserted</content>
</div>

このコンポーネントを使用した親のマークアップ:

1
2
3
4
<my-component>
<p>This is some original content</p>
<p>This is some more original content</p>
</my-component>

レンダリング結果:

1
2
3
4
5
<div>
<h1>This is my component!</h1>
<p>This is some original content</p>
<p>This is some more original content</p>
</div>

多数の挿入位置

<content> 要素は CSS セレクタを期待する select という特殊な属性を持ちます。異なる select 属性を用いて複数の <content> の挿入位置を指定することができます。それぞれは元のコンテンツの中でそのセレクタにマッチした要素によって置換されます。

0.11.6 以降では、<content> セレクタは、ホストノードのトップレベルの子だけ一致できます。これは Shadow DOM 仕様の振舞いを保ち、そしてネストされたテンプレートで誤って不要なノードを選択することを回避します。

例として、以下のテンプレートのような、多数のコンポーネント挿入のテンプレートを持っていると仮定:

1
2
3
4
5
<div>
<content select="p:nth-child(3)"></content>
<content select="p:nth-child(2)"></content>
<content select="p:nth-child(1)"></content>
</div>

親のマークアップ:

1
2
3
4
5
<multi-insertion">
<p>One</p>
<p>Two</p>
<p>Three</p>
</multi-insertion>

レンダリングされる結果:

1
2
3
4
5
<div>
<p>Three</p>
<p>Two</p>
<p>One</p>
</div>

コンテンツ挿入の仕組みは、元のコンテンツがどのように組み替えられ、表示されるべきか、という点に関して素晴らしい管理機能を提供します。これによってコンポーネントが非常に柔軟性と再利用性が高いものになります。

インラインテンプレート

0.11.6 では、コンポーネント向けに特別なパラメータ属性として、inline-template というパラメータが導入されます。これのパラメータが提供されるとき、コンポーネントはそれはテンプレートではなくテンプレートコンテンツとして内部コンテンツを使用します。これは、より柔軟なテンプレートオーサリングを可能にします。

1
2
3
4
<my-component inline-template>
<p>These are compiled as the component's own template</p>
<p>Not parent's transclusion content.</p>
</my-component>

非同期コンポーネント

非同期コンポーネントは 0.12.0 以降のみサポートされます。

大規模アプリケーションでは、実際に必要になったとき、サーバからコンポーネントをロードするだけの、アプリケーションを小さい塊に分割する必要があるかもしれません。それを簡単にするために、Vue.js はコンポーネント定義を非同期的に解決するファクトリ関数としてあなたのコンポーネントを定義することができます。Vue.js はコンポーネントが実際に描画が必要になったときファクトリ関数のみトリガし、そして将来の再描画のために結果をキャッシュします。例えば:

1
2
3
4
5
6
7
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})

ファクトリ関数は resolve コールバックを受け取り、その引数はサーバからあなたのコンポーネント定義を取り戻すときに呼ばれるべきです。ロードが失敗したことを示すために、reject(reason) も呼び出すことができます。ここでは setTimeout はデモとしてシンプルです。どうやってコンポーネントを取得するかどうかは完全にあなた次第です。1つ推奨されるアプローチは Webpack のコード分割機能で非同期コンポーネントを使うことです。

1
2
3
4
5
6
7
Vue.component('async-webpack-example', function (resolve) {
// この特別な require シンタックスは、
// 自動的に ajax リクエストでロードされているバンドルで、
// あなたのビルドコードを自動的に分割するために
// webpack で指示しています。
require(['./my-async-component'], resolve)
})

次: トランジション