Help us understand the problem. What is going on with this article?

はじめてのvue-property-decorator (nuxtにも対応)

この記事はVueを勉強している段階からTypeScriptでクラスベースのVueアプリを作りたい!という方へ向けて例を交えながらvue-property-decoratorの機能を基本応用上級の3セクションに分けて説明していきます。

基本では、Vueでアプリを作る上で必須となる機能を、応用では用意されている便利なデコレータを、上級では普通の開発ではほぼ使わない機能について説明します。
とりあえずは基本のみ理解しておけば困ることはないでしょう。

また、最後にnuxt-property-decorator独自のデコレータも紹介しています。

2019/12/09 追記nuxt-property-decorator独自のデコレータの紹介を追加しました

動作確認バージョン

基本

コンポーネントの定義

@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-decoratorVueクラスを継承することを忘れないように気をつけてください。

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のオブジェクトを指定することができます。

以降で各種デコレータを紹介しますが、そこで定義できないcomponentsfiltersmixinsなどのプロパティは@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に併せて定義できます。

Vue.js公式ドキュメント#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というdatamethodsは定義できません。

@Provide / @Inject

Vueでは親でprovideとして定義した値をその子要素(親子でなくても良い)からinjectで参照することができます。

Vue.js公式ドキュメント#inject/provide

以下の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のサンプルコードではreadonlyprop!: 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-decoratornuxt-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>
simochee
Nuxt & TypeScript大好きマンです
https://www.lollipop.onl
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした