htmx icon indicating copy to clipboard operation
htmx copied to clipboard

Problem applying focus() after afterWap

Open callor opened this issue 4 months ago • 1 comments

I am using the following code combining thymeleaf and HTMX

I wrote a code to receive a serial number from an input form, and when the focus is removed from this input form, a request is sent to the server and the response is received as html based on thymleaf.

After the screen swap rendering is complete, I want to capture the htmx:afterSwap event in javascript and react based on the result.

If the serial number is empty or duplicated, we want to implement a user UX that gives focus() to the input tag so that the user can input again.

However, the focus is not assigned to the serial number and moves on to the next input tag. After entering the serial number, use the tab key or use the mouse to click the next input tag.

Please tell me how to solve this problem.

I tried using various events, such as html:afterSettle and html:afterSwap, but I couldn't solve it.

input form

        <label class="equipment-label" id="equipmentSn-label">
            <strong class="label flex-1 flex justify-end-safe">
                <span class="text-red-700">*</span>
                <span class="text-blue-700">Serial Num</span>
            </strong>
            <input class="equipment-input-text"
                   id="equipmentSn"
                   th:field="*{equipmentSn}"
                   placeholder="ex) AFUXXXXXX"
                   type="text"
                   required
                   oninvalid="this.setCustomValidity('Please enter your serial number')"
                   oninput="this.setCustomValidity('')"
                   th:hx-post="@{/equipment/check-serial}"
                   hx-trigger="blure changed delay:100ms"
                   hx-target="#sn-check-result"/>
        </label>
        <label class="equipment-label">
            <strong class="label flex-1"></strong>
            <strong id="sn-check-result" class="flex-2 text-sm mt-1"></strong>
        </label>

Response HTML Code

<div th:fragment="result">
    <span th:if="${#strings.isEmpty(equipmentSn)}"  data-invalid="true" class="text-gray-400">
       Please enter your serial number
    </span>
    <span th:if="${!#strings.isEmpty(equipmentSn) and exists}"  data-invalid="true" class="text-red-700 font-bold">
        ⚠ This serial number has already been registered.
    </span>
    <span th:if="${!#strings.isEmpty(equipmentSn) and !exists}" data-invalid="false" class="text-green-600">
        ✔ Available serial numbers.
    </span>
</div>
document.body.addEventListener("htmx:afterSwap", function (evt) {
  const targetId = evt.target.id;

  if (targetId === "sn-check-result") {
    const dataInvalid = evt.detail.target.querySelector("[data-invalid='true']");

    if (dataInvalid) {
      setTimeout(() => {
        const inputSn = document.querySelector("#equipmentSn");
        console.log("equipmentSn element:", inputSn);
        console.log("is visible:", inputSn?.offsetParent !== null);
        console.log("is enabled:", !inputSn?.disabled);

        if (inputSn && inputSn.offsetParent !== null && !inputSn.disabled) {
          inputSn.focus();
          inputSn.select();
        } else {
          console.log("Focus failed - element not ready");
        }
      }, 100);
    }
  }
});

callor avatar Aug 19 '25 00:08 callor

https://jsfiddle.net/Latent22/pz3fx6e9/15/ Put your example code in a quick jsfiddle and it works fine for me. I had to update blure event to blur and remove some of the thymeleaf backend syntax but the focus seems to return fine here

htmx has built in support the the autofocus attribute https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autofocus so you could try using this which allows you to return the input as part of the swap or as part of a hx-swap-oob and add the autofocus attribute to the input when the server detects a validation issue. Its nice when you can move custom behavior to simple declarative html swaps instead of manual client side logic. But to do it this way you would either have to get it to return a hx-swap-oob to replace the the input with an autofocus version when the server detects its invalid or expand your target to a new div wrapping both of your labels and return a bigger fragment with the input and its validation info as a single thing. This makes returning and understanding validation quite simple.

https://jsfiddle.net/Latent22/u8pmd1ak/7/

Here is an example of this in use with the input and its validation prompt being swapped in with autofocus

Edit: note when looking at these fiddles you need to know that the demo.htmx.org script takes the contents of a template tag with a url parameter and uses this to mock an api request so check the content inside the templates for the response data being tested

MichaelWest22 avatar Aug 21 '25 11:08 MichaelWest22