Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTMX Integration #735

Merged
merged 18 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3285620
feat: HTMX
sdelamo Mar 12, 2024
5610b3e
add private constructor for Utils class
sdelamo Mar 13, 2024
d7238b8
Interfaces should not solely consist of constants
sdelamo Mar 13, 2024
96e77e8
Update views-htmx/src/main/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
21038ce
Update views-htmx/src/main/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
8e1457a
Update views-htmx/src/main/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
705907d
Update views-htmx/src/main/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
80e153e
Update views-htmx/src/main/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
9d68639
Update views-htmx/src/test/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
5a0311a
Update views-htmx/src/test/java/io/micronaut/views/htmx/http/HtmxRequ…
sdelamo Mar 14, 2024
b0be70f
Update views-htmx/src/test/java/io/micronaut/views/htmx/http/TodoItem…
sdelamo Mar 14, 2024
10157ee
Update views-htmx/src/main/java/io/micronaut/views/htmx/http/HtmxRequ…
sdelamo Mar 14, 2024
f06ffd2
Update views-htmx/src/test/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
cf1094a
Update views-htmx/src/test/java/io/micronaut/views/htmx/HtmxConfigura…
sdelamo Mar 14, 2024
4d27798
remove duplicated test
sdelamo Mar 14, 2024
beea993
Unused import
timyates Mar 14, 2024
c33cbe2
Add imports
timyates Mar 14, 2024
84c1e5a
Merge branch 'master' into htmx
sdelamo Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ include 'views-freemarker'
include 'views-handlebars'
include 'views-thymeleaf'
include 'views-turbo'
include 'views-htmx'
include 'views-velocity'
include 'views-rocker'
include 'views-pebble'
Expand Down
5 changes: 5 additions & 0 deletions src/main/docs/guide/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ views:
fieldsetAnnotations: Fieldset Annotations
fieldsetFetcher: Radio, Checkbox and Option Fetcher
customFormElement: Custom Form Elements
htmx:
title: HTMX
htmxRequestHeaders: HTMX Request Headers
outOfBandSwaps: Out of Band Swaps
htmxResponseHeaders: HTMX Response Headers
turbo:
title: Turbo
turboFrameView: TurboFrameView annotation
Expand Down
3 changes: 3 additions & 0 deletions src/main/docs/guide/views/htmx.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
To integrate with https://htmx.org[HTMX], add the following dependency:

dependency:micronaut-views-htmx[groupId="io.micronaut.views"]
6 changes: 6 additions & 0 deletions src/main/docs/guide/views/htmx/htmxRequestHeaders.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
You can bind api:views.htmx.http.HtmxRequestHeaders[] in a controller method.

In the following example, the parameter is bound if the request is an HTMX request. If it is not an HTMX request,
the parameter is bound as null.

snippet::io.micronaut.views.docs.htmx.HtmxTest[tags="htmxRequestHeaders",indent=0]
3 changes: 3 additions & 0 deletions src/main/docs/guide/views/htmx/htmxResponseHeaders.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The class api:views.htmx.http.HtmxResponseHeaders[] defines constants for the https://htmx.org/reference/#response_headers[HTMX Response headers].

snippet::io.micronaut.views.docs.htmx.HtmxTest[tags="htmxResponseHeaders",indent=0]
3 changes: 3 additions & 0 deletions src/main/docs/guide/views/htmx/outOfBandSwaps.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You can return an API:views.htmx.HtmxResponse[] in a controller method to render multiple views in a single HTMX response—for example, to do https://htmx.org/docs/#oob_swaps[Out Of Band Swaps].

snippet::io.micronaut.views.docs.htmx.HtmxTest[tags="outOfBandSwaps",indent=0]
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<logger name="com.projectcheckins.logging" level="TRACE"/>
<logger name="io.micronaut.http.client" level="TRACE"/>
<logger name="io.micronaut.data.query" level="TRACE"/>
</configuration>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'groovy'
id "io.micronaut.build.internal.views-tests"
groovy
id("io.micronaut.build.internal.views-tests")
}

