Skip to content
Merged
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: 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.

3 changes: 2 additions & 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