VueQuest第2回の講義では、下記の内容について学んでいきましょう。

  • 前回の「双方向データバインディング」を利用したバリデーションの実装
  • コンポーネントの分割
  • コンポーネント間の値の受け渡し方法

特に、最後のコンポーネント間の値の受け渡し方法は、Vueの基礎的な内容にして、非常によく使われる概念なので、着実に理解していきましょう。

バリデーションの実装

今回は、下記の2パターンのバリデーション実装方法を学んでいきたいと思います。

  • ウォッチャを利用した自力でのバリデーション実装
  • Vuetifyを利用したバリデーション実装

Vuetifyを利用すれば、簡単にバリデーションを実装できるのですが、双方向データバインディングとバリデーションの仕組みを深く理解するために、自力での実装方法も一緒に確認していきましょう。

Top.vueの変更

下記を参考に、コードを書き換えて下さい。

今回は、ウォッチャ(下記のwatchの部分)という新しい概念が出てきます。

これは、特定のデータ(今回でいえばmovieUrl)を監視する役割があり、データが変更された瞬間にデータの変更を感知して、ウォッチャ内に書かれた処理を実行してくれます。

今回はmovieUrlのウォッチャにバリデーションの記述をすることで、フォームへの入力値が変わった瞬間に、ウォッチャ内のバリデーションが実行されるようにしていきましょう。

                    <v-text-field
						label="YouTube動画ID"
						v-model="movieUrl"
                        :error-messages="movieErrorMessages"
						placeholder="動画URLの【v= 】の後に続く英数字"
						style="width: 300px"
					/>
					{{ movieUrl }}
					<v-text-field
						label="コメント"
						v-model="comment"
						:rules="commentRules"
						placeholder="動画紹介のための任意のコメント"
						class="mb-5"
						style="width: 500px"
					/>