dependencies {
Expand All @@ -18,15 +18,16 @@ dependencies {
testImplementation(mnSecurity.micronaut.security)
testImplementation(mnSerde.micronaut.serde.api)
testImplementation(mnSerde.micronaut.serde.jackson)

testImplementation(libs.groovy.json)
testImplementation projects.micronautViewsSoy
testImplementation(projects.micronautViewsHtmx)
testImplementation(projects.micronautViewsSoy)
testImplementation(projects.micronautViewsTurbo)
testImplementation projects.micronautViewsVelocity
testImplementation projects.micronautViewsHandlebars
testImplementation(projects.micronautViewsVelocity)
testImplementation(projects.micronautViewsHandlebars)
testRuntimeOnly(mnLogging.logback.classic)
}

tasks.named('test') {
tasks.withType<Test> {
useJUnitPlatform()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.micronaut.views.docs.htmx

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import io.micronaut.views.ModelAndView
import io.micronaut.views.docs.turbo.Fruit
import io.micronaut.views.htmx.http.HtmxRequestHeaders
import io.micronaut.views.htmx.http.HtmxResponse
import io.micronaut.views.htmx.http.HtmxResponseHeaders
import jakarta.inject.Inject
import spock.lang.Specification

@Property(name = "micronaut.views.soy.enabled", value = StringUtils.FALSE)
@Property(name = "micronaut.security.enabled", value = StringUtils.FALSE)
@Property(name = "spec.name", value = "HtmxRequestHeadersTest")
@MicronautTest
class HtmxTest extends Specification {

@Inject
@Client("/")
HttpClient httpClient

void testHtmxRequestHeaders() {
given:
BlockingHttpClient client = httpClient.toBlocking()

when:
String html = client.retrieve(HttpRequest.GET("/fruits/htmx").header("HX-Request", StringUtils.TRUE))
then:
'<h1>fruit: Apple</h1>\n<h2>color: Red</h2>\n' == html
when:
html = client.retrieve(HttpRequest.GET("/fruits/htmx"))
then:
html.contains("<!DOCTYPE html>")
}

void testOutOfBandSwaps() {
given:
BlockingHttpClient client = httpClient.toBlocking()
when:
String html = client.retrieve(HttpRequest.POST("/fruits/htmx", Collections.emptyMap()).header("HX-Request", StringUtils.TRUE));
then:
'<h1>fruit: Apple</h1>\n<h2>color: Red</h2>\n<div id="message" hx-swap-oob="true">Swap me directly!</div>' == html
}

void htmxResponseHeaders() {
given:
BlockingHttpClient client = httpClient.toBlocking();

when:
HttpResponse<?> response = client.exchange(HttpRequest.GET("/fruits/htmx/responseHeaders").header("HX-Request", StringUtils.TRUE));

then:
StringUtils.TRUE == response.getHeaders().get("HX-Refresh")
}

@Requires(property = "spec.name", value = "HtmxRequestHeadersTest")
@Controller("/fruits/htmx")
static class HtmxRequestHeadersController {
//tag::htmxRequestHeaders[]
@Get
ModelAndView<Map<String, Object>> index(@Nullable HtmxRequestHeaders htmxRequestHeaders) {
Map<String, Object> model = [fruit: new Fruit("Apple", "Red")]
if (htmxRequestHeaders != null) {
return new ModelAndView<>("fruit", model)
}
new ModelAndView<>("fruits", model)
}
//end::htmxRequestHeaders[]

//tag::outOfBandSwaps[]
@Post
HtmxResponse<?> outOfBandSwaps(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
HtmxResponse.builder()
.modelAndView(new ModelAndView<>("fruit", [fruit: new Fruit("Apple", "Red")]))
.modelAndView(new ModelAndView<>("swap", [:]))
.build()
}
//end::outOfBandSwaps[]

//tag::htmxResponseHeaders[]
@Get("/responseHeaders")
HttpResponse<?> htmxResponseHeaders(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
HttpResponse.ok().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE);
}
//end::htmxResponseHeaders[]
}
}
1 change: 1 addition & 0 deletions test-suite-groovy/src/test/resources/views/swap.vm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="message" hx-swap-oob="true">Swap me directly!</div>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.kapt)
id "io.micronaut.build.internal.views-tests"
id("io.micronaut.build.internal.views-tests")
}

