非同期処理の方法

備忘録的な感じです。初心者ですので間違っている箇所があるかもしれません。
お手柔らかにお願いいたします。
## Ajaxとは
Asynchronous JavaScript + XMLの略。
JavaScriptとXMLを使用して非同期でサーバとの通信を行うことができる。
現在はXMLよりもJSON形式でレスポンスを取得することが主流である。
## Ajax + jQueryのサンプルプログラム
zipcoda.netが提供するWeb APIを使用して郵便番号で住所を取得する。
```html:sample.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form>
        郵便番号：<input id="postal_code" type="text" size="7">
        <button id="fetch_address_button" type="button">住所を取得する</button>
        住所：<input id="address" type="text" size="50">
    </form>
    <script src="https://code.jQuery.com/jQuery-3.7.1.min.js"></script>
    <script src="js/sample.js"></script>
</body>
</html>
```
```js:sample.js
"use strict"

$(function() {
    // 住所を取得するボタンのクリックイベントを設定する。
    $('#fetch_address_button').on('click', function() {
        $.ajax({
            // 郵便番号検索APIのエンドポイントURL
            url: 'https://zipcoda.net/api',
            // GETメソッドでリクエストを送信する。
            type: 'GET',
            // レスポンスの形式はJSONPを指定する。
            dataType: 'jsonp',
            // 送信するリクエストパラメータ（郵便番号）
            data: {
                zipcode:$('#postal_code').val()
            },
            // 非同期処理を行うための設定
            async: true
        }).done(function(data) {
            // data = レスポンスデータ
            // 検索に成功した場合はHTMLに結果を反映する。
            // 取得したデータをJSON形式でコンソールに表示する。
            console.dir(JSON.stringify(data));
            // 住所欄に取得した住所をセットする。
            $('#address').val(data.items[0].address);
        }).fail(function(XMLHttpRequest, textStatus, errorThrown) {
            // 検索に失敗した場合はアラートで通知しててエラー情報をコンソールに表示する。
            alert('正しい結果を得ることができませんでした。');
            console.log('XMLHttpRequest:' + XMLHttpRequest.status);
            console.log('textStatus:' + textStatus);
            console.log('errorThrown:' + errorThrown.message);
        })
    })
});
```
## Ajax + JavaScriptのサンプルプログラム
口座一覧画面で口座情報を非同期処理で編集する。
※一部コードは省略しております。
※練習のために実装したので非同期処理にする有効性は考慮していません。
※誤りが含まれている可能性があります。申し訳ございません。
```html:accountList.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>口座一覧画面</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="/css/account.css">
</head>
<body>
    <h1>口座一覧画面</h1>
    <form id="edit_account_form">
        <div th:each="account : ${accountList}">
            <p>
                <span>口座名：</span>
                <span th:id="'accountName_' + ${account.id}" th:text="${account.name}"></span>
                <input type="text" th:id="'editName_' + ${account.id}" name="name" th:value="${account.name}" th:data-account-id="${account.id}" required maxlength="50" style="display:none;">
                <span class="error" th:id="'name_error_' + ${account.id}" th:data-account-id="${account.id}"></span>
            </p>
            <p>
                <span>口座残高：</span>
                <span th:id="'accountBalance_' + ${account.id}" th:text="${account.balance}"></span>
                <input type="text" th:id="'editBalance_' + ${account.id}" name="balance" th:value="${account.balance}" th:data-account-id="${account.id}" required pattern="[0-9]+" style="display:none;">
                <span>円</span>
                <span class="error" th:id="'balance_error_' + ${account.id}" th:data-account-id="${account.id}"></span>
            </p>
            <p>
                <button type="button" th:id="'edit_button_' + ${account.id}" th:data-account-id="${account.id}">編集</button>
                <button type="submit" th:id="'save_button_' + ${account.id}" th:data-account-id="${account.id}" style="display:none;">保存</button>
                <button>削除</button>
            </p>
        </div>
    </form>
    <div>
        <a href="/account/toRegister">口座登録</a>
    </div>
    <form th:action="@{/account/toMyPage}">
        <button>戻る</button>
    </form>
    <script src="/js/account.js"></script>
</body>
</html>
```
```js.account.js
"use strict";

document.addEventListener("DOMContentLoaded", function() {
    const editButtons = document.querySelectorAll("[id^='edit_button_']");
    const saveButtons = document.querySelectorAll("[id^='save_button_']");

    editButtons.forEach(button => {
        button.addEventListener("click", function() {
            const accountId = this.getAttribute("data-account-id");
            editAccount(accountId);
        });
    });

    saveButtons.forEach(button => {
        button.addEventListener("click", function() {
            const accountId = this.getAttribute("data-account-id");
            if (preventProcess(accountId)) {
                saveAccount(accountId);
            }
        });
    });

    const names = document.querySelectorAll("[id^='editName_']");
    const balances = document.querySelectorAll("[id^='editBalance_']");

    names.forEach(name => {
        name.addEventListener("input", (event) => {
            const accountId = name.getAttribute("data-account-id");
            const id = "name_error_" + accountId;
            const nameError = document.getElementById(id);
            if (name.validity.valid) {
                nameError.textContent = "";
            } else {
                showNameError(name, nameError);
            }
        });
    });

    balances.forEach(balance => {
        balance.addEventListener("input", (event) => {
            const accountId = balance.getAttribute("data-account-id");
            const id = "balance_error_" + accountId;
            const balanceError = document.getElementById(id);
            if (balance.validity.valid) {
                balanceError.textContent = "";
            } else {
                showBalanceError(balance, balanceError);
            }
        });
    });
});

function editAccount(accountId) {
    document.getElementById('accountName_' + accountId).style.display = 'none';
    document.getElementById('editName_' + accountId).style.display = 'inline';
        
    document.getElementById('accountBalance_' + accountId).style.display = 'none';
    document.getElementById('editBalance_' + accountId).style.display = 'inline';
        
    document.getElementById('save_button_' + accountId).style.display = 'inline';
}

async function saveAccount(accountId) {
    try {
        const name = document.getElementById('editName_' + accountId).value;
        const balance = document.getElementById('editBalance_' + accountId).value;

        const response = await fetch("/account/edit", {
            method: "POST",
            body: JSON.stringify({id: accountId, name: name, balance: balance}),
            headers: {
                "Content-Type": "application/json",
            }
        });
        if (response.ok) {
            document.getElementById('accountName_' + accountId).innerText = name;
            document.getElementById('accountBalance_' + accountId).innerText = balance;

            document.getElementById('accountName_' + accountId).style.display = 'inline';
            document.getElementById('editName_' + accountId).style.display = 'none';
            document.getElementById('accountBalance_' + accountId).style.display = 'inline';
            document.getElementById('editBalance_' + accountId).style.display = 'none';
            document.getElementById('save_button_' + accountId).style.display = 'none';
        } else {
            console.error("エラーが発生しました。");
        }
    } catch (error) {
        console.error("エラーが発生しました。", error);
    }
}

function showNameError(name, nameError) {
    if (name.validity.valueMissing) {
        nameError.textContent = "口座名を入力してください。";
    } else if (name.validity.tooLong) {
        nameError.textContent = "口座名は50文字以内で入力してください。";
    }   
}

function showBalanceError(balance, balanceError) {
    if (balance.validity.valueMissing) {
        balanceError.textContent = "口座残高を入力してください。";
    } else if (balance.validity.patternMismatch) {
        balanceError.textContent = "口座残高には0以上の整数を入力してください。";
    }
}

function preventProcess(accountId) {
    const nameId = "editName_" + accountId;
    const name = document.getElementById(nameId);
    const balanceId = "editBalance_" + accountId;
    const balance = document.getElementById(balanceId);
    if (!name.validity.valid || !balance.validity.valid) {
        return false;
    }
    return true;
}
```
```java:accountController.java
@Controller
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private AccountService accountService;

    /**
     * 口座を編集する。
     * @param id 口座ID
     * @param name 口座名
     * @param balance 口座残高
     * @return ホーム画面
     */
    @PostMapping("/edit")
    @ResponseBody
    public Map<String, String> edit(@Valid @RequestBody AccountForm accountForm, BindingResult result) {
        Map<String, String> errorMap = new HashMap<>();
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                String fieldName = error.getField();
                String errorCode = error.getCode();
                String errorMessage = "";

                if (fieldName.equals("name")) {
                    switch (errorCode) {
                        case "NotBlank":
                            errorMessage = "口座名を入力してください。";
                            break;
                        case "Size":
                            errorMessage = "口座名は50文字以内で入力してください。";
                            break;
                    }
                } else if (fieldName.equals("balance")) {
                    switch (errorCode) {
                        case "NotBlank":
                            errorMessage = "口座残高を入力してください。";
                            break;
                        case "Pattern":
                            errorMessage = "口座残高には0以上の整数を入力してください。";
                            break;
                    }
                }
                errorMap.put(fieldName, errorMessage);
            }
            return errorMap;
        }

        Account account = new Account();
        account.setId(accountForm.getId());
        account.setName(accountForm.getName());

        try {
            int balance = Integer.parseInt(accountForm.getBalance());
            account.setBalance(balance);
        } catch (NumberFormatException exception) {
            errorMap.put("balance", "口座残高には0以上の整数を入力してください。");
            return errorMap;
        }
        accountService.edit(account);
        return errorMap;
    }
}
```