Vue.js + TypeScript で親子コンポーネントのデータの受け渡し


SPA ログイン画面の例です。
記法は [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator)、CSS は [bulma](https://bulma.io/) を使っています。
似たようなソースは共通のコンポーネントにしたい！　
コード中にコメントしてみます。

## 親コンポーネント (login.vue)

~~~pages/login.vue
<template>
  <div>
    <!-- Email 入力 (自作の子コンポーネント) -->
    <!-- v-model="users.email"で親の users.email を双方向に書き換えられるようにしています。 -->
    <!-- その他の name= や type= (任意の名前) はカスタム属性です。親 -> 子へデータを渡します。-->
    <!-- 例えば name="email" だと、子に @Prop で name という箱を用意してやると "email" という文字列を受け取れます。 -->
    <base-input
      v-model="users.email"
      name="email"
    />
    <!-- パスワード入力 (自作の子コンポーネント) -->
    <base-input
      v-model="users.password"
      name="password"
      type="password"
    />
    <!-- ログインボタン (自作の子コンポーネント) -->
    <!-- 子の @Emit で clicked (任意の名前) を発火し、さらに private login() を発火させます。発火リレー。 -->
    <base-button
      @clicked="login"
      label="login"
      view="primary"
      validation="true"
    />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import LoginInterface from '@/interfaces/LoginInterface';

@Component
export default class Login extends Vue {
  private users: LoginInterface = { email: '', password: '' };

  // base-button の @clicked から発火されます。
  private login(): void {
    this.$stores.auth.create(this.users);
  }
}
</script>
~~~

ちなみにインターフェイスはこんな感じです。

~~~LoginInterface.ts
export default interface LoginInterface {
  email: string;
  password: string;
}
~~~

## 子コンポーネント (BaseInput.vue)

~~~components/atoms/BaseInput.vue
<template>
  <div class="field">
    <div class="control">
      <!-- ポイントは v-model:value="inputValue" -->
      <!-- private get inputValue() を通して親の値をもらい、private set inputValue(value: string) で親に値を送ります。 -->
      <!-- :placeholder はプレースホルダー名を vue-i18n で言語ファイルから取り出せるようにしています。 -->
      <!-- v-validate は plugin で定数ファイルからバリデーションルールを取り出せるようにしています。 -->
      <input
        :class="inputClass"
        :placeholder="$t(`label.${name}`)"
        v-validate="this.$config.validators[name]"
        :data-vv-as="$t(`label.${name}`)"
        :name="name"
        v-model:value="inputValue"
        :type="type"
        autofocus=""
      >
      {{ errors.first(name) }}
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class BaseInput extends Vue {
  // @Prop は、先ほど親でカスタム属性に指定した変数名にします。
  @Prop({ default: '' })
  private name!: string;
  @Prop({ default: '' })
  private value!: string;
  @Prop({ default: 'text' })
  private type!: string;

  // ゲッター (computed)
  private get inputValue(): string {
    return this.value;
  }

  // セッター
  private set inputValue(value: string) {
    // デコレーターの @Emit を使用する方法もあるのですが冗長なのでこちらで。
    this.$emit('input', value);
  }

  // class は親からカスタム属性で設定できるようにゲッターで管理してみました。
  // https://jp.vuejs.org/v2/guide/class-and-style.html#HTML-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0
  private get inputClass(): object {
    return {
      'input': true,
      'is-large': true,
    };
  }
}
</script>
~~~

## 子コンポーネント (BaseButton.vue)

~~~components/atoms/BaseButton.vue
<template>
  <div class="field">
    <!-- ポイントは @click="clicked" -->
    <!-- @Emit() private clicked() を通じて親の clicked() をコールします。 -->
    <button
      :class="buttonClass"
      @click="clicked"
      :disabled="validation && disabled"
      :name="label"
    >
      {{ $t(`label.${label}`) }}
    </button>
  </div>
</template>

<script lang="ts">
import { Component, Emit, Prop, Vue } from 'vue-property-decorator';

@Component
export default class BaseButton extends Vue {
  @Prop({ default: '' })
  private label!: string;
  @Prop({ default: '' })
  private view!: string;
  @Prop({ default: false })
  private validation!: boolean;

  // @Emit('clicked') と同義です。親と同じ function 名の場合は @Emit() で省略可能です。
  @Emit()
  private clicked(): void {}

  private get disabled(): boolean {
    // vee-validate
    return this.$validator.errors.items.length > 0;
  }

  private get buttonClass(): object {
    return {
      'button': true,
      'is-block': true,
      'is-info': true,
      'is-large': this.view === 'primary',
      'is-fullwidth': true,
    };
  }
}
</script>
~~~

## 備考

以前投稿した[「Vue.js で簡単なログイン画面 (トークン認証) を作ってみた」](https://qiita.com/sunadorinekop/items/f3486da415d3024c7ed4) を TypeScript で書き直したソースの抜粋です。