使用SpringBoot、thymeleaf和htmx,特别是在html表单中使用hx-put:对象未在请求中传递

问题描述 投票:0回答:1

我正在尝试使用 hx-put 来处理表单上的更新。我的项目使用 Spring 3.3.2、thymeleaf-extras-springsecurity6 和 htmx 2.0.3 (webjars)。

这是我用于处理更新的百里香片段:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<div th:fragment="email-form(email, personId)">
    <h4 th:text="${edit} ? 'Edit Email' : 'Add Email'">add or edit Email</h4>
    <form th:action="@{/person/{personId}/email/save(personId=${personId})}"
          th:object="${email}"
          method="post">
        <input type="hidden" th:field="*{emailId}"/>

        <div class="mb-3">
            <label for="emailType" class="form-label">Type</label>
            <select id="emailType" th:field="*{type}" class="form-select">
                <option value="PERSONAL">Personal</option>
                <option value="WORK">Work</option>
                <option value="HOME">Home</option>
                <option value="SCHOOL">School</option>
                <option value="OTHER">Other</option>
            </select>
        </div>
        <div class="mb-3">
            <label for="email" class="form-label">Email Address</label>
            <input id="email" type="email" th:field="*{emailAddress}" class="form-control"
                   placeholder="[email protected]" required>
        </div>
        <div class="d-flex justify-content-end">
            <button type="button" class="btn btn-success me-2"
                    th:if="${edit}"
                    th:hx-put="@{/person/{personId}/email/update(personId=${personId})}"
                    hx-target="#email-parent"
                    hx-swap="innerHTML transition:true"
                    th:attr="data-csrf-header=${_csrf.headerName}, data-csrf-token=${_csrf.token}, data-email-id=${email.emailId"
                    hx-trigger="click">
                Update Email
            </button>

            <button type="button" class="btn btn-success me-2"
                    th:if="${!edit}"
                    th:hx-post="@{/person/{personId}/email/save(personId=${personId})}"
                    hx-target="#email-parent"
                    hx-swap="innerHTML transition:true"
                    hx-trigger="click">
                Save
            </button>
            <button type="button" class="btn btn-secondary"
                    th:hx-get="@{/person/view/{personId}(personId=${personId})}"
                    hx-target="body" hx-swap="outerHTML transition:true">Cancel
            </button>
        </div>
    </form>
</div>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<div th:fragment="email-form(email, personId)">
    <h4 th:text="${edit} ? 'Edit Email' : 'Add Email'">add or edit Email</h4>
    <form th:action="@{/person/{personId}/email/save(personId=${personId})}"
          th:object="${email}"
          method="post">
        <input type="hidden" th:field="*{emailId}"/>

        <div class="mb-3">
            <label for="emailType" class="form-label">Type</label>
            <select id="emailType" th:field="*{type}" class="form-select">
                <option value="PERSONAL">Personal</option>
                <option value="WORK">Work</option>
                <option value="HOME">Home</option>
                <option value="SCHOOL">School</option>
                <option value="OTHER">Other</option>
            </select>
        </div>
        <div class="mb-3">
            <label for="email" class="form-label">Email Address</label>
            <input id="email" type="email" th:field="*{emailAddress}" class="form-control"
                   placeholder="[email protected]" required>
        </div>
        <div class="d-flex justify-content-end">
            <button type="button" class="btn btn-success me-2"
                    th:if="${edit}"
                    th:hx-put="@{/person/{personId}/email/update(personId=${personId})}"
                    hx-target="#email-parent"
                    hx-swap="innerHTML transition:true"
                    th:attr="data-csrf-header=${_csrf.headerName}, data-csrf-token=${_csrf.token}"
                    hx-trigger="click">
                Update Email
            </button>

            <button type="button" class="btn btn-success me-2"
                    th:if="${!edit}"
                    th:hx-post="@{/person/{personId}/email/save(personId=${personId})}"
                    hx-target="#email-parent"
                    hx-swap="innerHTML transition:true"
                    hx-trigger="click">
                Save
            </button>
            <button type="button" class="btn btn-secondary"
                    th:hx-get="@{/person/view/{personId}(personId=${personId})}"
                    hx-target="body" hx-swap="outerHTML transition:true">Cancel
            </button>
        </div>
    </form>
</div>
</html>

此表单用于添加新电子邮件和更新现有电子邮件。 正在尝试使用 hx-put 进行更新。

在我的 user-layout.html 中,我包含一个 htmx.configRequest 调用:

<!DOCTYPE html>
<html lang="en"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
    <meta charset="UTF-8">
    <title>Records Management - User Dashboard</title>
    <link rel="stylesheet" th:href="@{/css/style.css}"/>
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.3.3/css/bootstrap.min.css}"/>
    <link rel="stylesheet" th:href="@{/webjars/font-awesome/6.5.2/css/all.min.css}"/>
</head>
<body class="gradient-custom" hx-boost="true">
<header th:insert="~{fragments/header :: userHeader}"></header> <!-- User-specific header -->

<div class="container">
    <div th:insert="~{fragments/messages :: messages}"></div>
    <div layout:fragment="content"></div> <!-- Content will be injected here -->
</div>

<footer th:insert="~{fragments/footer :: userFooter}"></footer>

<script th:src="@{/webjars/bootstrap/5.3.3/js/bootstrap.bundle.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/htmx.org/2.0.3/dist/htmx.min.js}"></script>
<script>
    document.body.addEventListener('htmx:configRequest', function(event) {
        const target = event.target;
        const csrfHeader = target.getAttribute('data-csrf-header');
        const csrfToken = target.getAttribute('data-csrf-token');

        // If CSRF attributes are found, add them to the request headers
        if (csrfHeader && csrfToken) {
            event.detail.headers[csrfHeader] = csrfToken;
        }
    });
