DEV Community

Thomas Schühly
Thomas Schühly

Posted on • Edited on

Toasts with Thymeleaf, HTMX and Spring Boot

In this example, I will show you a way to return Toast Notifications from your server and render them interactively.

The HTML:

We will start with an HTMX Element that creates a request to our spring server:

<form class="" hx-target="#toast" hx-put="/api/someRoute"></form>
Enter fullscreen mode Exit fullscreen mode

As you can see we target an element with the id toast. To make this work I created a div with the id that is inside my
footer. The footer is shown on every page.

<footer>
    <div id="toast"></div>
</footer>
Enter fullscreen mode Exit fullscreen mode

But what if you don't want to target the toast element, and instead you want to target another element?
I will show you later how you can change the HTMX target on the server.

Next is our toast element, to make them generic we use parameterized Thymeleaf fragments.
In this example I use the awesome Alpine.js library, but this is also easily doable with
normal javascript.

We create a toast.html file in our root directory to use it later as a return view.


<div th:fragment="info(message,duration)"
     th:attr="x-init='setTimeout(() => $el.style.display = \'none\',' + ${duration} + ')'"
     class="fixed bottom-10 z-40 left-1/2 -translate-x-1/2"
     x-data="{}"
     x-ref="info">
    <div class="flex">
        <div class="alert alert-info shadow-lg my-6">
            <div>
                <span th:text="${message}"></span>
            </div>
            <button @click="$refs.info.style.display = 'none'">
                <img src="/svg/x-circle.svg">
            </button>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

To break it down:

  • th:fragment="info(message,duration) attribute declares that this is a parameterized Thymeleaf fragment we can later use in our spring backend.
  • th:attr="x-init='setTimeout(() => $el.style.display = \'none\',' + ${duration} + ')'" creates a timeout function on initialization that hides the toast after the duration we specified in the fragment parameter
  • Next, we position the element with the tailwind classes fixed bottom-10 z-40 left-1/2 -translate-x-1/2
  • The <span th:text="${message}"></span> element renders our text
  • With the <button @click="$refs.info.style.display = 'none'"> our user can click the toast away before it expires.

I also duplicated this element and changed the info to error and changed the styling to return info or error toasts.

The backend code:

To specify our toast I used an enum class:

enum class Toast {
    INFO {
        override fun ModelAndView(message: String, durationInMs: Int) =
            ModelAndView("toast :: info(message = '${message}', duration = '$durationInMs')")
    },
    ERROR {
        override fun ModelAndView(message: String, durationInMs: Int) =
            ModelAndView("toast :: error(message = '${message}', duration = '$durationInMs')")
    };

    abstract fun ModelAndView(message: String, durationInMs: Int): ModelAndView
}
Enter fullscreen mode Exit fullscreen mode

We can return this enum in our Controller if we want to show either an info message or an error message.

As you can see we return the parameterized fragment with a message we can extract from our business context and give our
user valuable information.

@Controller
ExampleController() {
    @RequestMapping("/api/someRoute")
    fun someRoute(): ModelAndView {
        try {
            // do Something
        } catch (e: Exception) {
            return Toast.ERROR.ModelAndView("An Error occurred: ${e.message}", 5000)
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

Change the HTMX target on the server

But what do you do when you want to swap the element itself and only show a toast if an error occurs?


<form hx-put="/api/someRoute"></form>
Enter fullscreen mode Exit fullscreen mode

We can change the behavior of HTMX easily with two Response Headers:

@RequestMapping("/api/someRoute")
fun someRoute(
    httpServletResponse: HttpServletResponse
): ModelAndView {
    try {
        // do Something
    } catch (e: Exception) {
        httpServletResponse.addHeader("HX-Retarget", "#errors");
        httpServletResponse.addHeader("HX-Reswap", "innerHTML");
        return Toast.ERROR.ModelAndView("An Error occurred: ${e.message}", 5000)
    }

}
Enter fullscreen mode Exit fullscreen mode

We just include the httpServletResponse: HttpServletResponse in the constructor of our function, and then

  • With HX-Retarget we can change the hx-target property
  • And with HX-Reswap we can change the hx-swap property

There are many more HTMX Response Headers you can change to influence the behavior of your application.

My Indiehacker product is built with htmx:
PhotoQuest

Top comments (0)