JavaScript
vue.js

[Vue.js] フォームの多重送信を防止する

本記事は、Vue.js #4 Advent Calendar 2017 1日目の記事です。

概要

本記事では、フォームの多重送信対策をVue.jsで実装する場合に、どういうオプションがあるか紹介します。

:no_good: イベント修飾子 .once

Vue.jsには、イベントハンドラの処理に変化を加えるイベント修飾子(Event Modifiers)という機能があります。

  • .stop
  • .prevent
  • .capture
  • .self
  • .once

このうち、.onceを使えば多重送信を防止できるのでは? と思うかもしれませんが、これは使えません。

<button @click.once="handleClick">
  Can click once
</button>

このボタンは、文字通り1回しか押せません。.onceが内部的に何をやっているかというと、イベントハンドラが一度呼び出されたらハンドラを削除しています。

function createOnceHandler (handler, event, capture) {
  const _target = target // save current target element in closure
  return function onceHandler () {
    const res = handler.apply(null, arguments)
    if (res !== null) {
      remove(event, onceHandler, capture, _target)
    }
  }
}

https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/modules/events.js#L31-L39

本当に1回だけしか押せなくしたい場合には.onceでよいでしょう。しかし、formのsubmitボタンは、バリデーションエラーが発生したら一度送信をキャンセルして非活性化、ユーザが入力値を修正したら活性化して再度押せるように、といった制御が必要です。そのため、.onceは、formのsubmitボタンには使えません。

:ok: フォームコンポーネントに状態を持つ

以下のように、フォームコンポーネントに処理中であることを示すフラグ(ここではthis.processing)を持たせると、処理を行っている間は別の処理が割り込めないようにできます。

<template>
  <div id="app">
    <form action="">
      <input
        type="submit"
        :disabled="processing"
        @click.prevent="submit"
      />
    </form>
  </div>
</template>

<script>
export default {
  data () {
    return {
      processing: false,
    }
  },
  methods: {
    submit() {
      if (this.processing) return;
      this.processing = true;
      this.doSomething()
        .then(() => {
          this.processing = false;
        });
    },
    doSomething() {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log(`Submitted on ${new Date()}`);
          resolve();
        }, 1000);
      });
    },
  }
}
</script>

ここでは、setTimeout()を使って、一度押したら1秒間は再度押すことができないようにしています。押せない間はボタンのdisabled属性を設定しておくと親切だと思います。

:100: 多重送信しないボタンコンポーネント

複数のフォームコンポーネントで多重送信対策を行う場合、処理の共通化をしたくなります。ここでは、Mixinの使用等、いくつかのオプションがあります。個人的にオススメなのは「多重送信しないボタンコンポーネント」の作成です。

まず、以下のようなbutton要素を拡張するコンポーネントを定義します。

<template>
  <button :disabled="disabled || processing" @click="handleClick">
    <slot></slot>
  </button>
</template>

<script>
  export default {
    name: 'single-submit-button',
    props: {
      // A function which returns Promise.
      onclick: {
        type: Function,
        required: true,
      },
      disabled: {
        type: Boolean,
        default: false,
      },
    },
    data() {
      return {
        processing: false,
      };
    },
    methods: {
      handleClick(event) {
        if (this.processing) return;
        this.processing = true;
        this.onclick(event)
          .then(() => {
            this.processing = false;
          });
      },
    },
  };
</script>

利用側は、以下のようになります。ポイントは、:onclickには、Promiseを返すメソッドの名前を書く、という点です。

<template>
  <div id="app">
    <form action="">
      <single-submit-button :onclick="doSomething" type="submit">
        Click me multiple times!
      </single-submit-button>
    </form>
  </div>
</template>

<script>
  import SingleSubmitButton from './SingleSubmitButton';

  export default {
    components: {
      SingleSubmitButton,
    },
    methods: {
      doSomething(event) {
        event.preventDefault();
        return new Promise((resolve) => {
          setTimeout(() => {
            console.log(`Submitted at ${new Date()}`);
            resolve();
          }, 1000);
        });
      },
    },
  };
</script>

<single-submit-button type="submit"> のように書けば、このtype属性は内部のbuttonに引き継がれます。そのため、静的な属性は普通に書くことができます。disabled属性は動的に値を書き換える可能性があるので、propsとして定義しています。

まとめ

Vue.jsでは、再利用性のある機能はコンポーネントに切り出すと、いい感じに書けると思います。

明日はtakayamさんの「Nuxtで実案件で開発するときに作ったオレオレプラグイン」です。※URLが決まったらリンクを張る予定