dependencies {
Expand All @@ -18,20 +18,21 @@ dependencies {
testImplementation(mnSerde.micronaut.serde.api)
testImplementation(mnSerde.micronaut.serde.jackson)

testImplementation(projects.micronautViewsHtmx)
testImplementation(mnSerde.micronaut.serde.jackson)
testImplementation(mn.micronaut.http.server.netty)
testImplementation(mn.micronaut.http.client)
testImplementation projects.micronautViewsSoy
testImplementation(projects.micronautViewsSoy)
testImplementation(projects.micronautViewsTurbo)
testImplementation(libs.kotlinx.coroutines.core)
testImplementation projects.micronautViewsVelocity
testImplementation projects.micronautViewsHandlebars
testImplementation(projects.micronautViewsVelocity)
testImplementation(projects.micronautViewsHandlebars)

testRuntimeOnly(libs.junit.jupiter.engine)
testRuntimeOnly(mnLogging.logback.classic)
}

tasks.named('test') {
tasks.withType<Test> {
useJUnitPlatform()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.micronaut.views.docs.htmx

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import io.micronaut.views.ModelAndView
import io.micronaut.views.docs.turbo.Fruit
import io.micronaut.views.htmx.http.HtmxRequestHeaders
import io.micronaut.views.htmx.http.HtmxResponse
import io.micronaut.views.htmx.http.HtmxResponseHeaders
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

@Property(name = "micronaut.views.soy.enabled", value = StringUtils.FALSE)
@Property(name = "micronaut.security.enabled", value = StringUtils.FALSE)
@Property(name = "spec.name", value = "HtmxRequestHeadersTest")
@MicronautTest
internal class HtmxTest {
@Test
fun testHtmxRequestHeaders(@Client("/") httpClient: HttpClient) {
val client = httpClient.toBlocking()
var html = client.retrieve(HttpRequest.GET<Any>("/fruits/htmx").header("HX-Request", StringUtils.TRUE))
assertEquals(
"""
<h1>fruit: Apple</h1>
<h2>color: Red</h2>
""".trimIndent(), html
)
html = client.retrieve(HttpRequest.GET<Any>("/fruits/htmx"))
assertTrue(html.contains("<!DOCTYPE html>"))
}

@Test
fun testOutOfBandSwaps(@Client("/") httpClient: HttpClient) {
val client = httpClient.toBlocking()
val html = client.retrieve(
HttpRequest.POST("/fruits/htmx", emptyMap<Any, Any>()).header("HX-Request", StringUtils.TRUE)
)
assertEquals(
"""
<h1>fruit: Apple</h1>
<h2>color: Red</h2>
<div id="message" hx-swap-oob="true">Swap me directly!</div>
""".trimIndent(), html
)
}

@Test
fun htmxResponseHeaders(@Client("/") httpClient: HttpClient) {
val client = httpClient.toBlocking()
val response: HttpResponse<*> = client.exchange<Any, Any>(
HttpRequest.GET<Any>("/fruits/htmx/responseHeaders").header("HX-Request", StringUtils.TRUE)
)
assertEquals(
StringUtils.TRUE,
response.headers["HX-Refresh"]
)
}

@Requires(property = "spec.name", value = "HtmxRequestHeadersTest")
@Controller("/fruits/htmx")
internal class HtmxRequestHeadersController {
//tag::htmxRequestHeaders[]
@Get
fun index(htmxRequestHeaders: HtmxRequestHeaders?): ModelAndView<Map<String, Any>> {
val model = mapOf("fruit" to Fruit("Apple", "Red"))
if (htmxRequestHeaders != null) {
return ModelAndView("fruit", model)
}
return ModelAndView("fruits", model)
}
//end::htmxRequestHeaders[]

//tag::outOfBandSwaps[]
@Post
fun outOfBandSwaps(htmxRequestHeaders: HtmxRequestHeaders): HtmxResponse<*> {
return HtmxResponse.builder<Any>()
.modelAndView(ModelAndView("fruit", mapOf("fruit" to Fruit("Apple", "Red"))))
.modelAndView(ModelAndView("swap", emptyMap<Any, Any>()))
.build()
}
//end::outOfBandSwaps[]

//tag::htmxResponseHeaders[]
@Get("/responseHeaders")
fun htmxResponseHeaders(htmxRequestHeaders: @NonNull HtmxRequestHeaders): HttpResponse<*> {
return HttpResponse.ok<Any>().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE)
}
//end::htmxResponseHeaders[]


}
}
1 change: 1 addition & 0 deletions test-suite-kotlin/src/test/resources/views/swap.vm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="message" hx-swap-oob="true">Swap me directly!</div>
15 changes: 8 additions & 7 deletions test-suite/build.gradle → test-suite/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java-library'
id "io.micronaut.build.internal.views-tests"
`java-library`
id("io.micronaut.build.internal.views-tests")
}

dependencies {
Expand All @@ -23,13 +23,14 @@ dependencies {
testImplementation(mnSerde.micronaut.serde.api)
testImplementation(mnSerde.micronaut.serde.jackson)

testImplementation projects.micronautViewsVelocity
testImplementation projects.micronautViewsCore
testImplementation(projects.micronautViewsHtmx)
testImplementation(projects.micronautViewsVelocity)
testImplementation(projects.micronautViewsCore)
testImplementation(projects.micronautViewsTurbo)
testImplementation projects.micronautViewsSoy
testImplementation projects.micronautViewsHandlebars
testImplementation(projects.micronautViewsSoy)
testImplementation(projects.micronautViewsHandlebars)
}

tasks.named('test') {
tasks.withType<Test> {
useJUnitPlatform()
}
Loading
Loading