[Vue.js+Typescript] ログイン画面っぽいものを作ってみた

# 目的
下記についてお勉強したことの備忘録

- 親子コンポーネント間でのデータ/イベントの渡し方(Prop/Emit)
- ページ遷移（vue-router）
- Typescriptでの記載方法

※勉強しながら書いているので間違っているかもしれません
　もっと良い記述方法があればぜひコメントください

# 作ったもの
ログイン画面っぽいもの
※ヘッダ部はvuetifyインストール時のものそのまま

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268011/4e095678-9176-ac05-a241-e51ae8895758.png)

- ユーザー名テキストボックス
- パスワードボックス
    - 表示/非表示切り替え可
- ログインボタン
    - ユーザー名/パスワードが1文字以上でenable
    - ボタン押下でルーティングされてる別ページへ遷移

※ウェブサイトのログイン画面として作る場合には、未ログインで直接ページ指定された際にはログインページへ遷移させる処理とかが必要だが、今回はパス

# 事前準備

- vue-cliでプロジェクト作成済み [⇒以前書いたやつ](https://qiita.com/willow-c/items/45b4faf305b5db17b6f6)
- vue add Vuetify [⇒公式ドキュメント](https://vuetifyjs.com/ja/)

# パスワードボックスコンポーネントを作成

```vue:PasswordBox.vue
<template>
    <v-text-field
        v-model="password" <!-- 変数passwordに対して値のバインドと変更通知を受け取れるようにする -->
        v-bind:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'" <!-- showPassword:booleanによって表示/非表示アイコンを切り替え -->
        v-bind:type="showPassword ? 'text' : 'password'" <!-- showPassword:booleanによってテキスト型/パスワード型を切り替える -->
        label="パスワード" <!-- パスワードボックスに表示するラベル -->
        outlined <!-- パスワードボックスの見た目 -->
        @click:append="showPassword = !showPassword" <!-- クリック時にshowPasswordをトグルする -->
    ></v-text-field>
</template>

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

@Component({
  components: {},
})

export default class PasswordBox extends Vue {
    private showPassword: boolean = false;
    private password: string = '';

    @Watch('password') // passwordが変更された時に呼び出される
    private onPasswordChanged(password: string): void {
        this.onPasswordChangedEvent(password);
    }

    @Emit('onPasswordChangedEvent') // 親コンポーネントに対してパスワードボックス内の値が変わったよイベントを通知する。ここで指定したイベント名で受け取る
    private onPasswordChangedEvent(password: string): void {}
}
</script>
```

# ボタンコンポーネントを作成
※Vuetifyのコンポーネントをそのまま使用しているので独立させる必要はなかったが、Propを使ってみたかったので分けた

```vue:NormalButton.vue
<template>
    <v-btn
        v-bind:disabled="!canClick" <!-- 親から指定されるEnable/Disableを変更するためのProp -->
        @click="onButtonClicked" <!-- ボタンクリック時に呼ばれる関数を指定 -->
    >{{content}}</v-btn> <!-- 親から指定されるボタンのラベル -->
</template>

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

@Component({
  components: {},
})

export default class NormalButton extends Vue {
    @Prop({default: false}) // 親コンポーネントからボタンのEnable/Disableを変更するための変数
    private canClick!: boolean;

    @Prop({default: ''}) // 親コンポーネントからボタンのラベルを指定するための変数。今回はログインボタンだけなので本来は不要
    private content!: string;

    @Emit('onButtonClickedEvent') // 親コンポーネントへのイベント通知。ここで指定した名称で通知される。
    private onButtonClicked(): void {} // 上のTemplateで指定した関数
}
</script>
```

# LoginページのViewを作成

```vue:Login.vue
<template>
  <v-form>
    <v-text-field
        v-model="username"
        label="ユーザー名"
        outlined
    ></v-text-field>
    <PasswordBox
        @onPasswordChangedEvent="onPasswordChanged" <!-- 子コンポーネントからイベントを受け取り、onPasswordChanged関数を呼ぶ -->
    ></PasswordBox>
    <NormalButton
        @onButtonClickedEvent="onButtonClicked" <!-- 子コンポーネントからイベントを受け取り、onButtonClicked関数を呼ぶ -->
        v-bind:canClick="canClick"  <!-- 子コンポーネントのEnable/Disableをtsから変更するためにBind -->
        content="ログイン"  <!-- ボタンのラベルを表示するために子コンポーネントのPropに指定 -->
    ></NormalButton>
  </v-form>
</template>

<script lang="ts">
import { Component, Watch, Vue } from 'vue-property-decorator';
import PasswordBox from '../components/PasswordBox.vue'; // 先に作成したコンポーネントをimport
import NormalButton from '../components/NormalButton.vue'; // 先に作成したコンポーネントをimport
import router from '../router'; // ログインボタン押下時にページ遷移させるためにimport

@Component({
  components: {
      PasswordBox, // 先に作成したパスワードボックスコンポーネントを使用する宣言
      NormalButton, // 先に作成したボタンコンポーネントを使用する宣言
  },
})

export default class Login extends Vue {
    private canClick: boolean = false;
    private username: string = '';
    private password: string = '';

    @Watch('username') // usernameのvalue変更時に呼ばれる
    private onUserNameChanged(username: string): void {
        this.username = username;
        this.changeLoginButtonState(); // ログインボタンのEnable/Disableを変更
    }

    private onPasswordChanged(password: string): void {
        this.password = password;
        this.changeLoginButtonState(); // ログインボタンのEnable/Disableを変更
    }

    private onButtonClicked(): void {
        router.push('/home'); // ログインボタン押下時に/homeへ遷移させる
        // 本来はここでサーバと通信して認証可否を判断するはず
    }

    private changeLoginButtonState(): void {
        this.canClick = this.IsTextInputed(this.username) && this.IsTextInputed(this.password); // 
    }

    private IsTextInputed(text: string): boolean {
        return text.length > 0 ? true : false; // 引数の文字列が1文字以上かどうか
    }
}
</script>
```

# App.vueを変更

```vue:App.vue
<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title class="headline text-uppercase">
        <span>Vuetify</span>
        <span class="font-weight-light">MATERIAL DESIGN</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn
        text
        href="https://github.com/vuetifyjs/vuetify/releases/latest"
        target="_blank"
      >
        <span class="mr-2">Latest Release</span>
      </v-btn>
    </v-app-bar>

    <v-content>
      <router-view/> <!-- 確か変更したのはここだけ -->
    </v-content>
  </v-app>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'App',
  components: {
  },
  data: () => ({
    //
  }),
});
</script>
```

# router.tsを変更

```typescript:router.ts
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export default new Router({
  routes: [
    {
      // 最初にログインページを表示するので、ルートを変更
      path: '/',
      name: 'login',
      component: () => import('./views/Login.vue'),
    },
    {
      // 今回はログインボタン押下時に/homeへ遷移させる
      path: '/Home',
      name: 'home',
      component: () => import('./views/Home.vue'),
    },
  ],
});
```

# 終わりに
Emit/Propを使った親子コンポーネント間でのイベント/値のやり取りを書いてみました。
親から子の持つ値を見るために、子の変更イベントを受け取って親の変数に保持しているが、これで正しいのかがわかってません。。。
親から子を見る場合について、もっと良いやり方があればコメントください。