tschuehly / spring-view-component

Server-side UI components with spring boot
MIT License
208 stars 11 forks source link
htmx jte kotlin spring spring-boot ssr thymeleaf

image

Spring ViewComponent allows you to create typesafe, reusable & encapsulated server-rendered UI components.

Table of Contents

What’s a ViewComponent?

ViewComponents consolidate the logic needed for a template into a single class, resulting in a cohesive object that is easy to understand. (Source: ViewComponent for Rails)

A Spring ViewComponent is a Spring Bean that defines the context for our Template:

Java ```java @ViewComponent public class SimpleViewComponent { public record SimpleView(String helloWorld) implements ViewContext { } public SimpleView render() { return new SimpleView("Hello World"); } } ```

We define the context by creating a record that implements the ViewContext interface

Next, we add the @ViewComponent annotation to a class and define a method that returns the SimpleView record.

Kotlin ```kotlin // SimpleViewComponent.kt @ViewComponent class SimpleViewComponent { fun render() = SimpleView("Hello World") data class SimpleView(val helloWorld: String) : ViewContext } ```

A ViewComponent always needs a corresponding HTML Template. We define the Template in the SimpleViewComponent.[html/jte/kte] In the same package as our ViewComponent class.

We can use Thymeleaf

// SimpleViewComponent.html
<!--/*@thymesVar id="d" type="de.tschuehly.example.thymeleafjava.web.simple.SimpleViewComponent.SimpleView"*/-->
<div th:text="${simpleView.helloWorld()}"></div>

or JTE

// SimpleViewComponent.jte
@param de.tschuehly.example.jte.web.simple.SimpleViewComponent.SimpleView simpleView
<div>${simpleView.helloWorld()}</div>

or KTE

@param simpleView: de.tschuehly.kteviewcomponentexample.web.simple.SimpleViewComponent.SimpleView
<div>
  <h2>This is the SimpleViewComponent</h2>
  <div>${simpleView.helloWorld}</div>
</div>

Render a ViewComponent

We can then call the render method in our controller to render the template.

Java ```java @Controller public class SimpleController { private final SimpleViewComponent simpleViewComponent; public TestController(SimpleViewComponent simpleViewComponent) { this.simpleViewComponent = simpleViewComponent; } @GetMapping("/") ViewContext simple() { return simpleViewComponent.render(); } } ```
Kotlin ```kotlin // Router.kt @Controller class SimpleController( private val simpleViewComponent: SimpleViewComponent, ) { @GetMapping("/") fun simpleComponent() = simpleViewComponent.render() } ```

Examples

If you want to get started right away you can find examples for all possible language combinations here: Examples

Nesting ViewComponents:

We can nest components by passing a ViewContext as property of our record, if we also have it as parameter of our render method we can easily create layouts:

Java ```java @ViewComponent public class LayoutViewComponent { private record LayoutView(ViewContext nestedViewComponent) implements ViewContext { } public ViewContext render(ViewContext nestedViewComponent) { return new LayoutView(nestedViewComponent); } } ```
Kotlin ```kotlin @ViewComponent class LayoutViewComponent { data class LayoutView(val nestedViewComponent: ViewContext) : ViewContext fun render(nestedViewComponent: ViewContext) = LayoutView(nestedViewComponent) } ```

Thymeleaf

In Thymeleaf we render the passed ViewComponent with the view:component="${viewContext}" attribute.


<nav>
  This is a navbar
</nav>
<!--/*@thymesVar id="layoutView" type="de.tschuehly.example.thymeleafjava.web.layout.LayoutViewComponent.LayoutView"*/-->
<div view:component="${layoutView.nestedViewComponent()}"></div>
<footer>
  This is a footer
</footer>

JTE / KTE

In JTE/KTE we can just call the LayoutView record directly in an expression:

@param layoutView: de.tschuehly.kteviewcomponentexample.web.layout.LayoutViewComponent.LayoutView
<nav>
  This is a Navbar
</nav>
<body>
${layoutView.nestedViewComponent}
</body>
<footer>
  This is a footer
</footer>

Local Development Configuration

You can enable hot-reloading of the templates in development, you need to have Spring Boot DevTools as a dependency.

spring.view-component.local-development=true

Installation

Thymeleaf:

LATEST_VERSION on Maven Central

Gradle ```kotlin implementation("de.tschuehly:spring-view-component-thymeleaf:0.8.3") sourceSets { main { resources { srcDir("src/main/java") exclude("**/*.java") } } } ```
Maven ```xml de.tschuehly spring-view-component-thymeleaf 0.8.3 src/main/java **/*.html src/main/resources maven-resources-plugin 3.3.0 ```

JTE

LATEST_VERSION on Maven Central

Gradle ```kotlin plugins { id("gg.jte.gradle") version("3.1.12") } implementation("de.tschuehly:spring-view-component-jte:0.8.3") jte{ generate() sourceDirectory.set(kotlin.io.path.Path("src/main/java")) } ```
Maven ```xml de.tschuehly spring-view-component-jte 0.8.3 gg.jte jte-maven-plugin 3.1.12 ${project.basedir}/src/main/java Html generate-sources generate ```

KTE

LATEST_VERSION on Maven Central

Gradle ```kotlin plugins { id("gg.jte.gradle") version ("3.1.12") } dependencies { implementation("de.tschuehly:spring-view-component-kte:0.8.3") } jte { generate() sourceDirectory.set(kotlin.io.path.Path("src/main/kotlin")) } ```

Technical Implementation

Spring ViewComponent wraps the Spring MVC container using an AspectJ Aspect and automatically resolves the template and puts the ViewContext in the ModelAndViewContainer

image