豆腐とコンソメ

豆腐とコンソメ

もろもろのプログラム勉強記録

Vue.jsでつくるオブジェクト指向フォーム(2)

前回の続き

www.tohuandkonsome.site

さて投げやりになってしまった前回だけど、気をとりなおしていくよ!

前回は、

Vue.jsのフォーム入力バインディング(v-model)を利用してaxiosでサーバーにpostする

ところまでやりました。

javascriptはこんな感じでしたね。

app.js

const app = new Vue({
    el: '.simple-form',
    data:{
        title:'',
        body:'',
    },
    methods:{
        //Formのsubmitイベントが発生したとき
        onSubmit:function(){
            //$dataは上記のdataのことだよ!  
            axios.post('/thread', this.$data)
            //HTTPリクエストが成功したとき
            .then(response => console.log(response.data))
            //HTTPリクエエストが失敗した時
            .catch(error => console.log(error.response));
        },
    }
});

それでは、この次に、エラーが発生したときにフォームのコントロールにエラーがでるようにしてあげましょう。


フォームコントロールにエラーを表示する

エラーを格納するクラスErrorsをつくろう

さて、エラーのチェックですが、ベーシックにbodytitleの必須チェックを行います。
また、チェックはサーバ側で行います。

Laravelでバリデーションチェックをすると、以下のようなレスポンスを返してくれます。

Laravelがバリデーションエラーで返してくれるレスポンス

{
    "errors": {
        "body": [
            "本文が未入力だよ"
        ], 
        "title": [
            "タイトルが未入力だよ"
        ]
    }, 
    "message": "The given data was invalid."
}

なので、このレスポンスを格納するErrorsクラスをつくってみましょう。

javascriptでもクラスをつくれるんだなぁという語弊のある感想はさておき、以下のようなerrorsプロパティをもつクラスをつくってあげます。

また、エラーを設定するセッター的なメソッドrecordも用意しています。

app.js(抜粋)

class Errors{
   constructor() {
    //エラー情報を管理するプロパティ
    this.errors = {}
   }

   //エラーを設定するメソッド
   record(errors){
        this.errors = errors
   }
}

クラスなのでerrors.jsをつくって管理すべきな気もしますが、面倒なのでここではapp.jsにそのまま書いちゃいます。

一方のVueインスタンスでは以下のようにErrorクラスをインスタンス化しておいてdataに用意しておきます。

app.js(抜粋)

