BeginnerEngineerBlog
中の人
中の人

【jQuery】ajax通信を入力終了または数秒後に実行

公開: 2022-03-15 23:10
更新: 2023-04-09 00:14
2202
jQuery ajax setTimeout php javascript
一定時間後に一度だけajax通信したい時や、文字入力中はajax通信したくない、または何文字以上入力されたら通信したいという時てありますよね。個人的にしっくりきたコードが書けた気がするのでメモとして紹介します。似たような実装をしたい方がいたら参考にしてください。

こんにちは!

中の人です!

最近(といっても結構前から)javascript界隈は、ReactやVueやTypeScriptやなんやかんやと新しい技術が出てきたので、今更jQueryかいって感じる方もいるかもしれませんが、うるさい黙れjQuery普通に便利だし俺のメモじゃいということで、タイトル通りajax通信を時間差で実行する処理をjQueryで実装する方法を紹介したいと思います!

また、全体のコードはgithubにプッシュしていますので、気になる方はチェックしてみてください。
(ちょっとサーバー側の処理をフレームワークの感じを意識して作ってあって変な処理になってますが、趣味で書いたコードなので気にしないでください。)


仕様


  1. 入力フォームに文字を入力
  2. ajaxでサーバーに入力した値を送信する
  3. 送信した値が含まれる文字列を返す
  4. 結果一覧を表示する
  5. ※ 連続入力の場合はajax通信を行わず、一定時間入力がない場合に1度だけajax通信を実行する
  6. ※ ajax通信中に文字入力があった場合は実行中のajax通信を中止する
  7. ※ 特定の文字数以上入力があった場合にajax通信をする

という仕様にしたいと思います。


完成イメージ


入力してから一定時間後に一回ajax通信をした場合


タイマーが0になった時に一度だけajax通信処理がおこなわれます。

入力後すぐにajax通信するけど、通信中に入力があった場合通信を中止した場合


ちょっとわかりづらいですが、二つ目のフォームは値が入力されたら即ajaxを実行するようにしています。また、通信が成功した場合は検索結果を表示しっぱなしにして、中断された場合、検索結果をリセットするようにしています。
右側のコンソールには、ajax通信が失敗(中断)した際に失敗時の値を出力する処理をいれてますので、見ての通りちゃんと中断されていますね。

ということで実装の方を紹介します!


code


フロント
(20220317追記
ajaxSetTimeoutId関数とdoAjax関数の引数に漏れがあったので修正しました。
)
📁 root/view/index.php

<?php
global $params;
$post_path = Route::createRouteUrl('/post');
?>

<div>
    <label for="text1">テキスト1</label>
    <input type="text" id="text1" name="text" data-post_path="<?php echo $post_path; ?>">
</div>
<div>
    <label for="text2">テキスト2</label>
    <input type="text" id="text2" name="text" data-post_path="<?php echo $post_path; ?>">
</div>
<div style="display: flex;">
    <div>タイマー:&emsp;</div>
    <div id="timer">
        3<!--insert_timer_js-->
    </div>
</div>

<ul id="insert_js">
    <!--insert_js-->
