この記事はVueを勉強している段階からTypeScriptでクラスベースのVueアプリを作りたい!という方へ向けて例を交えながらvue-property-decorator
の機能を基本と応用、上級の3セクションに分けて説明していきます。
基本では、Vueでアプリを作る上で必須となる機能を、応用では用意されている便利なデコレータを、上級では普通の開発ではほぼ使わない機能について説明します。
とりあえずは基本のみ理解しておけば困ることはないでしょう。
また、最後にnuxt-property-decorator
独自のデコレータも紹介しています。
2019/12/09 追記:nuxt-property-decorator
独自のデコレータの紹介を追加しました
動作確認バージョン
vue-property-decorator v8.4.2
-
nuxt-property-decorator v2.5.1
-
nuxt-property-decorator v2.7.2
で vuex-module-decorators が同梱されるようになりました(追記予定)
-
基本
コンポーネントの定義
@Component
は続けて定義しているクラスをVueが認識できる形式に変換しています。
以下の2つは同じ意味です。
<script>
export default {
name: 'SampleComponent'
};
</script>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {}
</script>
この時、vue-property-decorator
のVue
クラスを継承することを忘れないように気をつけてください。
Data
Dataはクラスのメンバーとして定義するだけで利用できます。
以下のサンプルでは名前と年齢をDataに持たせています。
<script>
export default {
data() {
return {
name: 'simochee',
age: 21
}
}
};
</script>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
name = 'simochee';
age = 21;
}
</script>
Dataをテンプレート内で使用するときはプレーンなVueと同じように参照できます。
<template>
<!-- simochee (21) -->
<p>{{ name }} ({{ age }})</p>
</template>
Computed
算出プロパティ(Computed)はクラスのGetterとして定義することで利用できます。
以下のサンプルではDataに定義されたスコアを3倍する算出プロパティを定義しています。
<script>
export default {
data() {
return {
score: 55
}
},
computed: {
triple() {
return this.score * 3;
}
}
};
</script>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
score = 55;
get triple() {
return this.score * 3;
}
}
</script>
Computedをテンプレート内で使用するときはプレーンなVueと同じように参照できます。
<template>
<!-- Triple score: 163! -->
<p>Triple score: {{ triple }}!</p>
</template>
メソッド
メソッドはクラスのメソッドとして定義するだけで利用することができます。
以下の例では、ボタンが押されたときにonClickButton
メソッドを呼び出しています。
<template>
<button @click="onClickButton">Click Me!</button>
</template>
このようなテンプレートがあったときのonClickButton
は以下のように定義できます。
<script>
export deafult {
methods: {
onClickButton() {
// ボタンが押されたときの処理
}
}
};
</script>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
onClickButton() {
// ボタンが押されたときの処理
}
}
</script>
Reactのようにメソッドにthis
をバインドする必要はありません。
ライフサイクルフック
ライフサイクルフックは、クラスにライフサイクルの名前でメソッドを定義するだけで利用できます。
<script>
export default {
mounted() {
// コンポーネントがマウントされたときの処理
},
beforeDestroy() {
// コンポーネントが破棄される直前の処理
}
}
</script>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
mounted() {
// コンポーネントがマウントされたときの処理
}
beforeDestroy() {
// コンポーネントが破棄される直前の処理
}
}
</script>
vue-property-decorator
ではライフサイクルフックとメソッドが同じ領域で定義されるため、ライフサイクルの名前でメソッドを定義しないように注意が必要です。
@Component
@Component
は引数としてVueのオブジェクトを指定することができます。
以降で各種デコレータを紹介しますが、そこで定義できないcomponents
やfilters
、mixins
などのプロパティは@Component
の引数として指定します。
<script>
export deafult {
components: {
AppButton,
ProductList
},
directives: {
resize
},
filters: {
dateFormat
},
mixins: [
PageMixin
]
};
</script>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component({
components: {
AppButton,
ProductList
},
directives: {
resize
},
filters: {
dateFormat
},
mixins: [
PageMixin
]
})
export default class SampleComponent extends Vue {
}
</script>
他にも、以下のドキュメントで紹介されているプロパティを指定できます。
@Prop
@Prop
は続けて定義したメンバーをprops
として使用できるようにします。
親コンポーネントからは定義したメンバー名でprops
を指定できます。
<script>
export deafult {
props: {
userName: {
type: String,
required: true
},
isVisible: {
type: Boolean,
default: false
}
}
};
</script>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
@Prop({ type: String, required: true })
userName: string;
@Prop({ type: Boolean, defualt: false })
isVisible: boolean;
}
</script>
@Prop
の引数にはprops
で指定可能なオプションがすべて指定できます。
@Watch
@Watch
は第1引数に監視したい値へのパスを、第2引数にウォッチャのオプションを指定できます。
以下のサンプルでは単一のDataとObjectのプロパティの値を監視するメソッドを定義しています。
なお、immediate: true
はコンポーネント初期化時にも処理を実行するかを指定するオプションです。
<script>
export deafult {
data() {
isLoading: false,
profile: {
name: 'simochee',
age: 21
}
},
watch: {
isLoading() {
// ローディング状態が切り替わったときの処理
},
'profile.age': {
handler: function() {
// プロフィールの年齢が変更されたときの処理
},
immediate: true
}
}
};
</script>
<script lang="ts">
import { Component, Watch, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
isLoading = false;
profile = {
name: 'simochee',
age: 21
};
@Watch('isLoading')
onChangeLoadingStatus() {
// ローディング状態が切り替わったときの処理
}
@Watch('profile.age', { immediate: true })
onChangeProfileAge() {
// プロフィールの年齢が変更されたときの処理
}
}
</script>
Vueの仕様からも分かる通り、@Watch
は同じパスに対して複数回指定することはできません。
複数回指定された場合は後勝ちとなるため、先に定義したウォッチャは実行されません。
応用
@PropSync
Vue.jsではprops
を指定する際に.sync
修飾子を付与することで、子コンポーネントから親コンポーネントの値を変更することができるようになります。
仕組みとしては、@update:<Prop名>
というイベントを受け取ったらDataに代入するという処理を暗黙的に行っています。
// 親コンポーネント
<template>
<!-- 以下の2つは同じ意味 -->
<ChildComponent
:childValue.sync="value"
/>
<ChildComponent
:childValue="value"
@update:childValue="value = $event"
/>
</template>
このとき、子コンポーネント以降で.sync
プロパティをバケツリレーする際に便利なのが@PropSync
デコレータです。
このデコレータを使用しない場合は、以下のように書かなければいけませんでした。
// 子コンポーネント
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
@Prop({ type: String })
childValue: string;
// value を変更したいときに呼び出す
updateValue(newValue) {
this.$emit('update:childValue', newValue);
}
}
</script>
これが、@PropSync
で定義した場合はメンバーへ値を代入するだけで同等の処理を実現することができます。
<script lang="ts">
import { Component, PropSync, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
@PropSync({ type: String })
childValue: string;
// value を変更したいときに呼び出す
updateValue(newValue) {
this.childValue = newValue
}
}
</script>
代入するだけで値の変更を通知できるため、さらに.sync
で孫コンポーネントへ値を渡す場合なども、とてもシンプルに書くことができます。
<template>
<SunComponent
:sunValue.sync="childValue"
/>
</template>
@Emit
Vueではコンポーネント間での値を双方向にやり取りすることができます。
親から子への値渡しはPropを指定することで行い、子から親への値渡しはイベントを通知することによってアクションや値を受け渡せるようになっています。
このとき、子から親へ値を渡すときのイベントを発行するのが$emit
メソッドです。
以下のサンプルでは子コンポーネントで送信処理が行われたことsubmit
イベントとして親コンポーネントへ通知し、受け取った値を元に親コンポーネント側でリクエストを送信しています。
// 子コンポーネント
<template>
<form @submit="onSubmit">
<input v-model="value">
<button type="submit">Submit</button>
</submit>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class ChildComponent extends Vue {
value = '';
// 値を送信する処理
onSubmit() {
this.$emit('submit', this.value);
}
}
</script>
// 親コンポーネント
<template>
<ChildComponent
@submit="onReceiveSubmit"
/>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ChildComponent from './ChildComponent.vue';
@Component({
components: {
ChildComponent
}
})
export default class ParentComponent extends Vue {
async onReceiveSubmit(newValue: string) {
// $emitでの第2引数を受け取ることができる
await this.$request.post(newValue);
}
}
</script>
@Emit
では$emit
の処理を事前に定義することができます。
イベント名は@Emit
の第1引数に明示的に指定するか、省略した場合は続けて定義しているメソッド名が利用されます。
また、メソッドで値を返却すれば$emit
でその値を送ることもできます。
上記のサンプルの子コンポーネントを@Emit
で書き換えると以下のようになります。
<template>
<form @submit="submit">
<input v-model="value">
<button type="submit">Submit</button>
</submit>
</template>
<script lang="ts">
import { Component, Emit, Vue } from 'vue-property-decorator';
@Component
export default class ChildComponent extends Vue {
value = '';
// 値を送信する処理
// イベント名を指定しない場合でも () は省略できない
@Emit()
submit() {
return this.value;
}
}
</script>
この他に、@Emit
を非同期メソッドへ設定することもできます。
なお、キャメルケースでイベント名、メソッド名を指定した場合、親コンポーネントで受け取る際にはケバブケースへ変換されますので注意が必要です。
// 子コンポーネント
@Emit()
submitForm() {}
// 親コンポーネント
<ChildComponent
@submit-form="onSubmit"
@submitForm="onSubmit" // 発火しない
/>
親コンポーネントでケバブケースのイベントを受け取りたい場合は、デコレータの引数にイベント名を指定する必要があります。
// 子コンポーネント
@Emit('submitForm')
submitForm() {}
@Ref
@Ref
は$refs
で参照できる要素、コンポーネントの型を定義します。
事前に定義しておくことでタイポや変更へ対応しやすくなります。
<template>
<ChildComponent ref="childComponent" />
<button ref="submitButton">Submit</button>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ChildComponent from '@/component/ChildComponent.vue';
@Component({
components: {
ChildComponent
}
});
export default class SampleComponent extends Vue {
@Ref() childComponent: ChildComponent;
@Ref() submitButton: HTMLButtonElement;
mounted() {
// 子コンポーネントのメソッドを実行する
this.childComponent.updateValue();
// ボタンをフォーカスする
this.submitButton.focus();
}
}
</script>
上級
以降は上級者向けのため、あまり詳しく記載しません。必要であれば公式ドキュメントなどを参照してください。
@Model
VueのModelを定義します。VueではModelを指定する際にPropも定義しそちらに型情報などを記載しなければいけませんでしたが、デコレータでは@Model
に併せて定義できます。
以下の2つは同じ意味です。
<script>
export deafult {
props: {
value: {
type: String,
required: true
}
},
model: {
prop: 'value',
event: 'update'
}
};
</script>
<script lang="ts">
import { Component, Model, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
@Model('update', { type: String, required: true })
value: string;
}
</script>
このとき、暗黙的にvalue
がPropとして定義されるためvalue
というdata
やmethods
は定義できません。
@Provide
/ @Inject
Vueでは親でprovide
として定義した値をその子要素(親子でなくても良い)からinject
で参照することができます。
以下の2つは同じ意味です。
<!-- Parent.vue -->
<script>
export deafult {
provide: {
foo: 'foo',
bar: 'bar'
}
};
</script>
<!-- Child.vue -->
<script>
export deafult {
inject: {
foo: 'foo',
bar: 'bar',
optional: { from: 'optional', default: 'default' }
}
};
</script>
<!-- Parent.vue -->
<script lang="ts">
import { Component, Provide, Vue } from 'vue-property-decorator';
@Component
export default class ParentComponent extends Vue {
@Provide() foo = 'foo';
@Provide('bar') baz = 'bar';
}
</script>
<!-- Child.vue -->
<script lang="ts">
import { Component, Inject, Vue } from 'vue-property-decorator';
@Component
export default class ChildComponent extends Vue {
@Inject() foo: string;
@Inject('bar') bar: string;
@Inject({ from: 'optional', default: 'default' }) optional: string;
@Inject(symbol) baz: string;
}
</script>
@ProvideReactive
/ @ProvideInject
@Provide
/ @Inject
の拡張です。親コンポーネントから@ProvideReactive
として提供された値の変更を子コンポーネントでキャッチできるようになります。
<!-- Parent.vue -->
<script lang="ts">
import { Component, ProvideReactive, Vue } from 'vue-property-decorator';
@Component
export default class ParentComponent extends Vue {
@ProvideReactive() foo = 'foo';
}
</script>
<!-- Child.vue -->
<script lang="ts">
import { Component, InjectReactive, Vue } from 'vue-property-decorator';
@Component
export default class ChildComponent extends Vue {
@InjectReactive() foo: string;
}
</script>
readonly
と!
、?
について
vue-property-decorator
のサンプルコードではreadonly
やprop!: String
のような!
が登場しています。
これらはいずれもTypeScriptの機能です。
readonly
修飾子は、メンバー変数を読み込み専用とするためのものです。
VueではProp、Modelへの直接の代入はエラーとなります。
誤った代入を事前に防ぐために@Prop
および@Model
で定義したメンバー変数へはreadonly
修飾子を付与することをおすすめします。
<script lang="ts">
import { Component, Prop, PropSync, Watch, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
@Prop({ type: String }) readonly name: string;
@Model('update', { type: Object }) readonly profile: IProfile;
@PropSync({ type: String }) value: string; // 代入可能
}
</script>
また、デコレータで定義されたすべてのメンバー変数についている!
は NonNullAssertionオペレータ と呼ばれる機能です。
!
がついたプロパティがNull/Undefinedではないことを明示します。
ただし、!
はrequired: true
またはデフォルト値が設定されているプロパティにのみ指定することをおすすめします。
逆に、必須項目でなくデフォルト値も指定されていない場合は、?
を指定することをおすすめします。
この?
はプロパティが任意項目であり、undefined
の可能性があることを明示します。
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class SampleComponent extends Vue {
@Prop({ type: String, required: true })
readonly name!: string;
@Prop({ type: Array, default: () => [] })
readonly items!: string[];
@Prop({ type: Object });
readonly profile?: IProfile;
mounted() {
// undefinedの可能性のあるオブジェクトのプロパティを
// 参照しようとしたのでタイプエラーになる
profile.age;
}
}
</script>
より強固に型安全にしたい場合はこれらに気をつけて開発するようにしてみてください。
nuxt-property-decorator
nuxt-property-decorator
はnuxt-community
がメンテナンスを行っているvue-property-decorator
のラッパーです。
vue-property-decorator
の機能に加え、Nuxt独自のライフサイクルメソッドやプロパティの対応などが追加されています。
また、nuxt-property-decorator
にはvue-property-decorator
にない独自のデコレータが実装されています。
@On
/ @Off
/ @Once
Vueではthis.$emit
したイベントをthis.$on
で検知することができます。
以下のソースは、mounted
時にfoo
イベントを待機させる$on
/$once
を仕込み、onClick
メソッド内でfoo
イベントを発火させています。
onClick
メソッドが実行されると、必ずcallback
メソッドも実行され、最初の1度のみonceCallback
メソッドも実行されます。
また、clearCallback
メソッドが呼ばれるとfoo
イベントに対し設定されたコールバックが$off
によって解除され、以降は発火しなくなります。
<script>
export default {
name: 'SampleComponent',
created() {
// fooイベントを待機
this.$on('foo', this.callback);
// fooイベントを1度だけ待機
this.$once('foo', this.onceCallback);
},
methods: {
callback(name) { ... },
onceCallback(name) { ... },
onClick(user) {
this.$emit('foo', user.name);
},
clearCallback() {
this.$off('foo', this.callback);
}
},
};
</script>
これをnuxt-property-decorator
の@On
/@Off
/@Once
デコレータで書き換えると以下のようになります。
<script lang="ts">
import { Component, Emit, On, Off, Once, Vue } from 'nuxt-property-decorator';
@Component
export default class SampleComponent extends Vue {
@On('foo')
callback(name: string): void { ... }
@Once('foo')
onceCallback(name: string): void { ... }
@Off('foo', 'callback')
clearCallback(): void { }
@Emit('foo')
onClick(user: IUser): string {
return user.name;
}
}
</script>
VueのAPIと異なるのは、@Off
で指定するのがコールバック関数そのものではなく、その名前であるという点です。
上記の例ではthis.callback
メソッドのイベントリスナを解除したいので'callback'
を指定しています。
@NextTick
Vueのthis.$nextTick
メソッドは、次の処理を描画が終わってから実行するときに使用します。
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'nuxt-property-decorator';
@Component
export default class SampleComponent extends Vue {
/** コンテンツ */
content = '';
/** コンテンツの高さ */
contentHeight = 0;
/** コンテンツの要素のRef */
@Ref('content') contentElement: HTMLParagraphElement;
/**
* コンテンツの変更と高さの変更
* @param newContent 新しいコンテンツ
*/
async onChangeContent(newContent: string): Promise<void> {
this.content = newContent;
// 新しいコンテンツ反映後の高さを取るため描画完了を待機
await this.$nextTick();
this.contentHeight = this.$ref.contentElement.clientHeight;
}
}
</script>
これが、@NextTick
デコレータを使うと以下のようになります。
@NextTick
デコレータは付与したメソッドの実行が完了したあとに、引数で指定したメソッドを呼ぶようにして使用します。
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'nuxt-property-decorator';
@Component
export default class SampleComponent extends Vue {
/** コンテンツ */
content = '';
/** コンテンツの高さ */
contentHeight = 0;
/** コンテンツの要素のRef */
@Ref('content') contentElement: HTMLParagraphElement;
/**
* コンテンツの変更と高さの変更
* @param newContent 新しいコンテンツ
*/
@NextTick('updateContentHeight')
onChangeContent(newContent: string): void {
this.content = newContent;
}
/**
* コンテンツの高さを更新する
*/
updateContentHeight(): void {
this.contentHeight = this.$ref.contentElement.clientHeight;
}
}
</script>