Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
reviews:
auto_review:
base_branches: [ "htmx", "eta", "spring", "hono", "hono-web1", "hono-eta" ]
base_branches: ["htmx", "eta", "spring", "hono", "hono-web1", "hono-eta"]
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.java]
indent_style = space
indent_size = 2

[*.{html,css,json,yaml,yml,xml}]
indent_style = space
indent_size = 2
29 changes: 29 additions & 0 deletions .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: autofix.ci # needed to securely identify the workflow

on:
pull_request:
push:
branches: ["main"]

permissions:
contents: read

jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-java@v5
with:
java-version: "25"
distribution: "temurin"
cache: maven

- run: ./mvnw spotless:apply

- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run format

- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
6 changes: 0 additions & 6 deletions .idea/biome.xml

This file was deleted.

1 change: 0 additions & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/externalDependencies.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"printWidth": 120
}
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"vscjava.vscode-java-pack",
"vmware.vscode-boot-dev-pack",
"vscjava.vscode-lombok",
"craigrbroughton.htmx-attributes"
"craigrbroughton.htmx-attributes",
"esbenp.prettier-vscode"
]
}
13 changes: 7 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ A Contact Management hypermedia app built with Spring Boot, Thymeleaf, and htmx.

## Stack

| Layer | Technology |
|-------|------------|
| Framework | Spring Boot (Web MVC) |
| Templates | Thymeleaf |
| Layer | Technology |
| ---------- | ----------------------------------------- |
| Framework | Spring Boot (Web MVC) |
| Templates | Thymeleaf |
| Hypermedia | htmx (`htmx-spring-boot-thymeleaf` 5.0.0) |
| Database | H2 (in-memory) + Spring Data JPA |
| Utilities | Lombok |
| Database | H2 (in-memory) + Spring Data JPA |
| Utilities | Lombok |

## Development Commands

Expand All @@ -46,6 +46,7 @@ src/
## htmx Integration

The `htmx-spring-boot-thymeleaf` library provides:

- `HtmxRequest` injectable in controllers to detect htmx requests
- Thymeleaf dialect for htmx attributes (`hx-get`, `hx-post`, etc.)
- `HtmxResponse` for setting response headers (`HX-Redirect`, `HX-Trigger`, etc.)
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ open http://localhost:8080
3. **Run** - use the **Run** task via `Ctrl+Shift+P → Tasks: Run Task → Run`, or press `Ctrl+Shift+P → Spring Boot Dashboard: Run` from the Spring Boot extension

**Tips:**

- Spring Boot DevTools (already included) enables hot reload - save a file and changes apply automatically
- Thymeleaf templates hot-reload without a restart
- H2 console is available at `http://localhost:8080/h2-console`
Expand All @@ -54,15 +55,16 @@ open http://localhost:8080
4. **Run** - use the included **Contacts** run configuration (`.run/Contacts.run.xml`) from the toolbar, or run `spring-boot:run` from the Maven tool window

**Tips:**

- Spring Boot DevTools (already included) enables hot reload - recompile with `Ctrl+F9` without restarting
- Enable **"Build project automatically"** in settings for seamless reloads on save
- Enable **"Build project automatically"** (**Settings → Build, Execution, Deployment → Compiler**) and **"Allow auto-make to start even if developed application is currently running"** (**Settings → Advanced Settings**) for seamless reloads on save
- Thymeleaf templates hot-reload without a restart
- H2 console is available at `http://localhost:8080/h2-console`

### Scripts

| Command | Description |
|--------------------------|------------------|
| ------------------------ | ---------------- |
| `./mvnw spring-boot:run` | Start dev server |
| `./mvnw test` | Run tests |
| `./mvnw package` | Build jar |
Expand Down Expand Up @@ -107,7 +109,7 @@ src/
### Routes

