33444


よろしくお願いいたします。  
HTMLでコード入力用のテキストボックス(#item_cd)があります。  
javascriptでは、#item_cdのonblurイベントで、入力されたコードからマスタデータを参照して、取得した値から名称の表示や計算を行うような処理を記述しました。  
処理はdeferredとpromiseを使って取得～計算までを一連の流れとして記述しています。

また、画面には「登録」ボタンが用意されており、登録ボタンのclickイベントで、入力されたコードとフォーム内に保持している情報からデータの更新を行うような処理が記述されています。

この状態で、#item_cdにフォーカスをセットした状態でコードを入力し、フォーカスを動かさずに登録ボタンをclickすると、onblur→clickの順番にイベントは発火しているのですが、onblurに記述した処理が完了する前にclickに記述した登録処理が実行されて、エラーが発生してしまいます。

基本的にフォーム上でコードを入力して名称を表示するといった処理は非同期で行いたいのですが、登録ボタンを押す点については他のイベント処理が完了するまで待つ必要があります。  
力技ですが各イベントの開始と終了時にフラグON/OFFを行い、登録ボタンがclickされたタイミングでチェックする方法が思いつきましたが、他に良い方法があれば教えて下さい。

* * *

2017.3.27 追記  
拙いですが、サンプル作ってみました。本当はjquery-2.1.1ですが、選択肢にないのでJqueryの2.1.3でも動きます。Blurで行う処理を遅延させる為にわざとwaitを入れてあります。<https://jsfiddle.net/tika1952/p67guu86/23/>


CC BY-SA 3.0
2017-03-21T07:22:26.370
33438
2019-12-13T18:06:16.893
2017-03-27T08:30:46.747
21135
21135
question
javascript jquery html5
blurイベントで開始した非同期処理が完了するまでフォーム送信をさせないためには


Promise を使うと、非同期処理の順序を制御することができます  
<https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise>

Promise には フラグOFF => ON の切り替えをする仕組みが備わっています  
そして、フラグON になったときに行ってほしい処理を予約することができます

非同期処理の処理順序を制御する簡単なサンプルを用意しました  
<https://jsfiddle.net/gocho/mpbpmzgy/>

[code]

    {
      const $item_cd = $('#item_cd');
      const $ajax_result = $('#ajax_result');
      const $register_button = $('#register_button');
      const $spinner = $('#spinner');
      const databaseMock = {
        1: {
          name: 'item 1',
          price: 550
        },
        2: {
          name: 'item 2',
          price: 700
        }
      };
      const errorValue = {
        error: 'error'
      };
    
      const promises = {
        // 入力必須でないなら、初期状態はresolved
        // item_cd: $.Deferred().resolve(errorValue).promise()
        // 入力必須なら、初期状態はpending
        item_cd: $.Deferred().promise()
      };
    
      $item_cd.on('blur', function(ev) {
        console.info('$item_cd.blur triggered');
        const ajaxResponse = databaseMock[this.value] || errorValue;
    
        console.info('do ajax');
        $spinner.html('ajax中...');
        // promiseをpendingにする
        promises.item_cd = $.ajax({
          url: '/echo/json/',
          method: 'POST',
          data: {
            json: JSON.stringify(ajaxResponse),
            delay: 1.5
          },
        }).promise().then(
          (response, status, jqXHR) => {
            // ajaxが完了した時点でpromiseがresolvedになる
            console.info('ajax finished');
            $spinner.html('');
            $ajax_result.val(JSON.stringify(response));
            return response;
          }
        );
      });
    
      $register_button.on('click', function(ev) {
        console.info('$register_button.click triggered');
        ev.preventDefault();
    
        console.info('blurPromise.then');
        // promiseがresolvedになったら処理を実行するよう、予約をする
        promises.item_cd.then((response) => {
          console.info('blurPromise resolved');
    
          // some register process with ajax value...
          console.info(response);
        });
      });
    }
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div>
      textbox for blur: <input id="item_cd" type="text" />
    </div>
    <div>
      ajax result: <input id="ajax_result" name="ajax_result_name" value="initial_value" />
    </div>
    <button id="register_button">登録</button>
    <div id="spinner"></div>
[/code]

Promise.allを利用する例  
<https://jsfiddle.net/gocho/mpbpmzgy/3/>

[code]

    {
      const $input_components = $('.input_component');
      const $register_button = $('#register_button');
      const $spinner = $('#spinner');
      const dbMock = {
        item_cd: {
          1: {
            name: 'item 1',
            price: 550
          },
          2: {
            name: 'item 2',
            price: 700
          }
        },
        some_key1: {
          1: {
            value1: 'value1'
          },
          2: {
            value1: 'value2'
          }
        },
        some_key2: {
          1: {
            value2: 10
          },
          2: {
            value2: 20
          }
        }
      };
      const errorValue = {
        error: 'error'
      };
    
      const promises = {
        item_cd: $.Deferred().resolve(dbMock.item_cd[1]).promise(),
        some_key1: $.Deferred().resolve(dbMock.some_key1[1]).promise(),
        some_key2: $.Deferred().resolve(dbMock.some_key2[1]).promise()
      };
    
      $input_components.on('blur', '.input_element', function(ev) {
        const $input_component = $(ev.delegateTarget);
        const $input_element = $input_component.find('.input_element');
        const name = ev.delegateTarget.dataset.name;
    
        console.info(`${name}.blur triggered`);
        const ajaxResponse = dbMock[name][$input_element.val()] || errorValue;
    
        console.info('do ajax');
        $spinner.html('ajax中...');
    
        promises.item_cd = $.ajax({
          url: '/echo/json/',
          method: 'POST',
          data: {
            json: JSON.stringify(ajaxResponse),
            delay: 1
          },
        }).promise().then(
          function(response, status, jqXHR) {
            console.info('ajax finished');
            $spinner.html('');
            $input_component.find('.ajax_result').html(JSON.stringify(response));
            return response;
          }
        );
      });
    
      $register_button.on('click', function(ev) {
        console.info('$register_button.click triggered');
        ev.preventDefault();
    
        // promisesが全てresolvedになったら処理を実行するよう、予約をする
        Promise.all([
          promises.item_cd,
          promises.some_key1,
          promises.some_key2
        ]).then(([item_cd, some_key1, some_key2]) => {
          console.info('promises all resolved');
          console.info(item_cd, some_key1, some_key2);
        });
      });
    }
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div>
      <div class="input_component" data-name="item_cd" >item_cd:
        <input class="input_element" type="text" />
        <p class="ajax_result"></p>
      </div>
      <div class="input_component" data-name="some_key1">some_key1:
        <input class="input_element" type="text" />
        <p class="ajax_result"></p>
      </div>
      <div class="input_component" data-name="some_key2">some_key2:
        <input class="input_element" type="text" />
        <p class="ajax_result"></p>
      </div>
    </div>
    <button id="register_button">登録</button>
    <div id="spinner"></div>
[/code]

状態を管理しないFlux的な例  
<https://jsfiddle.net/gocho/mpbpmzgy/4/>

[code]

    $(function($) {
      'use strict';
    
      const $input_components = $('.input_component');
      const $register_button = $('#register_button');
      const $register_button_ready = $('#register_button_ready');
      const $spinner = $('#spinner');
    
      const dbMock = {
        item_cd: {
          1: {
            item_name: 'item 1',
            price: 550
          },
          2: {
            item_name: 'item 2',
            price: 700
          }
        },
        some_key1: {
          1: {
            value1: 'value1'
          },
          2: {
            value1: 'value2'
          }
        },
        some_key2: {
          1: {
            value2: 10
          },
          2: {
            value2: 20
          }
        }
      };
      const errorValue = {
        error: 'error'
      };
    
      // component
      const inputComponents = $input_components.toArray()
        .map(function(el, ind) {
          const $root = $(el);
          const $input = $root.find('.input_element');
          const $ajax_result = $root.find('.ajax_result');
          const name = el.dataset.name;
          return {
            $root,
            $input,
            $ajax_result,
            name
          };
        }).reduce((accum, curr) => (accum[curr.name] = curr, accum), {});
    
      // はじめはrejectedにしておく
      // 初期入力値があるなら、起動時に1度validation処理をする
      const myFormPromises = {
        item_cd: $.Deferred().reject().promise(),
        some_key1: $.Deferred().reject().promise(),
        some_key2: $.Deferred().reject().promise()
      };
    
      // mini pubsub
      const MYFORM_EVENT_NAMES = {
        DO_AJAX: 'DO_AJAX',
        UPDATE_REGISTER_BUTTON_READY: 'UPDATE_REGISTER_BUTTON_READY',
        SHOW_SPINNER: 'SHOW_SPINNER',
        HIDE_SPINNER: 'HIDE_SPINNER',
      };
      const MYFORM_EVENTS = {
        trigger: function(eventName, obj) {
          return this.actions(eventName, obj);
        },
        actions: function(eventName, obj) {
          switch (eventName) {
            // CUSTOM_EVENTS 内で非同期処理を記述すると、CUSTOM_EVENTS が CUSTOM_EVENTS を trigger したくなる
            // 無限ループ、イベント連鎖の根元が分からない、発火の順序問題 など、非常に大きな問題を抱えることになる
            // 可能な限り、非同期処理の記述は避ける
            // 本物(facebook提供)の Flux でも難しい問題だと認識されている
            // redux-saga など、小難しいライブラリを使わないと解消できないので、非常にしんどい
            // 
            // とりあえず、必ずPromise返すようにして、非同期処理は外でさせるようにした
            case MYFORM_EVENT_NAMES.DO_AJAX:
              const inputComponent = obj.inputComponent;
              return new Promise((resolve, reject) => {
                $.ajax({
                  url: '/echo/json/',
                  method: 'POST',
                  data: {
                    json: JSON.stringify(obj.ajaxResponse),
                    delay: 1
                  },
                }).then(function(response, status, jqXHR) {
                  JSON.stringify(response) !== JSON.stringify(errorValue) ?
                    resolve(response) :
                    reject(response);
                });
              });;
    
            case MYFORM_EVENT_NAMES.SHOW_SPINNER:
              obj.$spinner.html('ajax中...');
              return Promise.resolve();
    
            case MYFORM_EVENT_NAMES.HIDE_SPINNER:
              obj.$spinner.html('');
              return Promise.resolve();
    
            case MYFORM_EVENT_NAMES.UPDATE_REGISTER_BUTTON_READY:
              const allPromise = Promise.all(Object.values(obj.myFormPromises));
              allPromise.then(
                () => obj.$register_button_ready.html('validate ok'),
                () => obj.$register_button_ready.html('validate ng')
              );
              return allPromise;
    
            default:
              return Promise.reject();
          }
        }
      };
    
    
      (function BindEvent() {
    
        $input_components.on('blur', '.input_element', function(ev) {
          const inputComponent = inputComponents[ev.delegateTarget.dataset.name];
          const name = inputComponent.name;
          const ajaxResponse = dbMock[name][inputComponent.$input.val()] || errorValue;
    
          MYFORM_EVENTS.trigger(
            MYFORM_EVENT_NAMES.SHOW_SPINNER, {
              $spinner
            }
          );
    
          // 他の処理箇所と Promise の値を共有するため、上位スコープの変数に代入
          // 状態変化部分なので、バグの温床になりやすい
          myFormPromises[name] = MYFORM_EVENTS.trigger(
            MYFORM_EVENT_NAMES.DO_AJAX, {
              inputComponent,
              ajaxResponse,
              $spinner
            }
          );
          myFormPromises[name].then(
            response => {
              console.info('ajax succeeded');
              MYFORM_EVENTS.trigger(
                MYFORM_EVENT_NAMES.HIDE_SPINNER, {
                  $spinner
                }
              );
              inputComponent.$ajax_result.html(JSON.stringify(response));
            },
            response => {
              console.info('ajax failed');
              MYFORM_EVENTS.trigger(
                MYFORM_EVENT_NAMES.HIDE_SPINNER, {
                  $spinner
                }
              );
              inputComponent.$ajax_result.html(JSON.stringify(response));
            }
          );
    
          MYFORM_EVENTS.trigger(
            MYFORM_EVENT_NAMES.UPDATE_REGISTER_BUTTON_READY, {
              $register_button_ready,
              myFormPromises
            }
          );
        });
    
    
        $register_button.on('click', function(ev) {
          ev.preventDefault();
    
          MYFORM_EVENTS.trigger(
            MYFORM_EVENT_NAMES.UPDATE_REGISTER_BUTTON_READY, {
              $register_button_ready,
              myFormPromises
            }
          ).then(
            // promises が全て resolved になったら成功時処理
            ([item_cd, some_key1, some_key2]) => {
              console.info('validation ok, do sumit');
              console.info(item_cd, some_key1, some_key2);
            },
            // promises が1つでも rejected なら、失敗時処理
            rejected => {
              console.info('validation ng, do not submit');
            }
          );
        });
    
      })();
    
      // 起動時の処理
      // 初期入力値が有るなら必要
      (function StartUp() {
    
        Object.entries(inputComponents)
          .filter(([name, inputComponent]) => inputComponent.$input.val())
          .forEach(([name, inputComponent]) => {
    
            const ajaxResponse = dbMock[name][inputComponent.$input.val()] || errorValue;
    
            MYFORM_EVENTS.trigger(
              MYFORM_EVENT_NAMES.SHOW_SPINNER, {
                $spinner
              }
            );
    
            // 他の処理箇所と Promise の値を共有するため、上位スコープの変数に代入
            // 状態変化部分なので、バグの温床になりやすい
            myFormPromises[name] = MYFORM_EVENTS.trigger(
              MYFORM_EVENT_NAMES.DO_AJAX, {
                inputComponent,
                ajaxResponse,
                $spinner
              }
            );
            myFormPromises[name].then(
              response => {
                console.info('ajax succeeded');
                MYFORM_EVENTS.trigger(
                  MYFORM_EVENT_NAMES.HIDE_SPINNER, {
                    $spinner
                  }
                );
                inputComponent.$ajax_result.html(JSON.stringify(response));
              },
              response => {
                console.info('ajax failed');
                MYFORM_EVENTS.trigger(
                  MYFORM_EVENT_NAMES.HIDE_SPINNER, {
                    $spinner
                  }
                );
                inputComponent.$ajax_result.html(JSON.stringify(response));
              }
            );
    
            MYFORM_EVENTS.trigger(
              MYFORM_EVENT_NAMES.UPDATE_REGISTER_BUTTON_READY, {
                $register_button_ready,
                myFormPromises
              }
            );
          });
    
      })();
    
    });
[/code]

[code]

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div>
      <div class="input_component" data-name="item_cd">item_cd:
        <input class="input_element" type="text" value="1" />
        <p class="ajax_result"></p>
      </div>
      <div class="input_component" data-name="some_key1">some_key1:
        <input class="input_element" type="text" />
        <p class="ajax_result"></p>
      </div>
      <div class="input_component" data-name="some_key2">some_key2:
        <input class="input_element" type="text" />
        <p class="ajax_result"></p>
      </div>
    </div>
    <button id="register_button">登録</button>
    <p id="register_button_ready"></p>
    <div id="spinner"></div>
[/code]

  
jQueryのpromiseはヘンなので、ちょっとぎこちなくなってしまっています

今時のライブラリは Promise / async function に対応しているので、もっとスマートに記述できると思います

async function  
<https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function>


CC BY-SA 4.0
2017-03-21T12:51:04.603
33444
2019-12-13T18:06:16.893
2019-12-13T18:06:16.893
32986
9796
33438
answer