【Vue.js #1 Advent Calendar 2017】Vue.js + Element でフォームダイアログをいい感じに呼び出す【24日目】

Vue.js #1 Advent Calendar 2017 24日目の記事になります。
少し前に Electron + Vue.js + Element でネイティブアプリの開発を行っていたのですが、その時に利用したコードなどを簡単に紹介してみようかと思います。

Element とは

Element(element-ui) とは Vue 2.0 ベースのコンポーネントライブラリになります。
例えば、以下のようにモーダルダイアログからフォーム入力などを行うことが出来ます。

index.html

<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<div id="app">

  <!-- 登録ダイアログ -->
  <el-button @click="registry_visible = true">登録</el-button>
  <pre>{{ registry_data }}</pre>

  <el-dialog :visible.sync="registry_visible" title="保存">
    <el-form :model="registry_form" label-width="120px">
      <el-form-item label="first name">
        <el-input v-model="registry_form.first_name"></el-input>
      </el-form-item>
      <el-form-item label="last name">
        <el-input v-model="registry_form.last_name"></el-input>
      </el-form-item>
      <el-form-item label="age">
        <el-input v-model="registry_form.age"></el-input>
      </el-form-item>
    </el-form>

    <span slot="footer" class="dialog-footer">
      <el-button @click="registry_visible = false">Cancel</el-button>
      <el-button type="primary" @click="registry_submit">保存</el-button>
    </span>
  </el-dialog>
  
</div>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="./script.js"></script>

script.js

new Vue({
  el: '#app',
  data: function() {
    return {
      registry_visible: false,
      registry_form: {},
      registry_data: [],
    }
  },

  methods: {
    registry_submit(){
      this.registry_data.push(this.registry_form);
      this.registry_form = {};
      this.registry_visible = false;
    },
  },
});

on jsbin.com

こんな感じのコードをもうちょっと使いやすくしてみたので今回はそれについて書いてみようかと思います。

問題点

問題点ってほどでもないと思うんですが、例えばフォーム入力にデフォルト値とかを設定したい場合にちょっと複雑になってきます。

<!-- 登録ダイアログ -->
<!-- 開く時に関数を呼び出す -->
<el-button @click="open_registry">登録</el-button>
<pre>{{ registry_data }}</pre>

<el-dialog :visible.sync="registry_visible" title="保存">
  <el-form :model="registry_form" label-width="120px">
    <el-form-item label="first name">
      <el-input v-model="registry_form.first_name"></el-input>
    </el-form-item>
    <el-form-item label="last name">
      <el-input v-model="registry_form.last_name"></el-input>
    </el-form-item>
    <el-form-item label="age">
      <el-input v-model="registry_form.age"></el-input>
    </el-form-item>
  </el-form>

  <span slot="footer" class="dialog-footer">
    <el-button @click="registry_visible = false">Cancel</el-button>
    <el-button type="primary" @click="registry_submit">保存</el-button>
  </span>
</el-dialog>
new Vue({
  el: '#app',
  data: function() {
    return {
      registry_visible: false,
      registry_form: {},
      registry_data: [],
    }
  },

  methods: {
    // ダイアログを開く関数
    open_registry(){
      this.registry_visible = true;
      form = this.registry_data[this.registry_data.length - 1] || {
        first_name: "yamada",
        last_name:  "tarou",
        age: 20
      };
      Object.assign(this.registry_form, form);
    },

    // フォームを登録する時に呼ばれる関数
    registry_submit(){
      this.registry_data.push(this.registry_form);
      this.registry_form = {};
      this.registry_visible = false;
    },

  },
});

JS Bin on jsbin.com

こんな感じで複数の関数を言ったり来たりするのがちょっともにょったのでなんとかしてみました。

async/await を利用する

まず、async/await を利用して上記open_registry()registry_submit() を1つの関数にまとめてみました。

index.html

<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<div id="app">
    <!-- 登録ダイアログ -->
    <el-button @click="registry">登録</el-button>
    <pre>{{ registry_data }}</pre>

    <el-dialog :visible.sync="registry_visible" title="保存" :before-close="close">
      <el-form :model="registry_form" label-width="120px">
        <el-form-item label="first name">
          <el-input v-model="registry_form.first_name"></el-input>
        </el-form-item>
        <el-form-item label="last name">
          <el-input v-model="registry_form.last_name"></el-input>
        </el-form-item>
        <el-form-item label="age">
          <el-input v-model="registry_form.age"></el-input>
        </el-form-item>
      </el-form>

      <span slot="footer" class="dialog-footer">
        <el-button @click="registry_visible = false">Cancel</el-button>
        <el-button type="primary" @click="submit">保存</el-button>
      </span>
    </el-dialog>
