Skip to content

Commit

Permalink
Merge pull request #64 from xhaggi/separate-functionalities-into-modules
Browse files Browse the repository at this point in the history
Separate functionalities into modules
  • Loading branch information
wimdeblauwe authored Aug 26, 2023
2 parents 9606303 + c97c2eb commit 805e29a
Show file tree
Hide file tree
Showing 51 changed files with 349 additions and 242 deletions.
152 changes: 82 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@

# Spring Boot and Thymeleaf library for htmx

This library provides helper classes and a [Thymeleaf](https://www.thymeleaf.org/) dialect
This project provides annotations, helper classes and a [Thymeleaf](https://www.thymeleaf.org/) dialect
to make it easy to work with [htmx](https://htmx.org/)
in a [Spring Boot](https://spring.io/projects/spring-boot) application.

More information about htmx can be viewed on [their website](https://htmx.org/).

## Installation
## Maven configuration

The library is available
The project provides the following libraries, which are available
on [Maven Central](https://mvnrepository.com/artifact/io.github.wimdeblauwe/htmx-spring-boot-thymeleaf),
so it is easy to add the dependency to your project.
so it is easy to add the desired dependency to your project.

### htmx-spring-boot

Provides annotations and helper classes.

```xml
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot</artifactId>
<version>LATEST_VERSION_HERE</version>
</dependency>
```

### htmx-spring-boot-thymeleaf

Provides a [Thymeleaf](https://www.thymeleaf.org/) dialect to easily work with htmx attributes.
```xml
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
Expand All @@ -28,7 +43,7 @@ so it is easy to add the dependency to your project.

### Configuration

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

### Request Headers

Expand Down Expand Up @@ -88,13 +103,67 @@ public String hxUpdateUser(){
}
```

### Processors
### OOB Swap support

htmx supports updating multiple targets by returning multiple partials in a single response with
[`hx-swap-oob`](https://htmx.org/docs/#oob_swaps). Return partials using this library use the `HtmxResponse` as a return
type:

```java
@GetMapping("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
model.addAttribute("userCount", 5);
return new HtmxResponse()
.addTemplate("users :: list")
.addTemplate("users :: count");
}
```

An `HtmxResponse` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how
to do that, or from `ModelAndView` instances (resolved or unresolved). For example:

```java
@GetMapping("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
return new HtmxResponse()
.addTemplate(new ModelAndView("users :: list")
.addTemplate(new ModelAndView("users :: count", Map.of("userCount",5));
}
```

Using `ModelAndView` means that each fragment can have its own model (which is merged with the controller model before rendering).

### Spring Security

The library has an `HxRefreshHeaderAuthenticationEntryPoint` that you can use to have htmx force a full page browser
refresh in case there is an authentication failure.
If you don't use this, then your login page might be appearing in place of a swap you want to do somewhere.
See [htmx-authentication-error-handling](https://www.wimdeblauwe.com/blog/2022/10/04/htmx-authentication-error-handling/)
blog post for detailed information.
To use it, add it to your security configuration like this:
```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
// probably some other configurations here
var entryPoint = new HxRefreshHeaderAuthenticationEntryPoint();
var requestMatcher = new RequestHeaderRequestMatcher("HX-Request");
http.exceptionHandling(exception ->
exception.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
return http.build();
}
```
### Thymeleaf
_See [Attribute Reference](https://htmx.org/reference/#attributes) for the related htmx documentation._
Thymeleaf processors are provided to allow Thymeleaf to be able to perform calculations and expressions
The Thymeleaf dialect has appropriate processors that enable Thymeleaf to perform calculations and expressions
in htmx-related attributes.
Note the `:` colon instead of the typical hyphen.
>**Note** The `:` colon instead of the typical hyphen.
- `hx:get`: This is a Thymeleaf processing enabled attribute
- `hx-get`: This is just a static attribute if you don't need the Thymeleaf processing
Expand All @@ -108,18 +177,17 @@ For example, this Thymeleaf template:
Will be rendered as:

```html

<div hx-get="/users/123" hx-target="#otherElement">Load user details</div>
```

The included Thymeleaf dialect has corresponding processors for most of the `hx-*` attributes.
The Thymeleaf dialect has corresponding processors for most of the `hx-*` attributes.
Please [open an issue](https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/issues) if something is missing.

> **Note**
> Be careful about using `#` in the value. If you do `hx:target="#mydiv"`, then this will not work as Thymeleaf uses
> the `#` symbol for translation keys. Either use `hx-target="#mydiv"` or `hx:target="${'#mydiv'}"`

### Map support for hx:vals
#### Map support for hx:vals

The [hx-vals](https://htmx.org/attributes/hx-vals/) attribute allows to add to the parameters that will be submitted
with the AJAX request. The value of the attribute should be a JSON string.
Expand All @@ -129,74 +197,17 @@ The library makes it a bit easier to write such a JSON string by adding support
For example, this Thymeleaf expression:

```html

<div hx:vals="${ {id: user.id } }"></div>
```

will render as:

```html

<div hx-vals="{&amp;quot;id&amp;quot;: 1234 }"></div>
```

(Given `user.id` has the value `1234`)

### OOB Swap support

htmx supports updating multiple targets by returning multiple partial response with
[`hx-swap-oob`](https://htmx.org/docs/#oob_swaps). Return partials using this library use the `HtmxResponse` as a return
type:

```java
@GetMapping("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
model.addAttribute("userCount",5);
return new HtmxResponse()
.addTemplate("users :: list")
.addTemplate("users :: count");
}
```

An `HtmxResponse` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how
to do that, or from `ModelAndView` instances (resolved or unresolved). For example:

```java
@GetMapping("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
return new HtmxResponse()
.addTemplate(new ModelAndView("users :: list")
.addTemplate(new ModelAndView("users :: count", Map.of("userCount",5));
}
```

Using `ModelAndView` means that each fragment can have its own model (which is merged with the controller model before rendering).

### Spring Security

The library has an `HxRefreshHeaderAuthenticationEntryPoint` that you can use to have htmx force a full page browser
refresh in case there is an authentication failure.
If you don't use this, then your login page might be appearing in place of a swap you want to do somewhere.
See [htmx-authentication-error-handling](https://www.wimdeblauwe.com/blog/2022/10/04/htmx-authentication-error-handling/)
blog post for detailed information.
To use it, add it to your security configuration like this:
```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
// probably some other configurations here
http...
var entryPoint=new HxRefreshHeaderAuthenticationEntryPoint();
var requestMatcher=new RequestHeaderRequestMatcher("HX-Request");
http.exceptionHandling(exception->
exception.defaultAuthenticationEntryPointFor(entryPoint,
requestMatcher));
return http.build();
}
```
## Articles

Links to articles and blog posts about this library:
Expand All @@ -209,7 +220,8 @@ Links to articles and blog posts about this library:

| htmx-spring-boot-thymeleaf | Spring Boot | Minimum Java version |
|---------------------------------------------------------------------------------------|-------------|----------------------|
| [2.2.0](https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/releases/tag/2.2.0) | 3.x. | 17 |
| [3.0.0](https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/releases/tag/2.1.0) | 3.1.x | 17 |
| [2.2.0](https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/releases/tag/2.2.0) | 3.0.x | 17 |
| [1.0.0](https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/releases/tag/1.0.0) | 2.7.x | 11 |

## Contributing
Expand All @@ -228,7 +240,7 @@ To release a new version of the project, follow these steps:

1. Update `pom.xml` with the new version and commit
2. Tag the commit with the version (e.g. `1.0.0`) and push the tag.
3. Create a new release in GitHub via https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/releases/new
3. Create a new release in GitHub via https://github.com/wimdeblauwe/htmx-spring-boot/releases/new
- Select the newly pushed tag
- Update the release notes. This should automatically start
the [release action](https://github.com/wimdeblauwe/htmx-spring-boot-thymeleaf/actions).
Expand Down
59 changes: 59 additions & 0 deletions htmx-spring-boot-thymeleaf/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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 https://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.0.0-SNAPSHOT</version>
</parent>

<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot-thymeleaf</artifactId>
<name>Spring Boot library for htmx and Thymeleaf</name>
<description>Spring Boot library to make it easy to work with htmx and Thymeleaf</description>

<dependencies>
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot</artifactId>
<version>${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-thymeleaf</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.wimdeblauwe.hsbt.thymeleaf;
package io.github.wimdeblauwe.htmx.spring.boot.thymeleaf;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.wimdeblauwe.hsbt.thymeleaf;
package io.github.wimdeblauwe.htmx.spring.boot.thymeleaf;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.wimdeblauwe.hsbt.mvc.HtmxSpringStandardExressionObjectFactory;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.expression.IExpressionObjectFactory;
Expand All @@ -12,7 +11,7 @@

public class HtmxDialect extends AbstractProcessorDialect implements IExpressionObjectDialect {

private HtmxSpringStandardExressionObjectFactory expressionObjectFactory;
private HtmxExpressionObjectFactory expressionObjectFactory;

private final ObjectMapper mapper;

Expand Down Expand Up @@ -61,7 +60,7 @@ public Set<IProcessor> getProcessors(String dialectPrefix) {
@Override
public IExpressionObjectFactory getExpressionObjectFactory() {
if (this.expressionObjectFactory == null) {
this.expressionObjectFactory = new HtmxSpringStandardExressionObjectFactory();
this.expressionObjectFactory = new HtmxExpressionObjectFactory();
}
return this.expressionObjectFactory;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.wimdeblauwe.hsbt.mvc;
package io.github.wimdeblauwe.htmx.spring.boot.thymeleaf;

import io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxHandlerMethodArgumentResolver;
import jakarta.servlet.http.HttpServletRequest;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.context.IWebContext;
Expand All @@ -11,7 +12,7 @@
import java.util.LinkedHashSet;
import java.util.Set;

public class HtmxSpringStandardExressionObjectFactory implements IExpressionObjectFactory {
public class HtmxExpressionObjectFactory implements IExpressionObjectFactory {

/*
* Any new objects added here should also be added to the "ALL_EXPRESSION_OBJECT_NAMES" See below.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.wimdeblauwe.hsbt.thymeleaf;
package io.github.wimdeblauwe.htmx.spring.boot.thymeleaf;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfiguration;
Expand All @@ -7,7 +7,7 @@

@AutoConfiguration
@ConditionalOnWebApplication
public class HtmxThymeleafConfiguration {
public class HtmxThymeleafAutoConfiguration {
@Bean
public HtmxDialect htmxDialect(ObjectMapper mapper) {
return new HtmxDialect(mapper);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.github.wimdeblauwe.htmx.spring.boot.thymeleaf.HtmxThymeleafAutoConfiguration
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
package io.github.wimdeblauwe.hsbt;
package io.github.wimdeblauwe.htmx.spring.boot.thymeleaf;

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

import io.github.wimdeblauwe.hsbt.mvc.PartialsController.TodoRepository;

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

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

@Bean
TodoRepository repository() {
return () -> 2;
}
}
Loading

0 comments on commit 805e29a

Please sign in to comment.