const app = new Vue({
    el: '.simple-form',
    data:{
        title:'',
        body:'',
        //Errorクラスをつくっておく
        errors: new Errors()
    },

そうしたら、エラーが発生したときにErrorインスタンスにさきほどのレスポンスをつっこむようにしてあげましょう。

app.js(抜粋)

    methods:{
        //Formのsubmitイベントが発生したとき
        onSubmit:function(){
            axios.post('/thread', this.$data)
            //HTTPリクエストが成功したとき
            .then(response => console.log(response.data))
            //HTTPリクエエストが失敗した時
            .catch(error => {
                //エラーを設定する
                this.errors.record(error.response.data.errors);
            });

axiosのレスポンスデータの取得の仕方は、errors.response.dataですが、ここではさらにその配下のネストである、errors.response.data.errorsのほうが何かと都合がいいので、こうしています。

さて、エラーが設定できたので、これをhtml側から参照できるようにしてあげましょう。

create_ajxa.blade.php(抜粋)

    <form class="simple-form" action="/thread" method="post" v-on:submit.prevent="onSubmit">
        {{csrf_field()}}
       <div class="simple-form__group">
            <label class="simple-form__title" for="title">タイトル</label> 
            <!-- v-modelで、フォームのinput系(select、textareaとかも)の要素とVueインスタンスの変数をバインディングする -->
            <input class="simple-form__input" type="text" name="title" v-model="title">
            //エラーの情報を表示する
            <p class="simple-form__error" v-if="errors.has('title')" v-text="errors.get('title')"></p> 

上記のように、errros.get()'でエラーメッセージを取得してあげます。 また、表示する際の条件としてerrors.has()`も追加しています。

実装の方は以下の通りです。

app.js(抜粋)

/**
 * エラー情報を管理するクラスだよ!
 */
class Errors{
   constructor() {
    //エラー情報を管理するプロパティ
    console.log("im created")
    this.errors = {}
   }

   /**
    * @param {*} errors :axiosのerror.response.data、つまりエラー時のレスポンスのbodyが入るんだよ
    */
   record(errors){
        this.errors = errors
   }

   /**
    * レスポンスデータのbody部分のエラーメッセージを返すよ
    *
    * @param {string} field  コントロールの名前
    * @returns {string}
    */
   get(field){
        if(this.errors[field]){
            return this.errors[field][0];
        }
   }

   /**
    * 
    * @param {string} field  コントロールの名前
    * @returns {boolean}
    */
   has(field){
      return  this.errors.hasOwnProperty(field)
   }

getメソッドですが、jsonを取得する際に、this.errors[filed]とするのか、this.errors.fieldどっちが正しいのかと混乱しました。

結論としては、オブジェクトのプロパティにアクセスする場合であればどっちでもいいみたいです。 とはいえ、上記のようにプロパティ名が変数になっている場合、配列のようにthis.errors[field]としないとだめみたいですが。

参考にさせていただいた記事
JavaScriptのオブジェクトのキーに変数の値を使うTips - Qiita

もう一方のhasメソッドですが、こちらも大事な役割も担っています。
hasOwnPropertyはすべてのオブジェクトが持っているメソッドで、オブジェクト内に引数で指定されたプロパティが存在するかをチェックする機能をもっています。

当初、axiosから設定するエラーデータを、errors.response.dataとしていて、Errorクラス内部でthis.errors.errros.hasOwnPropertyとやっていました。

しかし、これだと、 エラーが発生していない場合、this.errors.errorsは存在しておらず、hasOwnPropertyなんてないよ!と怒られてしまいます。

あたりまえのことですが、いろいろはまってしまいました。


ここまできたら、試しにエラーが表示されるか試してみます。
何も入力されていない状態で、postをすると、以下のようにエラーが表示されました。

f:id:konoemario:20171216135133p:plain

いい感じです。


テキストボックスに値を入力したらエラーを消すようにする

エラーを表示することができたので、今度はエラーを消していきます。

消すタイミングは、タイトルの通りテキストボックスに値が入力されたら消していきたいと思います。

イメージとしては、以下のようにコントロールにキー入力されたらというイベントを@keydownで捉えてあげます。
そして実行するメソッドはerrors.clear()になります。

create_ajxa.blade.php(抜粋)

       <div class="simple-form__group">
            <label class="simple-form__title" for="title">タイトル</label> 
            <!-- v-modelで、フォームのinput系(select、textareaとかも)の要素とVueインスタンスの変数をバインディングする -->
            <input class="simple-form__input" type="text" name="title" v-model="title" @keydown="errors.clear('title')">
       </div>

Errorsクラスはシンプルにdelete演算子を使って、プロパティを削除してしまいます。

app.js(抜粋)

class Errors{
//省略
   /**
    * filedで指定されたプロパティを削除する
    * @param {string} field 
    */
   clear(field){
    delete this.errors[field];
   }

}

これだけでも、やりたいことは実現できました。

ですが、以下のようにフォームの@keydownイベントして書いてあげると、コントロールごとに記載しなくてもいいのでさらにシンプルになります。

create_ajxa.blade.php(抜粋)

    <!--Submitのデフォルトイベントをキャンセルして、VueインスタンスのonSubmitメソッドを呼ぶ-->
    <form class="simple-form" action="/thread" method="post" v-on:submit.prevent="onSubmit" @keydown="errors.clear($event.target.name)">


フォームクラスをつくろう

ここまでの内容で、以下のことができるようになりました。

  • ブラウザで入力した値をaxiosでサーバーにpostする
  • サーバから返却されたエラーメッセージをコントロールにひも付けて表示する
  • コントロールに入力すことでエラーメッセージを消す

とはいえ、現時点で気になる点があります。

Vueインスタンスの、データを送信する部分ですがthis.$dataを送っています。
$dataにはtitlebody以外にもerrorsも含まれてしまっていてちょっと気持ちが悪いです。

app.js(抜粋)

    data:{
        title:'',
        body:'',
        errors: new Errors()
    },
    methods:{
        //Formのsubmitイベントが発生したとき
        onSubmit:function(){
            axios.post('/thread', this.$data)

なので、ここらでタイトル通りのフォームクラスを作っていきたいと思います。

app.js(抜粋)

/**
 * フォームクラスだよ!
 */
class Form{
   constructor(data) {

    //フォームのデータ 
    this.originalData = data;

    //エラー情報を管理するプロパティ
    this.errors = new Errors();
   }

}

Formクラスにはerrosプロパティとフォーム内のコントロール要素とバインディングされるデータoriginagDataプロパティを持ちます。

Vueインスタンス側では、こんな感じにしてあげます。

app.js(抜粋)

const app = new Vue({
    el: '.simple-form',
    data:{
        form: new Form({
            title:'',
            body:'',
        })
    },

こうすることで、VueインスタンスがFormクラスをもっていて、Formの中にはtitlebodyというコントロールがあってというなんとなくオブジェクト指向っぽくなってきた気がしませんか!(頭が悪い)

この方針で、コードを修正していきたいと思います。


フォームクラスに合わせていろいろと修正する

フォームクラスを作った弊害として、html側ではデータをバインディングする際に以下のようにv-model="form.originalData.title"としてあげなければいけません。

create_ajxa.blade.php(抜粋)

       <div class="simple-form__group">
            <label class="simple-form__title" for="title">タイトル</label> 
            <!-- v-modelで、フォームのinput系(select、textareaとかも)の要素とVueインスタンスの変数をバインディングする -->
            <input class="simple-form__input" type="text" name="title" v-model="form.originalData.title">
       </div>

ですが、さすがのLaracastさんです。
以下のようにすることで、Formクラスのプロパティとして扱えるようになります。

app.js(抜粋)

class Form{
   constructor(data) {

    //フォームのデータ 
    this.originalData = data;

    //dataの各要素をFormクラスのプロパティとして登録する
    for(let field in this.originalData){
        this[field] = this.originalData[field];
    }

これで、以下のようにform.titleという形でアクセスできるようになりました。
create_ajxa.blade.php(抜粋)

       <div class="simple-form__group">
            <label class="simple-form__title" for="title">タイトル</label> 
            <!-- v-modelで、フォームのinput系(select、textareaとかも)の要素とVueインスタンスの変数をバインディングする -->
            <input class="simple-form__input" type="text" name="title" v-model="form.title">
       </div>

また、formに書いたエラーをクリアする処理も、Errorsクラスはフォームクラスのプロパティになったので、以下のように修正します。

create_ajxa.blade.php(抜粋)

  <form class="simple-form" action="/thread" method="post" v-on:submit.prevent="onSubmit" @keydown="form.errors.clear($event.target.name)">

エラーを表示する部分も同様に修正しておきます。

create_ajxa.blade.php(抜粋)

       <div class="simple-form__group">
            <p class="simple-form__error" v-if="form.errors.has('title')" v-text="form.errors.get('title')"></p> 
       </div>


フォームクラスに機能を移管する

次に、フォームの内容を送信する機能もフォームクラスに移しちゃいましょう!

下記のように、submitメソッドを追加してあげます。
app.js(抜粋)

class Form{
//省略
   /**
    * Formのデータをサーバーに送信するよ!
    */
   submit(){
        //プロパティの値を再設定する
        for(let field in this.originalData){
            this.originalData[field] = this[field];
        }

        axios.post('/thread', this.originalData)
        //HTTPリクエストが成功したとき
        .then(response => this.onSuccess(response.data))
        //HTTPリクエエストが失敗した時
        .catch(error => this.onFail(error.response.data.errors));
   }

   /**
    * フォームデータの送信が成功した場合
    */
   onSuccess(response){
        console.log(response);
   }

   /**
    * フォームデータの送信が失敗した場合
    */
   onFail(error){
        this.errors.record(error);
   }

}

基本的に、Vueインスタンスに書いてあった処理をまるっともってくるだけです。
axiosで送信するデータのoriginalDataはコントロールにバインディングされていないので、keyだけあって、値は空っぽです。
なので送信する前に、データバインディングしてあるフォームクラスのプロパティの値を設定してあげています。

そろそろ長くなってきたので次回に回します!