Skip to content

Commit

Permalink
htmx: use thymeleaf instead of kotlinx.html
Browse files Browse the repository at this point in the history
kotlinx.html does not really support html fragments. For example, I
can't respond with "<li>foo</li>". Instead I have to respond with
"<body><ul><li>foo</li></ul></body>" which does not work with htmx
when swapping a li element.

See: Kotlin/kotlinx.html#228
  • Loading branch information
suniala committed Feb 4, 2024
1 parent 32a4dba commit 9d544f4
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 65 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
92 changes: 27 additions & 65 deletions src/main/kotlin/kotlinbook/htmxRoutes.kt
Original file line number Diff line number Diff line change
@@ -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<HTML> {
val pageBody = Placeholder<BODY>()

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<HTML> {
val fragment = Placeholder<BODY>()
override fun HTML.apply() {
body {
insert(fragment)
}
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/kotlinbook/web/WebResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,17 @@ sealed class WebResponse {
) =
copy(body, statusCode, headers)
}

data class ThymeleafWebResponse(
val template: String,
val model: Map<String, Any> = emptyMap(),
override val statusCode: Int = 200,
override val headers: Map<String, List<String>> = mapOf(),
) : WebResponse() {
override fun copyResponse(
statusCode: Int,
headers: Map<String, List<String>>
) =
copy(template, model, statusCode, headers)
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/kotlinbook/web/WebResponseSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,6 +48,9 @@ object WebResponseSupport {
with(resp.body) { apply() }
}
}

is WebResponse.ThymeleafWebResponse ->
call.respond(statusCode, ThymeleafContent(resp.template, resp.model))
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/templates/click-me.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<html>
<p>I have been clicked.</p>
</html>
20 changes: 20 additions & 0 deletions src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<html xmlns:th="http://www.thymeleaf.org">
<head>
<script src="webjars/htmx.org/1.9.4/dist/htmx.min.js"></script>
<script src="webjars/htmx.org/1.9.4/dist/ext/json-enc.js"></script>
<script src="webjars/htmx.org/1.9.4/dist/ext/sse.js"></script>
<title>HTXM Demo - Kotlinbook</title>
</head>
<body>
<h1>HTMX Demo</h1>
<button hx-get="/htmx/click-me" hx-swap="outerHTML">Click me!</button>
<ol>
<li th:each="item : ${listItems}"
th:id="'listItem' + ${item.id}"
th:hx-get="'/htmx/list-item/' + ${item.id}"
hx-swap="outerHTML"
th:text="'List item ' + ${item.id} + ' updated at ' + ${item.time}">
</li>
</ol>
</body>
</html>
8 changes: 8 additions & 0 deletions src/main/resources/templates/list-item.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html xmlns:th="http://www.thymeleaf.org">
<li
th:id="'listItem' + ${item.id}"
th:hx-get="'/htmx/list-item/' + ${item.id}"
hx-swap="outerHTML"
th:text="'List item ' + ${item.id} + ' updated at ' + ${item.time}">
</li>
</html>

0 comments on commit 9d544f4

Please sign in to comment.