2

I want to build a web page that will have HTML form with a number of select components describing location: country, state, city and street. Choosing value in given select component will update options in 'downstream' selects.

I was able to implement it using HTML + Thymeleaf + Spring Boot + HTMX where htmx does hx-get, thymeleaf returns rendered fragments.

When whole page is generated (GET /test) select components do get correct name attribute names:

<span id="stateFragment">
      <label for="stateId">State</label>
        <select name="state" hx-get="test/cities" hx-target="#cityFragment" hx-swap="outerHTML" id="stateId" >
      </select>
      </span>

When requesting fragment (GET /test/states) th:field on selects, Thymeleaf removes name attribute value.:

<select name="" hx-get="test/cities" hx-target="#cityFragment" hx-swap="outerHTML" id="stateId">
          <option value="ca_state1">ca_state1</option>
          <option value="ca_state2">ca_state2</option>
      </select>

HTML/Thymeleaf looks like below:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.thymeleaf.org" layout:decorate="~{layouts/defaultLayout}">
<head>
  <title>Passing parameters</title>
</head>
<body>

<div layout:fragment="content" class="row g-6">
  <div class="col">
    <form th:action="@{/test/new}" th:object="${location}" method="post">
      <label for="countryId">Country</label>

      <select hx-get="test/states" hx-target="#stateFragment" hx-swap="outerHTML"
              th:field="*{country}" id="countryId">
        <option value="us">USA</option>
        <option value="ca">Canada</option>
      </select>

      <span th:fragment="stateFragment" id="stateFragment">
      <label for="stateId">State</label>
        <select hx-get="test/cities" hx-target="#cityFragment" hx-swap="outerHTML"
          th:field="*{state}" id="stateId">
          <option th:each="state : ${states}" th:value="${state}" th:text="${state}"></option>
      </select>
      </span>

      <span th:fragment="cityFragment" id="cityFragment">
        <label for="cityId">City</label>
        <select th:field="*{city}" id="cityId">
          <option th:each="city : ${cities}" th:value="${city}" th:text="${city}"></option>
        </select>
      </span>
      <button type="submit">Submit</button>
    </form>
  </div>
</div>
</body>
</html>

And the controller is:

@Controller
@RequestMapping("/test")
public class TestController {

    @GetMapping
    public String getLocation(Model model) {
        model.addAttribute("location", new LocationDto());
        model.addAttribute("states", List.of());
        model.addAttribute("cities", List.of());
        return "test/new";
    }

    @GetMapping("/states")
    public String getStates(Model model, @RequestParam String country) {
        model.addAttribute("state","");
        model.addAttribute("states", List.of(country + "_state1", country + "_state2"));
        return "test/new :: stateFragment";
    }

    @GetMapping("/cities")
    public String getCities(Model model, @RequestParam String state) {
        model.addAttribute("cities", List.of(state + "_city1", state + "_city2"));
        return "test/new :: cityFragment";
    }
}

How can I render fragment of page without losing name attribute value when rendering just a fragment?

1 Answer 1

2
@GetMapping("/states")
public String getStates(Model model, @RequestParam String country) {
    model.addAttribute("location", new LocationDto());
}

@GetMapping("/cities")
public String getCities(Model model, @RequestParam String state) {
    model.addAttribute("location", new LocationDto());
}
<span th:fragment="stateFragment" id="stateFragment" th:object="${location}">
        <label for="stateId">State</label>
        <select hx-get="test/cities" hx-target="#cityFragment" hx-swap="outerHTML"
                th:field="*{state}" id="stateId">
          <option th:each="state : ${states}" th:value="${state}" th:text="${state}"></option>
        </select>
      </span>

      <span th:fragment="cityFragment" id="cityFragment" th:object="${location}">
        <label for="cityId">City</label>
        <select th:field="*{city}" id="cityId">
          <option th:each="city : ${cities}" th:value="${city}" th:text="${city}"></option>
        </select>
      </span>

th:field looks up the hierarchy for a th:object, finds nothing, and fails silently. It can't generate the name attribute because it doesn't know what object "state" belongs to. The result is name="" You need to provide the th:object to the fragment every time it renders. In HTML need to wrap your fragments with the th:object tag so they are self-contained and don't rely on a parent. Fragment controller method have to add the LocationDto bean to the model, just like your main page controller does.

Sign up to request clarification or add additional context in comments.

1 Comment

Than you @Max, it worked! I used a bit different approach. Instead of adding th:object to each select i used variable expressions: ${...} <select th:field="${location.city}" id="cityId"> <option th:each="city : ${cities}" th:value="${city}" th:text="${city}"></option> </select>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.