| Method | Path | Description |
|----------|--------------------------|-------------------------------------|
| -------- | ------------------------ | ----------------------------------- |
| `GET` | `/` | Redirect to `/contacts` |
| `GET` | `/contacts` | List contacts (search + pagination) |
| `GET` | `/contacts/new` | New contact form |
Expand All @@ -123,7 +125,7 @@ src/
## Tech stack

| Layer | Technology |
|------------|----------------------------------------------------------------------------------------------------------|
| ---------- | -------------------------------------------------------------------------------------------------------- |
| Language | Java 25 |
| Framework | [Spring Boot](https://spring.io/projects/spring-boot) 4.0.3 |
| Templating | [Thymeleaf](https://www.thymeleaf.org) |
Expand All @@ -135,12 +137,12 @@ src/

This repo tracks the evolution of the app across different stacks - earlier branches used [Hono](https://hono.dev/) (a TypeScript web framework), while `main` is the current Spring Boot implementation.

| Branch | Summary | Description |
|------------------------------------------------------------------------------------|-------------------------------|--------------------------------------------------------------------------------------------------------|
| [`hono-web1`](https://github.com/yamcodes/contact.app/tree/hono-web1) | Hypermedia-Driven Application | Server-rendered HTML using links and forms. Full page reloads. Pure hypermedia, no client-side JS. |
| [`hono`](https://github.com/yamcodes/contact.app/tree/hono) | HDA + HTMX | Same architecture as `hono-web1`, enhanced with HTMX for partial updates without JSON or client-side state. |
| [`hono-eta`](https://github.com/yamcodes/contact.app/tree/hono-eta) | HDA (Eta templates) | Same architecture as `hono-web1`, using string-based Eta templates instead of JSX. |
| **[`main`](https://github.com/yamcodes/contact.app/tree/main)** (You're here!) | **HDA + Spring Boot** | **Same architecture, rewritten in Java with Spring Boot, Thymeleaf, and htmx-spring-boot.** |
| Branch | Summary | Description |
| ------------------------------------------------------------------------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| [`hono-web1`](https://github.com/yamcodes/contact.app/tree/hono-web1) | Hypermedia-Driven Application | Server-rendered HTML using links and forms. Full page reloads. Pure hypermedia, no client-side JS. |
| [`hono`](https://github.com/yamcodes/contact.app/tree/hono) | HDA + HTMX | Same architecture as `hono-web1`, enhanced with HTMX for partial updates without JSON or client-side state. |
| [`hono-eta`](https://github.com/yamcodes/contact.app/tree/hono-eta) | HDA (Eta templates) | Same architecture as `hono-web1`, using string-based Eta templates instead of JSX. |
| **[`main`](https://github.com/yamcodes/contact.app/tree/main)** (You're here!) | **HDA + Spring Boot** | **Same architecture, rewritten in Java with Spring Boot, Thymeleaf, and htmx-spring-boot.** |

## License

Expand Down
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions docs/thymeleaf-fragments.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

```html
<!-- fragment -->
<!--suppress HtmlUnknownAttribute -->
<div th:fragment="contact-fields(contact)" th:object="${contact}">
<input th:value="*{first}" .../>
<input th:value="*{first}" ... />
</div>

<!-- usage -->
<div th:insert="~{fragments/contact-fields :: contact-fields(${contact})}"></div>
```

| | |
|----------------------|----------------------------------------------------------|
| -------------------- | -------------------------------------------------------- |
| **Explicit** | Fragment declares its dependencies in the signature |
| **Portable** | Works regardless of what's in the model |
| **Self-documenting** | You know what it needs just by looking at it |
Expand All @@ -23,18 +24,21 @@

## Model-Access Fragment

<!-- noinspection HtmlUnknownAttribute -->

```html
<!-- fragment -->
<!--suppress HtmlUnknownAttribute -->
<div th:fragment="contact-fields">
<input th:value="${contact.first}" .../>
<input th:value="${contact.first}" ... />
</div>

<!-- usage -->
<div th:insert="~{fragments/contact-fields :: contact-fields}"></div>
```

| | |
|-----------------------|-----------------------------------------------------------------------------|
| --------------------- | --------------------------------------------------------------------------- |
| **Implicit** | Fragment depends on `contact` being in the model, declared via `@thymesVar` |
| **Coupled** | Only works in contexts where the model has `contact` |
| **Simpler call site** | No arguments to pass |
Expand All @@ -46,6 +50,8 @@

A third pattern from the Layout Dialect itself. The calling template defines content inline using `layout:fragment`, and the reusable template exposes slots for it. Best for passing **HTML content** (not data) into a component - think modals, cards, panels.

<!-- noinspection HtmlUnknownAttribute -->

```html
<!-- reusable modal template -->
<section class="modal" layout:fragment="modal(title)">
Expand All @@ -62,7 +68,7 @@ A third pattern from the Layout Dialect itself. The calling template defines con
```

| | |
|----------------------|------------------------------------------------------------------|
| -------------------- | ---------------------------------------------------------------- |
| **For HTML content** | Caller defines the body, reusable template defines the structure |
| **Not for data** | Doesn't solve passing a Java object to a fragment |
| **Dialect-only** | `layout:insert` is not standard Thymeleaf |
Expand All @@ -72,7 +78,7 @@ A third pattern from the Layout Dialect itself. The calling template defines con
## When to use which

| Scenario | Approach |
|----------------------------------------------------------|---------------|
| -------------------------------------------------------- | ------------- |
| Fragment used in many places with different object names | Parameterized |
| Fragment tightly coupled to one model attribute | Model-access |
| IDE squiggles are a dealbreaker | Model-access |
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
"private": true,
"scripts": {
"build:css": "tailwindcss -i src/main/resources/static/css/input.css -o src/main/resources/static/css/output.css --minify",
"watch:css": "tailwindcss -i src/main/resources/static/css/input.css -o src/main/resources/static/css/output.css --watch"
"watch:css": "tailwindcss -i src/main/resources/static/css/input.css -o src/main/resources/static/css/output.css --watch",
"format": "prettier --write '**/*.{html,json,yaml,yml,md}'",
"format:check": "prettier --check '**/*.{html,json,yaml,yml,md}'"
},
"devDependencies": {
"@tailwindcss/cli": "^4.0.0",
"daisyui": "^5.0.0"
"daisyui": "^5.0.0",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2"
}
}
22 changes: 22 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</scm>
<properties>
<java.version>25</java.version>
<jackson-bom.version>3.1.0</jackson-bom.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -140,6 +141,27 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.44.5</version>
<configuration>
<java>
<googleJavaFormat>
<version>1.35.0</version>
<style>GOOGLE</style>
<reflowLongStrings>true</reflowLongStrings>
</googleJavaFormat>
</java>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/codes/yam/contacts/ContactController.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ public String contacts(
model.addAttribute("contactPage", page);
model.addAttribute("contacts", page.getContent());
model.addAttribute("search", q);
model.addAttribute("sort", pageable.getSort().stream()
.map(o -> o.getProperty() + "," + o.getDirection().name().toLowerCase())
.collect(Collectors.joining(",")));
if ("nav-search".equals(htmxRequest.getTriggerId())) //noinspection SpringMVCViewInspection
model.addAttribute(
"sort",
pageable.getSort().stream()
.map(o -> o.getProperty() + "," + o.getDirection().name().toLowerCase())
.collect(Collectors.joining(",")));
if ("nav-search".equals(htmxRequest.getTriggerId())) {
//noinspection SpringMVCViewInspection
return "fragments/contact-list-rows :: rows";
}
return "contacts/index";
}

Expand Down
5 changes: 4 additions & 1 deletion src/main/resources/static/css/input.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
@import "tailwindcss";
@source "../../templates";

@plugin "daisyui";
/* noinspection CssUnknownProperty */
@plugin "daisyui" {
themes: false;
}

/* noinspection CssUnknownProperty */
@plugin "daisyui/theme" {
Expand Down
Loading
Loading