</ul>
<script>
    $(function () {
        // 入力後通信までの秒数
        const timer_default_count = 3;
        // 入力文字数最低文字数
        const text_length = 3;
        // ajaxの結果受け取り用変数
        let jqxhr;
        // サーバーから返された値を表示する要素
        let insert_elem = $('#insert_js');
        // inputフォーム1
        let text_elem1 = $('#text1');
        // inputフォーム2
        let text_elem2 = $('#text2');
        // 入力後通信までの秒数を表示する要素
        let timer_elem = $('#timer');
        // setTimeoutのid用変数
        let timeout_id = null;
        // 入力後通信までの秒数を動的に表示するための変数
        let timer_count = timer_default_count;
        // setIntervalのid用変数
        let interval_id = null;
        // text_formの1と2を区別する用変数
        let form_type = null;

        // text_formの1に入力があったら(時間差で通信するフォーム)
        text_elem1.on('keyup', function () {
            // setTimeoutが実行中だったらカウントダウンをストップする
            ajaxClearTimeoutId();
            // 入力フォームの値をtextにセット
            let text = $(this).val();
            // 送信先のurlをurlにセット
            let url = $(this).data('post_path');
            // textが空もしくは規定の文字数以下の場合は
            if (
                text === '' ||
                text === null ||
                text.length < text_length
            ) {
                // ajax通信するまでのカウントダウンアニメーションをリセットする
                resetTimerCount();
                // 検索結果表示場所をクリアする
                cleanUpResult();
                return false;
            }
            // サーバーに送るパラメータをセットするFormDataをnew
            let data = new FormData();
            // FormDataのインスタンスにtextをセット
            data.append('text', text);
            // 時間差でajax通信するか判断するためform_typeに1をセット
            form_type = 1;
            // setTimeout関数で時間差でajax通信を実行する
            ajaxSetTimeoutId(url, data, form_type);
        });

        // text_formの2に入力があったら(時間差を使用しないフォーム)
        // 途中まで上記に同じ
        text_elem2.on('keyup', function () {
            ajaxClearTimeoutId();
            let text = $(this).val();
            let url = $(this).data('post_path');
            if (
                text === '' ||
                text === null ||
                text.length < text_length
            ) {
                resetTimerCount();
                // 時間差を使用しないので分かりやすくするためにcleanUpResult関数は実行しない
                return false;
            }
            let data = new FormData();
            data.append('text', text);
            form_type = 2;
            // 時間差を使用しないのでそのままajax通信処理を実行
            doAjax(url, data, form_type);
        });

        // 検索結果表示を削除する
        function cleanUpResult () {
            insert_elem.empty();
        }

        // ajax通信を時間差で実行する
        function ajaxSetTimeoutId (url, data, form_type) {
            // ajax通信までの時間をカウントダウンする
            countDown();
            // setTimeoutの返り値のidをtimeout_idにセット
            timeout_id = setTimeout(function () {
                // ajax通信を実行
                doAjax(url, data, form_type);
            }, 3000);// 3秒後に実行
        }

        // ajax通信をするためのsetTimeoutが実行中の場合、ストップする
        function ajaxClearTimeoutId () {
            if (timeout_id !== null) {
                clearTimeout(timeout_id);
                // timeout_idをリセット
                timeout_id = null;
            }
        }

        // ajax通信処理
        function doAjax (url, data, form_type) {
            // ajax通信中の場合ajax通信をストップする
            if (jqxhr) {
                jqxhr.abort();
            }
            // ajax通信処理の返り値をjqxhrにセット(↑これで上記の条件分岐ができる)
            jqxhr = $.ajax({
                type: 'POST',
                url: url,
                data: data,
                processData: false,
                contentType: false,
                dataType: 'json',
                timeout: 10000,
            }).done(function (data) {
                // 分かりやすくするため、text_form1の場合だけ検索結果をリセットする
                if (form_type === 1) {
                    cleanUpResult();
                }
                // 検索結果を表示する
                for (let i = 0;data.length > i;++i) {
                    insert_elem.append('<li>' + data[i] + '</li>');
                }
            }).fail(function (x,s,e) {
                // 通信失敗時は検索結果をリセットする
                cleanUpResult();
                console.log(x,s,e);
            });
        }

        // 入力からajax通信するまでの時間を表示する処理
        function countDown () {
            timer_count = timer_default_count;
            ajaxClearIntervalId();
            timer_elem.empty();
            timer_elem.append(timer_count);
            interval_id = setInterval(function () {
                if (timer_count <= 0) {
                    return false;
                }
                timer_count -= 1;
                timer_elem.empty();
                timer_elem.append(timer_count);

            }, 1000);
        }

        // 上記のsetInterval関数をストップしてタイマーの表示を初期値にリセットする処理
        function resetTimerCount () {
            ajaxClearIntervalId();
            timer_count = timer_default_count;
            timer_elem.empty();
            timer_elem.append(timer_count);
        }

        // タイマーをストップする処理
        function ajaxClearIntervalId () {
            if (interval_id !== null) {
                clearInterval(interval_id);
            }
        }
    });
</script>


サーバー
📁 root/api/post.php

<?php
global $http_method;
global $params;
// ajax通信ではなかったら404を表示して処理終了
if (
    !isset($_SERVER['HTTP_X_REQUESTED_WITH']) ||
    strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest'
) {
    echo '404';
    die;
}

// postメソッドではない場合エラーコードを返す
if (strtolower($http_method) !== 'post') {
    header('HTTP/1.1 500 Internal Server Bad Request');
    header('Content-Type: application/json; charset=UTF-8');
    die(json_encode(array('message' => 'ERROR', 'code' => 500)));
}

// 送られてきたtextパラメータを$textにセット
$text = null;
if (isset($params['text'])) {
    $text = $params['text'];
}

// 検索用文字の配列
$words = [
    'あ',
    'あい',
    'あいう',
    'あいうえ',
    'あいうえお',
    'か',
    'かき',
    'かきく',
    'かきくけ',
    'かきくけこ',
];

// $textの文字が入っている文字列を取得して$responseに格納
$response = [];
if (!is_null($text)) {
    foreach ($words as $word) {
        if (strpos($word, $text) !== false) {
            $response[] = $word;
        }
    }
}

// 検索結果を返して処理終了
header("Content-type: application/json; charset=UTF-8");
echo json_encode($response);
exit;


こんな感じです。


解説


