From 9d544f474670a17158a5cc5d0ee42fdea7d686ee Mon Sep 17 00:00:00 2001 From: Mikko Suniala Date: Sun, 4 Feb 2024 18:01:17 +0200 Subject: [PATCH] htmx: use thymeleaf instead of kotlinx.html kotlinx.html does not really support html fragments. For example, I can't respond with "
  • foo
  • ". Instead I have to respond with "" which does not work with htmx when swapping a li element. See: https://github.com/Kotlin/kotlinx.html/issues/228 --- build.gradle.kts | 2 + src/main/kotlin/kotlinbook/htmxRoutes.kt | 92 ++++++------------- src/main/kotlin/kotlinbook/web/WebResponse.kt | 13 +++ .../kotlinbook/web/WebResponseSupport.kt | 4 + src/main/resources/templates/click-me.html | 3 + src/main/resources/templates/index.html | 20 ++++ src/main/resources/templates/list-item.html | 8 ++ 7 files changed, 77 insertions(+), 65 deletions(-) create mode 100644 src/main/resources/templates/click-me.html create mode 100644 src/main/resources/templates/index.html create mode 100644 src/main/resources/templates/list-item.html diff --git a/build.gradle.kts b/build.gradle.kts index 8871b0a..bc64eab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,8 +23,10 @@ dependencies { implementation("io.ktor:ktor-server-auth:2.2.4") implementation("io.ktor:ktor-server-sessions:2.2.4") + // htmx implementation("io.ktor:ktor-server-webjars-jvm:2.2.4") implementation("org.webjars.npm:htmx.org:1.9.4") + implementation("io.ktor:ktor-server-thymeleaf:2.2.4") implementation("io.arrow-kt:arrow-fx-coroutines:1.1.5") implementation("io.arrow-kt:arrow-fx-stm:1.1.5") diff --git a/src/main/kotlin/kotlinbook/htmxRoutes.kt b/src/main/kotlin/kotlinbook/htmxRoutes.kt index 2a0de67..8630269 100644 --- a/src/main/kotlin/kotlinbook/htmxRoutes.kt +++ b/src/main/kotlin/kotlinbook/htmxRoutes.kt @@ -1,83 +1,45 @@ package kotlinbook import io.ktor.server.application.* -import io.ktor.server.html.* import io.ktor.server.routing.* +import io.ktor.server.thymeleaf.* import io.ktor.server.webjars.* import kotlinbook.web.WebResponse import kotlinbook.web.WebResponseSupport -import kotlinx.html.BODY -import kotlinx.html.HTML -import kotlinx.html.body -import kotlinx.html.button -import kotlinx.html.h1 -import kotlinx.html.head -import kotlinx.html.p -import kotlinx.html.script -import kotlinx.html.title +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import java.time.LocalTime + +data class ListItem(val id: Int, val time: LocalTime) fun Application.initHtmxRoutes() { install(Webjars) + install(Thymeleaf) { + setTemplateResolver(ClassLoaderTemplateResolver().apply { + prefix = "templates/" + suffix = ".html" + characterEncoding = "utf-8" + }) + } routing { get("/htmx", WebResponseSupport.webResponse { - WebResponse.HtmlWebResponse(HtmxLayout("HTMX").apply { - pageBody { - h1 { - +"HTMX Demo" - } - button { - attributes["hx-get"] = "/htmx/click-me" - attributes["hx-swap"] = "outerHTML" - +"Click me!" - } - } - }) + WebResponse.ThymeleafWebResponse( + "index", mapOf( + "listItems" to (1..100).map { listItemId -> + ListItem(listItemId, LocalTime.now()) + }) + ) }) get("/htmx/click-me", WebResponseSupport.webResponse { - WebResponse.HtmlWebResponse(FragmentLayout().apply { - fragment { - p { +"I have been clicked." } - } - }) + WebResponse.ThymeleafWebResponse("click-me") + }) + get("/htmx/list-item/{id}", WebResponseSupport.webResponse { + val id = checkNotNull(call.parameters["id"]).toInt() + WebResponse.ThymeleafWebResponse( + "list-item", mapOf( + "item" to ListItem(id, LocalTime.now()) + ) + ) }) - } -} - -private class HtmxLayout( - val pageTitle: String? = null -) : Template { - val pageBody = Placeholder() - - override fun HTML.apply() { - val pageTitlePrefix = if (pageTitle == null) { - "" - } else { - "$pageTitle - " - } - head { - title { - +"${pageTitlePrefix}KotlinBook" - } - script(src = htmx("dist/htmx.min.js")) {} - script(src = htmx("dist/ext/json-enc.js")) {} - script(src = htmx("dist/ext/sse.js")) {} - } - body { - insert(pageBody) - } - } - - companion object { - private val htmx = { e: String -> "webjars/htmx.org/1.9.4/$e" } - } -} - -private class FragmentLayout() : Template { - val fragment = Placeholder() - override fun HTML.apply() { - body { - insert(fragment) - } } } diff --git a/src/main/kotlin/kotlinbook/web/WebResponse.kt b/src/main/kotlin/kotlinbook/web/WebResponse.kt index 399d3c5..3b6041a 100644 --- a/src/main/kotlin/kotlinbook/web/WebResponse.kt +++ b/src/main/kotlin/kotlinbook/web/WebResponse.kt @@ -59,4 +59,17 @@ sealed class WebResponse { ) = copy(body, statusCode, headers) } + + data class ThymeleafWebResponse( + val template: String, + val model: Map = emptyMap(), + override val statusCode: Int = 200, + override val headers: Map> = mapOf(), + ) : WebResponse() { + override fun copyResponse( + statusCode: Int, + headers: Map> + ) = + copy(template, model, statusCode, headers) + } } diff --git a/src/main/kotlin/kotlinbook/web/WebResponseSupport.kt b/src/main/kotlin/kotlinbook/web/WebResponseSupport.kt index d98ffe2..6c16cf3 100644 --- a/src/main/kotlin/kotlinbook/web/WebResponseSupport.kt +++ b/src/main/kotlin/kotlinbook/web/WebResponseSupport.kt @@ -6,6 +6,7 @@ import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.html.* import io.ktor.server.response.* +import io.ktor.server.thymeleaf.* import io.ktor.util.pipeline.* import kotliquery.Session import kotliquery.TransactionalSession @@ -47,6 +48,9 @@ object WebResponseSupport { with(resp.body) { apply() } } } + + is WebResponse.ThymeleafWebResponse -> + call.respond(statusCode, ThymeleafContent(resp.template, resp.model)) } } } diff --git a/src/main/resources/templates/click-me.html b/src/main/resources/templates/click-me.html new file mode 100644 index 0000000..04c232a --- /dev/null +++ b/src/main/resources/templates/click-me.html @@ -0,0 +1,3 @@ + +

    I have been clicked.

    + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..3b433a6 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,20 @@ + + + + + + HTXM Demo - Kotlinbook + + +

    HTMX Demo

    + +
      +
    1. +
    2. +
    + + \ No newline at end of file diff --git a/src/main/resources/templates/list-item.html b/src/main/resources/templates/list-item.html new file mode 100644 index 0000000..dc2bead --- /dev/null +++ b/src/main/resources/templates/list-item.html @@ -0,0 +1,8 @@ + +
  • +
  • + \ No newline at end of file