</script>
</body>
</html>

需要此代码来解决 Spring Security 的 CORS 要求。

我的控制器代码方法是:

@PutMapping(value = "/person/{personId}/email/update")
    public String updateEmail(@ModelAttribute Email email,
                              @PathVariable("personId") Long personId,
                              Model model) {

        // Verify that 'email' here contains the ID correctly and not the email string.
        LOGGER.info("Updating email for personId: {}, emailId: {}", personId, email.getEmailId());
        Person person = personService.getPersonById(personId);

        emailService.saveOrUpdateEmail(person, email);
        model.addAttribute("emails", person.getEmails());
        model.addAttribute("personId", personId);

        return "person/emails-info :: emails-info";
//        return "person/email-item :: email-item";
    }

我在提交表单时看到的是控制器中的

@ModelAttribute Email email
已实例化,但对象中的所有属性都是空的。 当我查看“网络”选项卡中的浏览器流量时,我看到了有效负载:

_csrf: aemCvGmY8p2U6i0t2sysF7dEPI5eZPLq-NAbPY6WC-bNKYycD4zghFCgyq25iEsbuOGYc9UmEbduVpTHyuAuW7f0b97-HLyu
emailId: 2
type: WORK
emailAddress: [email protected]

但是,在日志中,emailId 为空,type 为空,emailaddress 为空。 我开始认为 hx-put 不适用于 Spring boot/thymeleaf。 有什么想法吗?

spring-boot spring-security thymeleaf htmx
1个回答
0
投票

我发现了两件事:

  1. 我发现,默认情况下,HTMX 使用
    application/x-www-form-urlencoded
    进行 POST 请求,但对于其他 HTTP 方法(如 PUT),它可能默认为 text/plain。 不知道为什么,但只是观察到确实如此。
  2. 为了解决这个问题,我将
    hx-encoding="application/x-www-form-urlencoded"
    添加到了我的按钮中。这确保了我的 PUT 请求不会打开内容类型。
    https://htmx.org/attributes/hx-encoding/
  3. 我还发现我必须将以下内容添加到我的 WebConfig.java 中:
@Bean
    public FormContentFilter formContentFilter() { //added with hx-pu
        return new FormContentFilter();
    }

深入研究 FormContentFilter 的文档,我发现:

解析 HTTP PUT、PATCH 和 DELETE 请求的表单数据的过滤器 并将其公开为 Servlet 请求参数。默认情况下,Servlet 规范仅要求 HTTP POST 这样做。

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/FormContentFilter.html

我的百里香叶片段的最终状态如下所示:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<div th:fragment="email-form(email, personId)">
    <h4 th:text="${edit} ? 'Edit Email' : 'Add Email'">add or edit Email</h4>
    <form th:action="@{/person/{personId}/email/save(personId=${personId})}"
          th:object="${email}"
          method="post">
        <input type="hidden" th:field="*{emailId}"/>
        <div class="mb-3">
            <label for="emailType" class="form-label">Type</label>
            <select id="emailType" th:field="*{type}" class="form-select">
                <option value="PERSONAL">Personal</option>
                <option value="WORK">Work</option>
                <option value="HOME">Home</option>
                <option value="SCHOOL">School</option>
                <option value="OTHER">Other</option>
            </select>
        </div>
        <div class="mb-3">
            <label for="email" class="form-label">Email Address</label>
            <input id="email" type="email" th:field="*{emailAddress}" class="form-control"
                   placeholder="[email protected]" required>
        </div>
        <div class="d-flex justify-content-end">
            <button type="button" class="btn btn-success me-2"
                    th:if="${edit}"
                    th:hx-put="@{/person/{personId}/email/update(personId=${personId})}"
                    hx-target="#email-parent"
                    hx-swap="innerHTML transition:true"
                    th:attr="data-csrf-header=${_csrf.headerName}, data-csrf-token=${_csrf.token}"
                    hx-trigger="click"
                    hx-encoding="application/x-www-form-urlencoded">
                Update Email
            </button>

            <button type="button" class="btn btn-success me-2"
                    th:if="${!edit}"
                    th:hx-post="@{/person/{personId}/email/save(personId=${personId})}"
                    hx-target="#email-parent"
                    hx-swap="innerHTML transition:true"
                    hx-trigger="click">
                Save
            </button>
            <button type="button" class="btn btn-secondary"
                    th:hx-get="@{/person/view/{personId}(personId=${personId})}"
                    hx-target="body" hx-swap="outerHTML transition:true">Cancel
            </button>
        </div>
    </form>
</div>
</html>

我的控制器方法:

@PutMapping(value = "/person/{personId}/email/update")
    public String updateEmail(@ModelAttribute Email email,
                              @PathVariable("personId") Long personId,
                              Model model) {

        // Verify that 'email' here contains the ID correctly and not the email string.
        LOGGER.info("Updating email for personId: {}, emailId: {}", personId, email.getEmailId());
        LOGGER.info("email address = {}", email.getEmailAddress());

        Person person = personService.getPersonById(personId);

        emailService.saveOrUpdateEmail(person, email);
        model.addAttribute("emails", person.getEmails());
        model.addAttribute("personId", personId);

        return "person/emails-info :: emails-info";
//        return "person/email-item :: email-item";
    }

花了很多天才弄清楚这一点。 希望它对其他人有帮助!

© www.soinside.com 2019 - 2024. All rights reserved.