簡単にですが、ajaxする処理を発火させるevent
        text_elem2.on('keyup', function () {
            ajaxClearTimeoutId();
            ........
が実行されたら、setTimeoutのidを削除してajax通信するまでのタイマー処理を中断します。

また、ajax通信中の時にeventが発火したら

        // ajax通信処理
        function doAjax (url, data, form_type) {
            // ajax通信中の場合ajax通信をストップする
            if (jqxhr) {
                jqxhr.abort();
            }
            // ajax通信処理の返り値をjqxhrにセット(↑これで上記の条件分岐ができる)
            jqxhr = $.ajax({
            .......

            if (jqxhr) {
                jqxhr.abort();
            }

$.ajaxの返り値を持っている場合、abort()関数で処理を中断しています。

また特定の文字数の判定は

            // textが空もしくは規定の文字数以下の場合は
            if (
                text === '' ||
                text === null ||
                text.length < text_length
            ) {
                // ajax通信するまでのカウントダウンアニメーションをリセットする
                resetTimerCount();
                // 検索結果表示場所をクリアする
                cleanUpResult();
                return false;
            }

こんな感じで入力された値が空かどうかの判定の際に一緒にしています。

その他はコメントを参照してください。


実際は


今回わかりやすくカウントダウンの処理も一緒に実装しているので、codeがわちゃわちゃしてわかりにくいと思いますので、カウントダウンなし、2個目のフォームもなしの場合は

<?php
global $params;
$post_path = Route::createRouteUrl('/post');
?>

<div>
    <label for="text">テキスト1</label>
    <input type="text" id="text1" name="text" data-post_path="<?php echo $post_path; ?>">
</div>

<ul id="insert_js">
    <!--insert_js-->
</ul>
<script>
    $(function () {
        // 入力後通信までの秒数
        const timer_default_count = 3;
        // 入力文字数最低文字数
        const text_length = 3;
        // ajaxの結果受け取り用変数
        let jqxhr;
        // サーバーから返された値を表示する要素
        let insert_elem = $('#insert_js');
        // inputフォーム1
        let text_elem1 = $('#text1');
        // setTimeoutのid用変数
        let timeout_id = null;

        // text_formの1に入力があったら(時間差で通信するフォーム)
        text_elem1.on('keyup', function () {
            // setTimeoutが実行中だったらカウントダウンをストップする
            ajaxClearTimeoutId();
            // 入力フォームの値をtextにセット
            let text = $(this).val();
            // 送信先のurlをurlにセット
            let url = $(this).data('post_path');
            // textが空もしくは規定の文字数以下の場合は
            if (
                text === '' ||
                text === null ||
                text.length < text_length
            ) {
                // 検索結果表示場所をクリアする
                cleanUpResult();
                return false;
            }
            // サーバーに送るパラメータをセットするFormDataをnew
            let data = new FormData();
            // FormDataのインスタンスにtextをセット
            data.append('text', text);
            // setTimeout関数で時間差でajax通信を実行する
            ajaxSetTimeoutId(url, data);
        });

        // 検索結果表示を削除する
        function cleanUpResult () {
            insert_elem.empty();
        }

        // ajax通信を時間差で実行する
        function ajaxSetTimeoutId (url, data) {
            // setTimeoutの返り値のidをtimeout_idにセット
            timeout_id = setTimeout(function () {
                // ajax通信を実行
                doAjax(url, data);
            }, timer_default_count * 1000);// 3秒後に実行
        }

        // ajax通信をするためのsetTimeoutが実行中の場合、ストップする
        function ajaxClearTimeoutId () {
            if (timeout_id !== null) {
                clearTimeout(timeout_id);
                // timeout_idをリセット
                timeout_id = null;
            }
        }

        // ajax通信処理
        function doAjax (url, data) {
            // ajax通信中の場合ajax通信をストップする
            if (jqxhr) {
                jqxhr.abort();
            }
            // ajax通信処理の返り値をjqxhrにセット(↑これで上記の条件分岐ができる)
            jqxhr = $.ajax({
                type: 'POST',
                url: url,
                data: data,
                processData: false,
                contentType: false,
                dataType: 'json',
                timeout: 10000,
            }).done(function (data) {
                cleanUpResult();
                // 検索結果を表示する
                for (let i = 0;data.length > i;++i) {
                    insert_elem.append('<li>' + data[i] + '</li>');
                }
            }).fail(function (x,s,e) {
                // 通信失敗時は検索結果をリセットする
                cleanUpResult();
                console.log(x,s,e);
            });
        }
    });
</script>


このくらいのcode量ですかね。

まぁ、もっと簡潔に書けそうな気もしますが、とりあえず仕様は満たせてるんでよしとしましょう!

ということで紹介は以上になります!


終わりに


このような実装をするとき、どうやったっけてなって毎回ネット記事を漁るので、ひとまず自分の中の最適解的なものをメモしておこうと思って記事にしました。

また冒頭でも書きましたがgithubにプッシュしていますので、実際に動かして見たい方はcloneしてみてください。
(Docker関係のファイルも一緒なので環境がある方はすぐに実行できると思います)

参考になれば幸いです!

ではまた!
0
0
0
0
通信エラーが発生しました。
【広告】
似たような記事