Skip to content

Instantly share code, notes, and snippets.

@alexanderankin
Last active July 15, 2023 06:01
Show Gist options
  • Select an option

  • Save alexanderankin/d22f080e6f37d90979c8b62b6102ef28 to your computer and use it in GitHub Desktop.

Select an option

Save alexanderankin/d22f080e6f37d90979c8b62b6102ef28 to your computer and use it in GitHub Desktop.
package org.example;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.TemplateSpec;
import org.thymeleaf.context.IContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.StringTemplateResolver;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
@SpringBootApplication
class HtmxApp {
private static final List<Map<String, Object>> examples = new ArrayList<>();
public static void main(String[] args) {
SpringApplication.run(HtmxApp.class, args);
examples.add(Map.of("id", 0, "name", "initial example"));
Flux.interval(Duration.of(10, ChronoUnit.SECONDS))
.doOnNext(i -> examples.add(Map.of("id", i + 1000, "name", i + "th example")))
.subscribe();
}
@Configuration
static class TemplateConfig {
@Bean
StringTemplateResolver stringTemplateResolver() {
return new StringTemplateResolver();
}
}
@RequiredArgsConstructor
@Configuration
static class ServerConfig {
private final TemplateEngine templateEngine;
@Bean
RouterFunction<ServerResponse> routerFunction() {
return RouterFunctions.route()
.GET("/", request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(templateEngine.process(new TemplateSpec("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/[email protected]"></script>
<title>Welcome</title>
</head>
<body>
<h1>hello, <span th:text="${name} ?: 'world'"></span></h1>
<h2>check out these examples:</h2>
<button
hx-get=/examples
hx-trigger=click
hx-target=#examples-table
hx-swap=innerHTML
>refresh</button>
<div
id=examples-table
hx-get=/examples
hx-trigger=load
hx-target=#examples-table
hx-swap=innerHTML
></div>
<button
hx-get=/examples/form
hx-trigger=click
hx-target=#examples-form
hx-swap=innerHTML
>
Add another
</button>
<div id=examples-form></div>
</body>
</html>
""", TemplateMode.HTML), new MapContext(Map.of("items", examples)))))
.GET("/examples/form", r -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(templateEngine.process(new TemplateSpec("""
<form hx-post=/examples hx->
<input name="name" type="text">
<button type="submit">Submit</button>
</form>
""", TemplateMode.HTML), new MapContext(Map.of()))))
.GET("/examples", request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(templateEngine.process(new TemplateSpec("""
<table>
<thead><tr><th>ID</th><th>Name</th></tr></thead>
<tbody>
<tr th:each="prod: ${items}">
<td th:text="${prod.id}">id</td>
<td th:text="${prod.name}">name</td>
</tr>
</tbody>
</table>
""", TemplateMode.HTML), new MapContext(Map.of("items", examples)))))
.POST("/examples", request -> request.formData().map(MultiValueMap::toSingleValueMap)
.doOnNext(m -> m.computeIfAbsent("id", k -> UUID.randomUUID().toString()))
.cast(Map.class).doOnNext(examples::add)
.thenReturn(ServerResponse.ok().build()).flatMap(Mono::from))
.build();
}
}
@RequiredArgsConstructor
static class MapContext implements IContext {
private final Map<String, Object> map;
@Override
public Locale getLocale() {
return Locale.getDefault();
}
@Override
public boolean containsVariable(String name) {
return map.containsKey(name);
}
@Override
public Set<String> getVariableNames() {
return map.keySet();
}
@Override
public Object getVariable(String name) {
return map.get(name);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment