Vue.js、ElementUIのDialogの動的生成やってみた

# 背景
サーバーレスで自分用の家計簿的なwebサービスを勉強も兼ねて開発中。
データ入力画面、一覧画面と開発が終わり、次はデータ編集機能となるが、その時の実現方法は悩みどころ。別ページに移っての編集も選択肢だけど、以下の観点から、モーダルダイアログを採用する事に決定。

- 一覧画面から編集に移って、編集終了後は一覧画面に戻りたい。
- 戻った際はもちろん編集内容反映したいが、再読み込みするのは表示位置が戻ったり、動作が遅くなる一因なので嫌。
- 編集する要素数は少ない（５つ程度）。

# Let's try

#### 情報集め
[element.ui公式サイト](https://element.eleme.io/#/en-US/component/dialog) 
ダイアログページを見てみる。サンプルはもちろん普通に動いているが、サンプルソースからすると、ダイアログは独立していなくて、それを使用するページのソースに書き込む感じ。入力ダイアログは複数画面で同一のものを使いまわしたい。

色々さがしていたら、以下の資料を発見。まさにやりたかった事。
[揮発性の高いコンポーネントを作る話](https://speakerdeck.com/uhck/hui-fa-xing-falsegao-ikonponentowozuo-ruhua)

以下、Vueコンポーネントのライフサイクルに関する情報も重要そう。公式ページかな？
[コンポーネントの基本](https://jp.vuejs.org/v2/guide/instance.html)

mixinという技術もある様子。同じ処理を複数のコンポーネントに埋め込めるみたい。ただ、今回はダイアログ側Vueに組み込んで使いまわす形にしたいので、使わなさそう。
[ミックスイン](https://jp.vuejs.org/v2/guide/mixins.html)

#### 方針
前述の[揮発性の高いコンポーネントを作る話](https://speakerdeck.com/uhck/hui-fa-xing-falsegao-ikonponentowozuo-ruhua)を基本にしたい。ただ、自分がやりたかった、**入力ダイアログで入力したらその値をメイン画面に反映** という機能で必要となる、ダイアログ内の値の渡し方がちょっと解らなかった。自分にVueの知識が十分にあればこれを拡張できたと思うが、まだその段階に至っていない。一旦これを参考に、以下方針で実装する。

- ダイアログの要素、共通処理はダイアログ用Vueソースにまとめる。
- 呼び出し側はそれを作成、パラメーター設定、呼び出し、後処理するだけにする。
- 使う度に生成、破棄したいが、今回は、画面起動時に１回だけ作成する形にする（破棄を考慮しなくて良い様に）。

#### ポイント
##### Dialog側
- パラメーターにcallbackを使ってダイアログ終了時処理にデータを渡せるように
- オブジェクト生成時に、親要素にDOM要素追加
- 処理成功時には、前述callbackを実行。その際に、ダイアログで編集したデータも渡す。

##### 呼び出し側
- Vue.extend 命令を使う事で、コンストラクタを生成
- そのコンストラクタで、指定パラメーターでの動的生成
- 自分の生成時に、ダイアログも生成
- callback関数でダイアログ成功時の処理を受け取る
- **this.$forceUpdate()** を使うと強制レンダリングできる。配列の要素の中身の更新ではレンダリングかからないらしく、これで処理した。

#### 実際のソース
```vue:Dialog側Vue
<template>
  <el-dialog title="入力" :visible.sync="slipDialogVisible">
    <el-form ref="form" :model="slip" label-width="60px">
      <!-- 中略 -->
    </el-form>
    <div class="dialog-footer">
      <el-button type="warning" :plain="true" @click="ondelsubmit">削除</el-button>
      <el-button type="danger" :plain="true" @click="slipDialogVisible = false">中止</el-button>
      <el-button type="primary" @click="onupdsubmit">登録</el-button>
    </div>
  </el-dialog>
</template>

<script>

export default {
  name: 'SlipInputDlg',
  // 引数
  props: {
    // 親要素
    parent: {
      type: Element,
      require: true
    },
    // ・・中略・・
    // 更新時に親側に入力データを渡すためのcallback
    callbackprm: {
      type: Function,
      require: false
    }
  },
  data () {
    return {
      slipDialogVisible: false,
      "後略": duumy
      }
    }
  },
  mounted: function () {
    // 生成時に、親要素に追加
    this.parent.appendChild(this.$el)
  },
  methods: {
    // ダイアログにデータをセットする為の親から呼ばれる関数
    setSlipData: function (slipdata) {
      // ・・中略・・
    },
    // ダイアログを表示する為に親から呼ばれる関数
    show: function () {
      this.slipDialogVisible = true
    },
    // 削除ボタン押されたときの内部処理
    ondelsubmit: function () {
      this.onsubmitsub('del')
    },
    // 更新ボタン押されたときの内部処理
    onupdsubmit: function () {
      this.onsubmitsub('upd')
    },
    onsubmitsub: function (mode) {
      // ・・中略・・
      var self = this
      this.$axios.post(this.apienv.baseendpoint + 'slip', slipdata, config).then(
        response => {
          self.slipDialogVisible = false
          self.$message({message: '登録しました', type: 'success'})
          if (self.callbackprm !== undefined) {
            // 更新成功したらcallbak関数を呼ぶ
            self.callbackprm(mode, slipdata)
          }
        }
      )
    }
  }
}
</script>

```

```vue:呼び出し側.vue
<template>
      <!-- 中略 -->
        <!-- 編集対象データクリック時にダイアログ表示 -->
        <div v-for="(item, index) in sliplist" class="slip-item" :key="index" @click="slipedit">
          <!-- 中略 -->
        </div>
      <!-- 中略 -->
</template>
// ダイアログ側Vueの読み込み
import SlipInputDlg from '@/components/SlipInputDlg'
// ・・略・・
// コンストラクタ宣言部
const SlipInputDlgConstructor = Vue.extend(SlipInputDlg)
// ・・略・・
export default {
  // ・・略・・
  // 呼び出し側生成時に、ダイアログ側も生成
  mounted (e) {
    this.inputDlg = new SlipInputDlgConstructor({
      propsData: {
        parent: this.$el,
        // ・・略・・
        callbackprm: this.dlgCloseCallback
      }
    })
    this.inputDlg.$mount()
  },
  methods: {
    // 更新操作時、ダイアログに選択データの情報をセットして、表示
    slipedit: function (e) {
      var elements = e.target.parentNode.childNodes
      const idx = [].slice.call(elements).indexOf(e.target) - 1
      this.dlgeditidx = idx - 1
      this.inputDlg.setSlipData(this.sliplist[this.dlgeditidx])
      this.inputDlg.show()
    },
    // ダイアログ側で更新処理がされたときのcallback
    dlgCloseCallback: function (mode, newSlipData) {
      if (mode === 'upd') {
        // ・・親側データ更新。略・・
        // データ再読み込みしたくないので強制表示更新
        this.$forceUpdate()
      } else {
        this.sliplist.splice(this.dlgeditidx, 1)
      }
    },
    // ・・後略・・
  }
}
```

[ダイアログ側全文はこちら](https://github.com/silverbox/homeaccounting/blob/2-3-front-dev01/frontside/homeaccount/src/components/SlipInputDlg.vue)
[呼び出し側全文はこちら](https://github.com/silverbox/homeaccounting/blob/2-3-front-dev01/frontside/homeaccount/src/components/List.vue)

# 実際の画面
![dialog_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/2636b296-2d72-abc7-1dc4-cbf92a66cc45.png)

# まとめ
- そのままだと、各ページの埋め込み要素となり、一つのVueファイルを使いまわせない。
- 表示するだけなら結構楽だと思うけど、ダイアログの中でデータ編集するとなると、データセット＋取り出しが必要になってきてそれなりの処理が必要。