<script>
	  export default {
	    data () {
		  return {
			  movieUrl: "",
			  movieErrorMessages: [],
			  comment: "",
			  commentRules: [
                  value => !!value || '入力してください',
                  value => value.length <= 16 || '16文字以内で入力してください'
              ],
		  }
	    },
        watch: {
          movieUrl: function (value) {
            const stringValidation = /^[a-zA-Z0-9!-/:-@¥[-`{-~]+$/.test(value)
            if (!value) {
              this.movieErrorMessages = [
                '入力してください',
              ]
            } else if (value.length !== 11) { 
              this.movieErrorMessages = [
                '11文字で入力してください',
              ]
            } else if (!stringValidation) { 
              this.movieErrorMessages = [
                '半角英数記号で入力してください',
              ]
            } else {
              this.movieErrorMessages = []
            }
          }
        }
    }
</script> 

error-messagesというのは、v-text-fieldコンポーネントなどで利用可能なVuetifyの属性を示し、ここにエラーメッセージを代入することで、Vuetifyがエラーメッセージを表示してくれる仕組みです。

:error-messagesや:rulesの「:(コロン)」の意味については、後ほど下記で解説します。

まず、ウォッチャを利用した自力でのバリデーション方法について説明します。

今回は、バリデーションに引っ掛かった場合に、movieErrorMessagesにエラーメッセージを入力して、次の条件の場合にエラー文が表示されるようにしています。

  • 動画URLの入力が空欄だった場合
  • 動画URLの入力値が11文字でなかった場合
  • 動画URLの入力値が半角英数記号でなかった場合

そして、最後のelseの条件分岐によって、エラーメッセージを消去することで、上記の3つのエラー条件に当てはまらなければエラー文を表示させないようにしています。

また、Vuetifyを用いた簡単なバリデーション実装方法についても解説します。

今回は、コメントの入力項目にVuetifyのバリデーションを利用しましょう。:rulesの中身にバリデーションルールを設定すれば、簡単にバリデーションを掛けてくれるというVuetifyの便利な機能を利用しています。

下記の記述方法が少し難しいですが、それぞれ、こちらの意味を示しています。

value => !!value || '入力してください'
  • 入力値が空欄でない場合 → trueを返す
  • 入力値が空欄である場合 → 「入力してください」という文字列を返す
value => value.length <= 16 || '16文字以内で入力してください'
  • 入力値が16文字以下の場合 → trueを返す
  • 入力値が16文字以上の場合 → 「入力してください」という文字列を返す

return A || B という記法は、「Aがtrueであればtrueを返し、AがfalseであればBを返す」という意味を示します。今回の場合、returnを省略しており、functionなど関数を示す記述を省略することによって、たった1行で処理を表現しています。

rulesを使って値をVuetifyに渡す際、配列の中身として、trueを受け取ればエラーメッセージを表示せず、文字列を受け取ればその文字列をエラーメッセージとして表示してくれる処理をしてくれます。

コンポーネントを分割しよう

Vueの特徴として、再利用可能なコンポーネント(部品)をたくさん作っていけるというものがあります。

現在Top.vueのv-cardの中身として存在する「入力フォーム」を他の同じような投稿処理を行う場面で使ったりと、再利用可能な形で扱っていきたいと思います。

今回は再利用はしないのですが、再利用しなくても別コンポーネントとして切り分けておくと、コードも非常に簡潔になり見やすくなります。

では、入力フォームをTop.vueとは別のコンポーネントに分割していきましょう。

InputForm.vueの作成

下記を参考に、新しくファイルを作成下さい。

<template>
  <v-form>
    <v-text-field
      label="YouTube動画ID"
      v-model="movieUrl"
      :error-messages="movieErrorMessages"
      placeholder="動画URLの【v= 】の後に続く英数字"
      style="width: 300px"
    />
    <v-text-field
      label="コメント"
      v-model="comment"
      :rules="commentRules"
      placeholder="動画紹介のための任意のコメント"
      class="mb-5"
      style="width: 500px"
    />
  </v-form>
</template>

<script>
  export default {
    name: 'InputForm',
		data () {
			return {
				movieUrl: "",
				movieErrorMessages: [],
				comment: "",
				commentRules: [
                    value => !!value || '入力してください',
                    value => value.length <= 16 || '16文字以内で入力してください'
                ],
			}
		},
    watch: {
      movieUrl: function (value) {
        const stringValidation = /^[a-zA-Z0-9!-/:-@¥[-`{-~]+$/.test(value)
        if (!value) {
          this.movieErrorMessages = [
            '入力してください',
          ]
        } else if (value.length !== 11) { 
          this.movieErrorMessages = [
            '11文字で入力してください',
          ]
        } else if (!stringValidation) { 
          this.movieErrorMessages = [
            '半角英数記号で入力してください',
          ]
        } else {
          this.movieErrorMessages = []
        }
      }
    }
  }
</script>

Top.vueの変更

下記を参考に、コードを書き換えて下さい。

<template>
	<v-container>
		<v-card
			class="my-10"
			elevation="10"
			rounded="xl"
			width="100%"
		>
			<v-container
				class="mx-10 text-center"
			>
                <input-form />
			</v-container>
		</v-card>
	</v-container>
</template>

<script>
  import InputForm from './InputForm.vue'

  export default {
      name: 'Top',
      components: {
        InputForm,
      },
  }
</script>

分割ができたら、きちんと入力フォームが動作するか確認しておきましょう。

今回の分割で、再利用可能なコンポーネントを作ることができて、コードもとても見やすくなったかと思います。

コンポーネント間の値の受け渡し

Vueで非常に重要な要素となる、値の受け渡し方法を学んでいきましょう。値の受け渡し方については、主に下記2通りのやり方があります。

少し理解に苦しむ部分もあるかも知れませんが、是非付いてきて頂けると嬉しいです。

親→子: props を利用

親コンポーネント(今回でいえばTop.vue)から、子コンポーネント(今回でいえばInputForm.vue)へ値を受け渡す際には、propsという技術を使います。

子→親: $emit を利用

子コンポーネント(今回でいえばInputForm.vue)から、親コンポーネント(今回でいえばTop.vue)へ値を受け渡す際には、$emitという技術を使います。

InputForm.vue の変更

下記を参考に、コードを書き換えて下さい。

<template>
  <v-card
    class="my-10"
    :elevation="formElevation"
    :rounded="formRounded"
    width="100%"
  >
    <v-container
      class="px-10 text-center"
    >
      <v-form
        ref="form"
      >
        <v-text-field
          label="YouTube動画ID"
          v-model="movieUrl"
          :error-messages="movieErrorMessages"
          placeholder="動画URLの【v= 】の後に続く英数字"
          style="width: 300px"
        />
        <v-text-field
          label="コメント"
          v-model="comment"
          :rules="commentRules"
          placeholder="動画紹介のための任意のコメント"
          class="mb-5"
          style="width: 500px"
        />
        <v-btn
            color="blue"
            @click="formClick"
            type="button"
            x-large
        >
          CLICK!
        </v-btn>
      </v-form>
    </v-container>
  </v-card>
</template>

<script>
  export default {
    name: 'InputForm',

    props: {
      formRounded: {
        type: String,
        required: false,
        default: 'sm',
      },
      formElevation: {
        type: String,
        required: false,
        default: '1',
      },
    },

    data () {
      return {
        movieUrl: "",
        movieErrorMessages: [],
        comment: "",
        commentRules: [
          value => !!value || '入力してください',
          value => value.length <= 16 || '16文字以内で入力してください'
        ],
      }
    },

    watch: {
      movieUrl: function (value) {
        this.validateMovieUrl(value)
      }
    },

    methods: {
      formClick () {
        // バリデーションを強制実行
        this.validateMovieUrl(this.movieUrl)
        const validateComment = this.$refs.form.validate()
        // フォームにエラーがなければ、動画保存処理を実行
        if (this.movieErrorMessages.length === 0 && validateComment) {
          this.$emit('storeMovie', this.movieUrl, this.comment)
        }
      },

      validateMovieUrl (value) {
        const stringValidation = /^[a-zA-Z0-9!-/:-@¥[-`{-~]+$/.test(value)
        if (!value) {
          this.movieErrorMessages = [
            '入力してください',
          ]
        } else if (value.length !== 11) { 
          this.movieErrorMessages = [
            '11文字で入力してください',
          ]
        } else if (!stringValidation) { 
          this.movieErrorMessages = [
            '半角英数記号で入力してください',
          ]
        } else {
          this.movieErrorMessages = []
        }
      },
    },
  }
</script>   

Top.vue の変更

下記を参考に、コードを書き換えて下さい。

<template>
  <v-container>
    <input-form
      formRounded="xl"
      :formElevation="formElevation"
      @storeMovie="storeMovie"
    />
  </v-container>
</template>

<script>
  import InputForm from './InputForm.vue'

  export default {
    components: {
      InputForm,
    },

    data () {
      return {
        formElevation: "10",
      }
    },

    methods: {
      storeMovie (movieUrl, comment) {
        console.log(movieUrl, comment) 
      },
    },
  }
</script>

親から子への値の受け渡し props

InputForm.vueを見てください。

ここでは、まずpropsで親から子へ値を渡すために、v-cardをTop.vueからInputForm.vueに移動させています。

次に、elevation や rounded を formElevation と formRounded という親から下りてきた値で代入しています。

ここで、elevation や rounded の前についている「:」(コロン)について解説します。

これまでは、elevation="10" のような形で、特にコロンを付けずに対応してきましたが、この記述方法はあくまで10という「数字」だったり「文字列」だったりを属性として渡す方法です。

今回のように、文字列などではなくdataで指定した値props で親から渡ってきた値など、変数のように中身を入れ替え可能な「動的な値」を受け取った場合は、「:」(コロン)が必須であると覚えてください。

今回はTop.vueという親コンポーネントから渡ってきた値を使いたいので、「:」(コロン)を付けています。

そして、親から下りてきた値を規定しているのが props と書かれている部分です。propsはオブジェクト形式で値を規定することで、親から渡ってくる値の型などを厳密にチェックすることができます。記述の方法としては下記の解説のようになっています。

  • type: 値の型チェック(今回でいえば、Stringで文字列で指定)
  • required: 値の必須チェック(今回でいえば、falseなので必須ではない)
  • default: 親から値が渡ってこなかった場合のデフォルト値指定(今回で言えば、'xl'をデフォルト値とする)

このように記述することで、親から誤った型の値が渡された場合、エラーが出力されて開発中に早めに誤りに気付くことができます。なので、なるべくpropsで型チェックなどを行う癖を付けておくようにしましょう。

Top.vueを確認してください。

input-formの属性である「formRounded」の値を"xl"とすることで、カードの角の丸みを定義しています。試しに、"sm"や"circle"に変えてみましょう。入力フォームの角の丸みが変化するのがお分かり頂けると思います。

このように、親であるTop.vueから、子であるInputForm.vueに値を受け渡すことが可能なのが、propsという技術です。

ちなみに「formElevation」の前に「:」(コロン)が付いているのは、親コンポーネントTop.vueにおいてformElevationの値をdataで定義しているから(動的な値だから)です。今回は練習のために、formRoundedとformElevationの値を渡し方sを微妙に変えており、formElevationのみ動的な値としています。

子から親への値の受け渡し $emit

今回は、InputForm.vueでCLICKボタンを押した際に、入力フォームに入力された値をInputForm.vue(子)からTop.vue(親)に受け渡す形で$emitを使っています。

InputForm.vueを見てください。

v-btnをクリックしたときに、formClickというメソッドが実行されるように記述しています。イベントを実行する際は、@clickのような「@イベント名」という形で実行するイベントを指定できます。今回の場合は、「クリックされたら、formClickメソッドを実行する」という意味合いです。

なお、メソッドを記述する際は、methodsというオブジェクトの中にメソッドを記述します。

さて、formClickメソッドの中で、$emitを使って親に値を受け渡す方法を解説します。formClickメソッドの中にいろいろ記述していますが、一旦は$emitの部分だけに注目してください。$emitという関数は、下記の書き方で記述します。

this.$emit('カスタムイベント名', 親に渡したい値)

$emit関数の第一引数には、親で使うことになるカスタムイベント名(自分の好きな名前でOKです)、第二引数以降親に渡したい値を記述します。今回は、第二引数と第三引数にそれぞれ、「動画URL」と「コメント」を渡しています。

Top.vueを確認してください。

値を受け取る側の親の記述方法について解説します。親では、子コンポーネントを記述しているtemplateの属性に下記の形で記述します。

@子で指定したカスタムイベント名="親の持つメソッド名"

まず、@clickなどのイベントと同じ形で、先ほど子で記述したカスタムイベント名を記述して、イコールの後で動作させたい「親のメソッド名」を宣言します。つまり、イメージ的には、親側のイベントを子コンポーネントから無理やり発火させるような形を取ります。

次に、親のmethodsでメソッドの中身を記述することで、このメソッドとカスタムイベントを結びつけることが可能です。今回はconsole.logで子供から受け取った2つの値をコンソールに表示するように記述しています。

propsよりも$emitの方が記述方法含めて複雑ですので、少しずつ理解していきましょう。

クリック時にバリデーションを実行する

ここで記述している、バリデーション処理について解説します。

$emitで値を親に受け渡すだけだと、バリデーションチェックを行っていない値をそのまま子から親に渡していることになるので、formClickメソッドが実行された際にバリデーションを行ってから$emitが実行されるように記述しています。

まず、動画URLに関するバリデーションの記述を、movieUrlのウォッチャからvalidateMovieUrlメソッドに切り分けています。こうすることで、formClickでもvalidateMovieUrlメソッドを呼び出すだけでバリデーションを実行できます。そして、if文のところで、バリデーション実行後に「動画URLのエラーメッセージがなかった場合のみ、$emitで値を親に渡すことができる」ように条件を付けています。

また、コメントに対するバリデーションは、vuetifyのメソッドを使って行っています。formClickメソッドでv-formが持っているvalidate()メソッドを呼び出せば、強制的にコメントのバリデーションを実行できます。このvalidate()メソッドは、エラーがなければtrueを、エラーが存在すればfalseを返すので、if文のところで、「エラーがなかった場合のみ、$emitで値を親に渡すことができる」ように記述しています。

次回#3はこちら

今回の講義は以上となります。

次は、本講義のメイン機能である、「動画取得機能」「動画投稿機能」を実装していきます。

https://yu-nocode.com/vue-quest-3

この記事が気に入ったら
フォローしよう

最新情報をお届けします

Twitterでフォローしよう

おすすめの記事