</div>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="./script.js"></script>

script.js

new Vue({
  el: '#app',
  data: function() {
    return {
      registry_visible: false,
      registry_form: {},
      registry_data: [],
    }
  },

  methods: {
    open(name, form){
      this[`${name}_form`] = form;
      this[`${name}_visible`] = true;
      return new Promise((resolve, reject) => {
        var submitted = false;
        this.submit = () => {
          this[`${name}_visible`] = false;
          submitted = true;
          resolve(this[`${name}_form`]);
        };

        this.close = () => {
          this[`${name}_visible`] = false;
          if( !submitted ){
            reject("Dialog is closead.");
          }
        };
      });
    },
    // ダミー
    submit(){},
    close(){},

    async registry(){
      this.registry_visible = true;
      form = this.registry_data[this.registry_data.length - 1] || {
        first_name: "yamada",
        last_name:  "tarou",
        age: 20
      };
      result = await this.open("registry", {...form});
      this.registry_data.push(result);
    },
  },
});

JS Bin on jsbin.com

こんな感じで async/await を使うことで

result = await this.open("registry", {...form});

のように直接フォームを呼び出して、その値を取得することが出来ます。

コンポーネント化する

さてさて、先ほどのコードだとちょっと汎用性にかけるので簡単にコンポーネント化してみたいと思います。

Vue.component("form-dialog-wrapper", {
  template: `
    <div>
      <slot :submit="submit" :form="form" :close="close" :visible.sync="visible"></slot>
    </div>
  `,
  data(){
    return {
      visible: false,
      form: {},
    }
  },

  methods: {
    open(form){
      this.form = form;
      this.visible = true;
      return new Promise((resolve, reject) => {
        var submitted = false;
        this.submit = () => {
          this.visible = false;
          submitted = true;
          resolve(this.form);
        };

        this.close = () => {
          this.visible = false;
          if( !submitted ){
            reject("Dialog is closead.");
          }
        };
      });
    },

    // ダミー
    close(){},
    submit(){}
  }
});

コンポーネントはこんな感じです。
Vue.js のポイントとしては slot を使って form のデータのやり取りを行うところでしょうか。
HTML 側では以下のように使用します。

index.html

<!-- 登録ダイアログ -->
<el-button @click="registry">登録</el-button>
<pre>{{ registry_data }}</pre>

<!-- JavaScript 側から参照できるように ref で名前付けしておく -->
<form-dialog-wrapper ref="dialog_registry">
  <!-- form データへの参照は slo-scope 経由で行う -->
  <el-dialog title="保存" slot-scope="props" :visible.sync='props.visible' :before-close="props.close">
    <!-- slot-scope 経由でデータを参照することでコンポーネントのデータのみ bind 出来る -->
    <el-form :model="props.form" label-width="120px">
      <el-form-item label="first name">
      <el-input v-model="props.form.first_name"></el-input>
      </el-form-item>
      <el-form-item label="last name">
      <el-input v-model="props.form.last_name"></el-input>
      </el-form-item>
      <el-form-item label="age">
      <el-input v-model="props.form.age"></el-input>
      </el-form-item>
    </el-form>

    <span slot="footer" class="dialog-footer">
      <el-button @click="props.close">Cancel</el-button>
      <el-button type="primary" @click="props.submit">保存</el-button>
    </span>
  </el-dialog>
</form-dialog-wrapper>

こんな感じで slot-scope="props" を利用してコンポーネントのデータを参照します。
このようにすることでメインの Vue ではフォームの入力データなどを用意する必要がなくなります。
実際にメインの Vue は以下のようになります。

new Vue({
    el: '#app',
    data: function() {
        return {
            registry_data: [],
        }
    },

    methods: {
        async registry(){
            form = this.registry_data[this.registry_data.length - 1] || {
                first_name: "yamada",
                last_name:  "tarou",
                age: 20
            };

            // ref 経由でコンポーネントを参照する
            result = await this.$refs["dialog_registry"].open({...form});
            this.registry_data.push(result);
        },
    },
});

このようにかなりコードがすっきりしている事がわかると思います。

JS Bin on jsbin.com

まとめ

と、言う感じでフォームダイアログをいい感じにしてみました。
元々はフォームダイアログが複数あってどんどんデータが肥大化していったのでなんとかしたいと思いこんな感じのロジックを思いつきました。
Vue.js もそうですが async/await めちゃ便利ですね…。
Vue.js 自体はまだ全然やれてないんですが、こういうのを回避する手段って他にもあったりするんですかね…(ちなみに Vuex はまだやってない。

そんな感じで簡単に書いてみましたー。
もっとこうしたらいいよーみたいなのがあればコメントや Twitter までおねがいしますー。