Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ build/

### VS Code ###
.vscode/
/jte-classes/
javac.**
57 changes: 57 additions & 0 deletions htmx-spring-boot-jte/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot-parent</artifactId>
<version>3.6.0-SNAPSHOT</version>
</parent>

<artifactId>htmx-spring-boot-jte</artifactId>
<name>Spring Boot library for htmx and JTE</name>
<description>Spring Boot library to make it easy to work with htmx and JTE</description>

<dependencies>
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-spring-boot-starter-3</artifactId>
<version>3.1.12</version>
</dependency>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte</artifactId>
<version>3.1.12</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.wimdeblauwe.htmx.spring.boot.jte.endpoint;

import gg.jte.TemplateEngine;
import gg.jte.html.policy.*;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JteConfig {
final
TemplateEngine templateEngine;

public JteConfig(TemplateEngine templateEngine) {
this.templateEngine = templateEngine;
}

@PostConstruct
void configureTemplateEngine() {
templateEngine.setHtmlPolicy(new JtePolicy());
}

static class JtePolicy extends PolicyGroup {

JtePolicy() {
addPolicy(new PreventUppercaseTagsAndAttributes());
addPolicy(new PreventOutputInTagsAndAttributes(false));
addPolicy(new PreventUnquotedAttributes());
addPolicy(new PreventInvalidAttributeNames());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.wimdeblauwe.htmx.spring.boot.jte;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

/**
* Just created this here to make Spring Boot test slices work
*/
@SpringBootApplication(exclude = SecurityAutoConfiguration.class) // Security is on by default
public class DummyApplication {

public static void main(String[] args) {
SpringApplication.run(DummyApplication.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.wimdeblauwe.htmx.spring.boot.jte;

import io.github.wimdeblauwe.htmx.spring.boot.endpoint.HtmxEndpoint;

import java.util.Map;

import java.util.Optional;

import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class EndpointController {


public HtmxEndpoint<ModelMap, ModelAndView> htmxTestEndpoint = new HtmxEndpoint<>(
"/test", HttpMethod.GET, this::test);

private ModelAndView test() {
return new ModelAndView("test",Map.of("endpointController", this, "userForm", Optional.empty()));
}


public record UserForm(
String userName, String password) {
}

public HtmxEndpoint<UserForm, ModelAndView> createUserEndpoint = new HtmxEndpoint<>(
"/createUser",
HttpMethod.POST,
this::createUser
);

private ModelAndView createUser(UserForm userForm) {
return new ModelAndView("test",Map.of("endpointController", this,"userForm",Optional.ofNullable(userForm)));
}
}
Empty file.
24 changes: 24 additions & 0 deletions htmx-spring-boot-jte/src/test/jte/test.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import io.github.wimdeblauwe.htmx.spring.boot.jte.EndpointController.UserForm
@param java.util.Optional<UserForm> userForm
@param io.github.wimdeblauwe.htmx.spring.boot.jte.EndpointController endpointController
<script src="https://unpkg.com/[email protected]"></script>

<form ${endpointController.createUserEndpoint.call()}>
<label>
username
<input type="text" name="userName">
</label>
<label>
password
<input type="text" name="password">
</label>
<button type="submit">
Save
</button>
</form>

@if( userForm.isPresent())
<div>
Hello ${userForm.get().userName()}! Your password is: ${userForm.get().password()}
</div>
@endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
gg.jte.developmentMode=true
gg.jte.templateLocation=htmx-spring-boot-jte/src/test/jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.wimdeblauwe.htmx.spring.boot.thymeleaf.endpoint;

import io.github.wimdeblauwe.htmx.spring.boot.endpoint.HtmxEndpoint;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class EndpointController {


public HtmxEndpoint<?, ModelAndView> htmxTestEndpoint = new HtmxEndpoint<>(
"/test", HttpMethod.GET, this::test);

private ModelAndView test() {
return new ModelAndView("test");
}


public record UserForm(
String userName, String password) {
}

public HtmxEndpoint<UserForm, ModelAndView> createUserEndpoint = new HtmxEndpoint<>(
"/createUser",
HttpMethod.POST,
this::createUser
);

private ModelAndView createUser(UserForm userForm) {
return new ModelAndView("test");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.github.wimdeblauwe.htmx.spring.boot.endpoint;

import jakarta.servlet.ServletException;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.function.*;

import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

public abstract class AbstractHtmxEndpoint<T, R> implements RouterFunction<ServerResponse> {

public final String path;
public final HttpMethod method;
private final Supplier<R> supplier;
private final Function<T, R> function;
private final Class<T> tClass;

public AbstractHtmxEndpoint(String path, HttpMethod method, Function<T, R> function, T... tClass) {
this.path = path;
this.method = method;
this.function = function;
this.tClass = getClassOf(tClass);
this.supplier = null;

}

static <T> Class<T> getClassOf(T[] array) {
return (Class<T>) array.getClass().getComponentType();
}

public AbstractHtmxEndpoint(String path, HttpMethod method, Supplier<R> supplier) {
this.path = path;
this.method = method;
this.supplier = supplier;
this.function = null;
this.tClass = null;
}

@Override
@NonNull
public Optional<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
RequestPredicate predicate = RequestPredicates.method(method).and(RequestPredicates.path(path));
if (predicate.test(request)) {
R model = getBody(request);
if (model instanceof ModelAndView modelAndView) {
Map<String, Object> modelData = modelAndView.getModel();
return Optional.of(
req -> RenderingResponse.create(templateName(model))
.modelAttributes(modelData)
.build()
);
} else {
return Optional.of(
req -> RenderingResponse.create(templateName(model))
.modelAttribute(model)
.build());
}
}
return Optional.empty();
}

abstract String templateName(R modelAndView);

private R getBody(ServerRequest req) {
if (function == null && supplier != null) {
return supplier.get();
}
try {
if (function != null) {
MediaType mediaType = req.headers().contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
if(mediaType.includes(MediaType.APPLICATION_FORM_URLENCODED)){
T formData = req.bind(tClass);
return function.apply(
formData
);
}
T body = req.body(tClass);
return function.apply(
body
);
}
} catch (ServletException | IOException e) {
throw new RuntimeException(e);
} catch (BindException e) {
throw new RuntimeException(e);
}
throw new RuntimeException("Failed to get body");
}

public String call() {
return "hx-" + method.name().toLowerCase() + " =\"" + path + "\"";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.wimdeblauwe.htmx.spring.boot.endpoint;

import org.springframework.http.HttpMethod;

import java.util.function.Function;
import java.util.function.Supplier;

import org.springframework.web.servlet.ModelAndView;

public class HtmxEndpoint<T, S> extends AbstractHtmxEndpoint<T, S> {

public HtmxEndpoint(String path, HttpMethod method,
Supplier<S> supplier) {
super(path, method, supplier);
}

@Override
String templateName(S responseType) {
if(responseType instanceof String viewName){
return viewName;
} else if (responseType instanceof ModelAndView modelAndView) {
return modelAndView.getViewName();
}
throw new IllegalArgumentException("HtmxEndpoint doesnt support" + responseType.getClass());
}

public HtmxEndpoint(String path, HttpMethod method, Function<T, S> function, T... tClass) {
super(path, method, function, tClass);
}
}
Loading