フロントエンド開発Blog

オレには鈍器がある

CSS Modules , ES2015 , JavaScript , Vue.js , Vuex , babel , webpack

前回に続いてEditコンポーネントについて解説します。

Edit.vue以下のコンポーネント群


Edit.vue
 KkbForm
 Notification
 Loading

KkbFormが肝で、残りは共通コンポーネントです。

KkbForm

いろんなギミックが詰まっています。

このコンポーネントは新規追加と既存データの編集2通りで使われます。router.jsを見ると、URLパターンが/editの場合と/edit/:idの2通りの場合にマウントされることが分かります。


// router.js 10行目付近

var routes = [
  { path: '/', component: Monthly },
  { path: '/monthly/:yearmonth', component: Monthly },
  { path: '/edit/', component: Edit }, //★ココ
  { path: '/edit/:id', component: Edit }, //★ココ
  { path: '/yearly/:year', component: Yearly }
];

mountされたとき、routerのparamsが空なら新規、そうでないなら編集として動作します。


  /**
   * マウント時に毎回実行
   */
  mounted: function(){
    if(this.$route.params.id){
      // 編集モード
      // IDと一致するデータ取得
      let targetData = this.getTargetDataFromMonthlyState(this.$route.params.id);

      // localstateに値を登録
      this.edit = targetData;
      //this.$vue.set(this, "edit", targetData);

      // datepickerのtimeに該当に時間を代入
      this.time = targetData.year + "-" + targetData.month + "-" + targetData.date;
    }else{
      // 新規追加モード VuexのStoreではなくlocalStateを利用したことで初期化処理が不要になった(マウント時に初期化されるため)

    }

    // 商品名候補リスト取得
    if(this.formTitleList.length === 0){
      this.getFormTitleList();
    }
  },

このコンポーネントのmethodsに定義されたgetTargetDataFromMonthlyState関数でStore.state.monthlyデータから該当IDのデータを取得します。そのデータをlocalstateのeditに登録することでForm要素の個々の値が既存の情報が表示されます。Store.state.monthlyからデータを引用する関係上、月別データは取得済みである前提の処理となっています。


// ./components/KkbForm.vue 160行目付近

    /**
     * 月別データからIDを指定して該当するデータを返却
     * @param  {Number} id 記事ID
     * @return {Object}    該当データオブジェクト
     */
    getTargetDataFromMonthlyState: function(id){
      // Storeのmonthlyデータから編集対象となるデータを取得
      let targetData = this.monthly.detail.filter(function(d){
        return d.Form.id == id;
      });
      // IDからふるい分けして取得したデータが空でないならデータを整形して返却
      if(targetData && targetData.length){
        targetData = targetData[0]["Form"];
        // 金額などで数字がnullで送られた場合は空文字列に初期化
        targetData["price"]     = targetData["price"]     == null ? "" : targetData["price"];
        targetData["pay_danna"] = targetData["pay_danna"] == null ? "" : targetData["pay_danna"];
        targetData["pay_yome"]  = targetData["pay_yome"]  == null ? "" : targetData["pay_yome"];
        // データ構造をlocalstate用に変換
        targetData = this.convertAjaxDataToState(targetData);//★
      }
      return targetData;
    },

getTargetDataFromMonthlyStateの中で使われているconvertAjaxDataToStateという関数は、AJAXで取得したデータ構造をlocalstateのeditデータ向けに変換して返すメソッドになります。処理は単純にキー名の変更や不要データのdeleteだけでシンプルですのでコード掲載は割愛。

また、Formの送信時は逆のことをしています。convertStateToAjaxDataというメソッドがそれに相当します。sendFormというactionsに引数としてconvertStateToAjaxData関数を渡し、actions内でAJAXレスポンスデータに対して使用されます。


// ./components/KkbForm.vue 100行目付近

    <div class="formBtnArea">
      <p><button @click="sendForm({edit, clearEditState, convertStateToAjaxData})" :class="style.btnSubmit">SUBMIT</button><button @click="allDanna" :class="style.btnA">全額旦那</button><button @click="allYome" :class="style.btnB">全額嫁</button><button @click="diff" :class="style.btnGeneral">差額反映</button></p>
    </div>

SUBMITボタンのclickイベントにsendForm({edit, clearEditState, convertStateToAjaxData})のように関数をオプションとして渡す。


// ./store/actions.js 150行目付近
/**
 * フォーム入力内容を送信
 * @param  {Object}     Vuexオブジェクト
 * @param  {Object}     option連想配列
 */
