Skip to content

Commit 9cc7331

Browse files
committed
Introduce HtmxView, HtmxRedirectView, HtmxLocationRedirectView and HtmxRefreshView
1 parent 738672a commit 9cc7331

14 files changed

Lines changed: 993 additions & 144 deletions

File tree

README.md

Lines changed: 97 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
# Spring Boot and Thymeleaf library for htmx
66

7-
This project provides annotations, helper classes and a [Thymeleaf](https://www.thymeleaf.org/) dialect
8-
to make it easy to work with [htmx](https://htmx.org/)
9-
in a [Spring Boot](https://spring.io/projects/spring-boot) application.
7+
The project simplifies the integration of [htmx](https://htmx.org/) with [Spring Boot](https://spring.io/projects/spring-boot) / [Spring Web MVC](https://docs.spring.io/spring-framework/reference/web/webmvc.html) applications.
8+
It provides a set of views, annotations, and argument resolvers for controllers to easily handle htmx-related request and response headers.
9+
This ensures seamless interaction between the frontend and backend, especially for dynamic content updates via htmx.
1010

11-
More information about htmx can be viewed on [their website](https://htmx.org/).
11+
Additionally, the project includes a custom [Thymeleaf](https://www.thymeleaf.org/) dialect to enable smooth rendering of htmx-specific attributes within Thymeleaf templates.
12+
With these tools, developers can quickly implement htmx-driven interactions, such as AJAX-based partial page updates, with minimal configuration.
1213

1314
## Maven configuration
1415

@@ -45,7 +46,7 @@ Provides a [Thymeleaf](https://www.thymeleaf.org/) dialect to easily work with h
4546

4647
The included Spring Boot Auto-configuration will enable the htmx integrations.
4748

48-
### Mapping controller methods to htmx requests
49+
### Mapping Requests
4950

5051
Controller methods can be annotated with
5152
[HxRequest](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRequest.html)
@@ -57,8 +58,8 @@ The following method is called only if the request was made by htmx.
5758
```java
5859
@HxRequest
5960
@GetMapping("/users")
60-
public String htmxRequest(){
61-
return "partial";
61+
public String users() {
62+
return "view";
6263
}
6364
```
6465

@@ -70,8 +71,8 @@ or [HxRequest#triggerName](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spr
7071
```java
7172
@HxRequest("my-element")
7273
@GetMapping("/users")
73-
public String htmxRequest(){
74-
return "partial";
74+
public String users() {
75+
return "view";
7576
}
7677
```
7778
If you want to restrict the invocation of a controller method to having a specific target element defined,
@@ -80,34 +81,66 @@ use [HxRequest#target](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-
8081
```java
8182
@HxRequest(target = "my-target")
8283
@GetMapping("/users")
83-
public String htmxRequest(){
84-
return "partial";
84+
public String users() {
85+
return "view";
8586
}
8687
```
8788

88-
#### Using HtmxRequest to access HTTP request headers sent by htmx
89+
### Request Headers
8990

90-
The [HtmxRequest](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.html) object can be used as controller method parameter to access the various [htmx Request Headers](https://htmx.org/reference/#request_headers).
91+
To access the various [htmx Request Headers](https://htmx.org/reference/#request_headers) in a controller method, you can use the class [HtmxRequest](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.html)
92+
as a controller method argument.
9193

9294
```java
9395
@HxRequest
9496
@GetMapping("/users")
95-
public String htmxRequest(HtmxRequest htmxRequest) {
96-
if(htmxRequest.isHistoryRestoreRequest()){
97+
public String users(HtmxRequest htmxRequest) {
98+
if (htmxRequest.isHistoryRestoreRequest()) {
9799
// do something
98100
}
99-
return "partial";
101+
return "view";
100102
}
101103
```
102104

103105
### Response Headers
104106

105-
There are two ways to set [htmx Response Headers](https://htmx.org/reference/#response_headers) on controller methods.
106-
The first is to use annotations, e.g. `@HxTrigger`, and the second is to use the class [HtmxResponse](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.html) as the return type of the controller method.
107+
There are two ways to set [htmx Response Headers](https://htmx.org/reference/#response_headers) in controller methods. The first is to use [HtmxResponse](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.html)
108+
as controller method argument in combination with different Views e.g. [HtmxRedirectView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRedirectView.html)
109+
as return value. The second is to use annotations, e.g. `@HxTrigger` to set the necessary response headers. The first method is more flexible and allows you to dynamically set the response headers based on the request.
107110

108-
See [Response Headers Reference](https://htmx.org/reference/#response_headers) for the related htmx documentation.
111+
#### HtmxResponse and Views
112+
113+
Most of the [htmx Response Headers](https://htmx.org/reference/#response_headers) can be set by using [HtmxResponse](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.html) as controller method argument,
114+
except for some control flow response headers such as [HX-Redirect](https://htmx.org/headers/hx-redirect/). For these response headers, you have to use a corresponding view as return value of the controller method.
115+
116+
* [HtmxRedirectView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRedirectView.html) - sets the [HX-Redirect](https://htmx.org/headers/hx-redirect/) header to do a client-side redirect.
117+
* [HtmxLocationRedirectView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxLocationRedirectView.html) - sets the [HX-Location](https://htmx.org/headers/hx-location/) header to do a client-side redirect without reloading the whole page.
118+
* [HtmxRefreshView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRefreshView.html) - sets the [HX-Refresh](https://htmx.org/headers/hx-refresh/) header to do a client-side refresh of the current page.
119+
120+
121+
```java
122+
@HxRequest
123+
@PostMapping("/user/{id}")
124+
public Object user(@PathVariable Long id, @ModelAttribute @Valid UserForm form,
125+
BindingResult bindingResult, RedirectAttributes redirectAttributes,
126+
HtmxResponse htmxResponse) {
127+
128+
if (bindingResult.hasErrors()) {
129+
return "user/form";
130+
}
131+
132+
// update user ...
133+
redirectAttributes.addFlashAttribute("successMessage", "User has been successfully updated.");
134+
htmxResponse.addTrigger("user-updated");
135+
136+
return new HtmxRedirectView("/user/list");
137+
}
138+
```
139+
140+
#### Annotations
141+
142+
The following annotations can be used on controller methods to set the necessary response headers.
109143

110-
The following annotations are currently supported:
111144
* [@HxLocation](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.html)
112145
* [@HxPushUrl](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.html)
113146
* [@HxRedirect](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRedirect.html)
@@ -122,115 +155,69 @@ The following annotations are currently supported:
122155

123156
>**Note** Please refer to the related Javadoc to learn more about the available options.
124157
125-
#### Examples
126-
127158
If you want htmx to trigger an event after the response is processed, you can use the annotation `@HxTrigger` which sets the necessary response header [HX-Trigger](https://htmx.org/headers/hx-trigger/).
128159

129160
```java
130161
@HxRequest
131162
@HxTrigger("userUpdated") // the event 'userUpdated' will be triggered by htmx
132163
@GetMapping("/users")
133-
public String hxUpdateUser(){
134-
return "partial";
135-
}
136-
```
137-
138-
If you want to do the same, but in a more flexible way, you can use `HtmxResponse` as the return type in the controller method instead.
139-
```java
140-
@HxRequest
141-
@GetMapping("/users")
142-
public HtmxResponse hxUpdateUser(){
143-
return HtmxResponse.builder()
144-
.trigger("userUpdated") // the event 'userUpdated' will be triggered by htmx
145-
.view("partial")
146-
.build();
164+
public String users() {
165+
return "view";
147166
}
148167
```
149168

150-
### Out Of Band Swaps
169+
### HTML Fragments
151170

152-
htmx supports updating multiple targets by returning multiple partials in a single response, which is called [Out Of Band Swaps](https://htmx.org/docs/#oob_swaps).
153-
For this purpose, use [HtmxResponse](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.html)
154-
as the return type of a controller method, where you can add multiple templates.
171+
In Spring MVC, view rendering typically involves specifying one view and one model. However, in htmx a common capability is to send multiple HTML fragments that
172+
htmx can use to update different parts of the page, which is called [Out Of Band Swaps](https://htmx.org/docs/#oob_swaps). For this, controller methods can return
173+
[HtmxView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.html)
155174

156175
```java
157176
@HxRequest
158-
@GetMapping("/partials/main-and-partial")
159-
public HtmxResponse getMainAndPartial(Model model){
160-
model.addAttribute("userCount", 5);
161-
return HtmxResponse.builder()
162-
.view("users-list")
163-
.view("users-count")
164-
.build();
177+
@GetMapping("/users")
178+
public View users(Model model) {
179+
model.addAttribute("users", userRepository.findAll());
180+
model.addAttribute("count", userRepository.count());
181+
182+
var view = new HtmxView();
183+
view.add("users/list");
184+
view.add("users/count");
185+
186+
return view;
165187
}
166188
```
167189

168-
An `HtmxResponse` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how
169-
to do that, or from `ModelAndView` instances (resolved or unresolved). For example:
190+
An `HtmxView` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how
191+
to do that, or from `ModelAndView` instances (resolved or unresolved). Each fragment can have its own model, which is merged with the controller model before rendering.
170192

171193
```java
172194
@HxRequest
173-
@GetMapping("/partials/main-and-partial")
174-
public HtmxResponse getMainAndPartial(Model model){
175-
return HtmxResponse.builder()
176-
.view(new ModelAndView("users-list")
177-
.view(new ModelAndView("users-count", Map.of("userCount",5))
178-
.build();
179-
}
180-
```
181-
182-
Using `ModelAndView` means that each fragment can have its own model (which is merged with the controller model before rendering).
183-
184-
### HtmxResponse.Builder as an argument
185-
186-
An `HtmxReponse.Builder` can be injected as a controller method. This creates the parameter and adds it to the model,
187-
allowing it to be used without requiring it be the method return value. This is useful when the return value is needed for
188-
the template.
189-
190-
This allows for the following usage:
191-
192-
```java
193-
@GetMapping("/endpoint")
194-
public String endpoint(HtmxResponse.Builder htmxResponse, Model model) {
195-
htmxResponse.trigger("event1");
196-
model.addAttribute("aField", "aValue");
197-
return "endpointTemplate";
198-
}
199-
```
200-
201-
For example the [JTE templating library](https://jte.gg/) supports statically typed templates and can be used like so:
195+
@GetMapping("/users")
196+
public View users(Model model) {
197+
var view = new HtmxView();
198+
view.add("users/list", Map.of("users", userRepository.findAll()));
199+
view.add("users/count", Map.of("count", userRepository.count()));
202200

203-
```java
204-
@GetMapping("/endpoint")
205-
public JteModel endpoint(HtmxResponse.Builder htmxResponse) {
206-
htmxResponse.trigger("event1");
207-
String aField = "aValue";
208-
return templates.endpointTemplate(aField);
201+
return view;
209202
}
210203
```
211204

212205

213-
### Error handlers
206+
### Exceptions
214207

215-
It is possible to use `HtmxResponse` as a return type from error handlers.
216-
This makes it quite easy to declare a global error handler that will show a message somewhere whenever there is an error
217-
by declaring a global error handler like this:
208+
It is also possible to use `HtmxRequest` and `HtmxResponse` as method argument in handler methods annotated with `@ExceptionHandler`.
218209

219210
```java
220211

221212
@ExceptionHandler(Exception.class)
222-
public HtmxResponse handleError(Exception ex) {
223-
return HtmxResponse.builder()
224-
.reswap(HtmxReswap.none())
225-
.view(new ModelAndView("fragments :: error-message", Map.of("message", ex.getMessage())))
226-
.build();
213+
public String handleError(Exception ex, HtmxRequest htmxRequest, HtmxResponse htmxResponse) {
214+
if (htmxRequest.isHtmxRequest()) {
215+
htmxResponse.setRetarget("#error-message");
216+
}
217+
return "error";
227218
}
228219
```
229220

230-
This will override the normal swapping behaviour of any htmx request that has an exception to avoid swapping to occur.
231-
If the `error-message` fragment is declared as an Out Of Band Swap and your page layout has an empty div to "receive"
232-
that piece of HTML, then only that will be placed on the screen.
233-
234221
### Spring Security
235222

236223
The library has an `HxRefreshHeaderAuthenticationEntryPoint` that you can use to have htmx force a full page browser
@@ -243,20 +230,18 @@ To use it, add it to your security configuration like this:
243230

244231
```java
245232
@Bean
246-
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
233+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
247234
// probably some other configurations here
248-
249235
var entryPoint = new HxRefreshHeaderAuthenticationEntryPoint();
250236
var requestMatcher = new RequestHeaderRequestMatcher("HX-Request");
251-
http.exceptionHandling(exception ->
252-
exception.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
237+
http.exceptionHandling(configurer -> configurer.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
253238
return http.build();
254239
}
255240
```
256241

257242
### Thymeleaf
258243

259-
#### Markup Selectors and Out Of Band Swaps
244+
#### Markup Selectors and HTML Fragments
260245

261246
The Thymeleaf integration for Spring supports the specification of a [Markup Selector](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-c-markup-selector-syntax)
262247
for views. The Markup Selector will be used for selecting the section
@@ -270,13 +255,16 @@ fragment `count` (th:fragment="count") from the template `users`.
270255

271256
```java
272257
@HxRequest
273-
@GetMapping("/partials/main-and-partial")
274-
public HtmxResponse getMainAndPartial(Model model){
275-
model.addAttribute("userCount", 5);
276-
return HtmxResponse.builder()
277-
.view("users :: list")
278-
.view("users :: count")
279-
.build();
258+
@GetMapping("/users")
259+
public View users(Model model) {
260+
model.addAttribute("users", userRepository.findAll());
261+
model.addAttribute("count", userRepository.count());
262+
263+
var view = new HtmxView();
264+
view.add("users :: list");
265+
view.add("users :: count");
266+
267+
return view;
280268
}
281269
```
282270

0 commit comments

Comments
 (0)