export const sendForm = function({commit}, {edit, clearEditState, convertStateToAjaxData}){
  // 商品名と価格が入力済みなら送信処理を実行
  if(edit.title && edit.price){
    let _this = this;
    let sendData = Object.assign({}, edit);

    // localstateからAJAXパラメータのデータ構造に変換
    sendData = convertStateToAjaxData(sendData);

actions内にて関数を使用している。

次いで日付入力部分ですが、inputvue-datepicker-localが連携して動作しています。inputはedit.year edit.month edit.dateという3つのlocalなデータとv-modelで紐づいています。対してdatepickertimeというlocalなデータをv-modelとして使用しています。

本来ならedit.year edit.month edit.dateを取りまとめた値がtimeにアサインされ、timeを変更したらedit.year edit.month edit.dateにそれぞれバラされてアサインされるのが理想ですが、そのやり方が分からない。

二種類の別々のdataをそれぞれイイ感じに同期する方法が見つからなかったのでごり押しの手を使ってます。

input(edit.year edit.month edit.date)からdatepicker(time)への同期はblurイベントに紐づけてtimeを更新します。


    /**
     * 年月日をblurした際の挙動
     * @param  {String} type year|month|date
     */
    dateBlur: function(flag){
      // blur字に0埋め処理
      if(flag === "month"){
        this.edit.month = ("0" + this.edit.month).slice(-2);
      }else if(flag === "date"){
        this.edit.date = ("0" + this.edit.date).slice(-2);
      }

      // datepickerのtimeに入力値をsync
      this.time = this.edit.year + "-" + this.edit.month + "-" + this.edit.date;
    }

datepicker(time)からinput(edit.year edit.month edit.date)への同期はwatchでtimeデータを監視し、変更後に同期処理を実行します。


watch: {
    // datepickerの値の変動をedit値に反映
    'time' (changedDate) {
      if(typeof changedDate !== "string"){
        this.edit.year = "" + changedDate.getFullYear();
        this.edit.month = ("0" + (changedDate.getMonth() + 1)).slice(-2);
        this.edit.date = ("0" + changedDate.getDate()).slice(-2);
      }
    }
}

なかなかの力業ですが、意図通りのものができました。

次に商品名のオートコンプリートですが、こちらはAJAXで取得したデータをdatalistに取得して表示するようにしました。


    <dl class="formInputList">
      <dt class="titleArea"><label for="edit_title">商品名</label></dt><dd class="inputArea"><input type="text" v-model="edit.title" class="text" id="edit_title" autocomplete="on" list="formTitleList" />
        <datalist id="formTitleList">
          <option v-for="(item, idx) in formTitleList" :value="item">{{item}}</option>
        </datalist>
      </dd>
    </dl>

v-forで展開されるformTitleListというデータはVuexのStateの値です。コンポーネントのマウント時、Store.state.formTitleListが空ならactionsのgetFormTitleList関数が実行され、mutations経由で更新されます。


  // ./components/KkbForm.js 300行目付近

  /**
   * マウント時に毎回実行
   */
  mounted: function(){
    if(this.$route.params.id){
      // 編集モード
      // IDと一致するデータ取得
      let targetData = this.getTargetDataFromMonthlyState(this.$route.params.id);
      // localstateに値を登録
      this.edit = targetData;
      //this.$vue.set(this, "edit", targetData);
      // datepickerのtimeに該当に時間を代入
      this.time = targetData.year + "-" + targetData.month + "-" + targetData.date;
    }else{
      // 新規追加モード VuexのStoreではなくlocalStateを利用したことで初期化処理が不要になった(マウント時に初期化されるため)
    }
    // 商品名候補リスト取得
    if(this.formTitleList.length === 0){
      this.getFormTitleList(); //★ココ
    }
  },

何度もfetchする必要はないので、formTitleListが空なら実行。


// ./store/actions.js 280行目付近
/**
 * FormTitleListを取得
 * @param  {Object}     Vuexオブジェクト
 */
export const getFormTitleList = function({commit}){
  // productionもdevもGETで取得
  let ajaxUrlString = ajaxUrl.formTitleList;
  let method = "GET";

  let headers = {'Accept': 'application/json'}

  let ajaxOption = {
    method,
    headers,
    credentials: 'include'
  }

  fetch(ajaxUrlString, ajaxOption)
    .then(function(res){
      if(res.ok){
        return res.json();
      }
    }).then(function(json){
      commit(types.UPDATE_FORM_TITLE_LIST, json.list); // ★mutationsへ
    });
}

// ./store/mutations.js 130行目付近

  /**
   * Formの商品名部分のオートコンプリート候補データを更新
   * @param  {Object} state  state
   * @param  {Array}  list   候補文字列リスト
   */
  [types.UPDATE_FORM_TITLE_LIST]: function(state, list){
    state.formTitleList = list;
  }

これで静的なJSONファイルで項目の増減が可能になりました。

続いて全額旦那、全額嫁、差額反映ボタンのクリックイベントにそれぞれ簡単な処理が書かれています。処理自体はとてもシンプルですのでコードは割愛。。

actionsのsendFormについてもう少し補足しておくと、JSONにisNewという値があり、1なら新規追加として、入力内容を全てクリアする処理を実行するようにしています。


// ./store/actions.js 200行目付近

        if(json.isNew == 1 && json.status == 1){
          // 新規入力だったらフォーム入力内容を初期化
          clearEditState();
          //edit = Object.assign({},DEFAULT_EDIT_STATE);
        }

clearEditStateという関数はコンポーネントから渡されたコールバック関数です。KkbFormコンポーネントの以下の記述のように、オプション変数として渡されています。


@click="sendForm({edit, clearEditState, convertStateToAjaxData})"

ページトップへ

関連ページ

ページトップへ