Spring ViewComponent allows you to create typesafe, reusable & encapsulated server rendered ui components.
- What’s a ViewComponent?
- Render a ViewComponent
- Nesting ViewComponents:
- Local Development
- ViewAction: Interactivity with HTMX
- Installation
- Composing pages from components
- Serverless components - Spring Cloud Function support
Think of ViewComponents as an evolution of the presenter pattern, inspired by React.
A ViewComponent is a Spring Bean that defines the context for our Template:
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
// HomeViewComponent.kt
@ViewComponent
class SimpleViewComponent{
fun render() = SimpleView("Hello World")
data class SimpleView(val helloWorld: String) : ViewContext
}
A ViewComponent always need 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
// HomeViewComponent.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>
We can then call the render method in our controller to render the template.
Java
@Controller
public class SimpleController {
private final SimpleViewComponent simpleViewComponent;
public TestController(SimpleViewComponent simpleViewComponent) {
this.simpleViewComponent = simpleViewComponent;
}
@GetMapping("/")
ViewContext simple() {
return simpleViewComponent.render();
}
}
Kotlin
// Router.kt
@Controller
class SimpleController(
private val simpleViewComponent: SimpleViewComponent,
) {
@GetMapping("/")
fun simpleComponent() = simpleViewComponent.render()
}
If you want to get started right away you can find examples for all possible language combinations here: Examples
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
@ViewComponent
public
class LayoutViewComponent {
private record LayoutView(ViewContext nestedViewComponent) implements ViewContext {
}
public ViewContext render(ViewContext nestedViewComponent) {
return new LayoutView(nestedViewComponent);
}
}
Kotlin
@ViewComponent
class LayoutViewComponent {
data class LayoutView(val nestedViewComponent: ViewContext) : ViewContext
fun render(nestedViewComponent: ViewContext) = LayoutView(nestedViewComponent)
}
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>
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>
You can enable hot-reloading of the templates in development:
spring.view-component.local-development=true
With ViewActions you can create interactive ViewComponents based on htmx without having to reload the page.
You define a ViewAction inside your Thymeleaf/JTE template with the view:action
attribute.
// ActionViewComponent.html
<!--/*@thymesVar id="actionView" type="de.tschuehly.example.thymeleafjava.web.action.ActionViewComponent.ActionView"*/-->
<script defer src="https://unpkg.com/[email protected]"></script>
<button view:action="countUp">Default ViewAction [GET]</button>
<h3 th:text="${actionView.counter()}"></h3>
Here is the corresponding ViewComponent class that has a @GetViewAction
annotation on the countUp method.
As you can see the attribute value of the view:action="countUp"
correlates to the countUp method in our ViewComponent class.
Java
@ViewComponent
public class ActionViewComponent {
Integer counter = 0;
public record ActionView(Integer counter) implements ActionViewContext {
}
public ViewContext render() {
return new ActionView(counter);
}
@GetViewAction(path = "/customPath/countUp")
public ViewContext countUp() {
counter += 1;
return render();
}
}
Kotlin
@ViewComponent
class ActionViewComponent {
data class ActionView(val counter: Int) : ViewContext
fun render() = ActionView(counter)
var counter: Int = 0
@GetViewAction("/customPath/countUp")
fun countUp(): IViewContext {
counter += 1
return render()
}
}
Behind the scenes at build time Spring ViewComponent parses the template to htmx attributes using an annotation processor.
The hx-get attribute will create a http get request to the /actionviewcomponent/countup
endpoint that is automatically generated.
The /actionviewcomponent/countup
endpoint will return the re-rendered ActionViewComponent template.
The hx-target="#actionviewcomponent"
attribute will swap the returned HTML to the div with the id="actionviewcomponent"
that will wrap the view component.
<div id="actionviewcomponent" style="display: contents;">
<script defer src="https://unpkg.com/[email protected]"></script>
<h2>ViewAction Get CountUp</h2>
<button hx-get="/actionviewcomponent/countup" hx-target="#actionviewcomponent">
Default ViewAction [GET]
</button>
</div>
You can also pass a custom path as annotation parameter: @PostViewAction("/customPath/addItemAction")
You can use different ViewAction Annotations that map to the corresponding htmx ajax methods:
@GetViewAction
@PostViewAction
@PutViewAction
@PatchViewAction
@DeleteViewAction
If you are using Maven you need to configure the annotation processor like this:
Annotation Processor Configuration
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-core</artifactId>
<version>${de.tschuehly.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
LATEST_VERSION on Maven Central
Gradle
implementation("de.tschuehly:spring-view-component-thymeleaf:LATEST_VERSION")
annotationProcessor("de.tschuehly:spring-view-component-core:LATEST_VERSION")
Maven
<dependency>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-core</artifactId>
<version>LATEST_VERSION</version>
</dependency>
<dependency>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-thymeleaf</artifactId>
<version>LATEST_VERSION</version>
</dependency>
Both, Java DSL and Kotlin DSL are supported:
LATEST_VERSION on Maven Central
Gradle
implementation("de.tschuehly:spring-view-component-jte:LATEST_VERSION")
annotationProcessor("de.tschuehly:spring-view-component-core:LATEST_VERSION")
Maven
<dependency>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-core</artifactId>
<version>LATEST_VERSION</version>
</dependency>
<dependency>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-jte</artifactId>
<version>LATEST_VERSION</version>
</dependency>
LATEST_VERSION on Maven Central
Gradle
implementation("de.tschuehly:spring-view-component-kte:LATEST_VERSION")
annotationProcessor("de.tschuehly:spring-view-component-core:LATEST_VERSION")
Maven
<dependency>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-core</artifactId>
<version>LATEST_VERSION</version>
</dependency>
<dependency>
<groupId>de.tschuehly</groupId>
<artifactId>spring-view-component-kte</artifactId>
<version>LATEST_VERSION</version>
</dependency>
!!! Currently only supported in Thymeleaf !!!
If you want to compose a page/response from multiple components you can use the ViewContextContainer
as response in
your controller, this can be used for htmx out of band responses.
@Controller
class Router(
private val homeViewComponent: HomeViewComponent,
private val navigationViewComponent: NavigationViewComponent,
) {
@GetMapping("/multi-component")
fun multipleComponent() = ViewContextContainer(
navigationViewComponent.render(),
homeViewComponent.render()
)
}
Currently only supported in Thymeleaf !!!
If you want to deploy your application on a serverless platform such as AWS Lambda or Azure Functions you can easily do that with the Spring Cloud Function support.
Just add the dependency implementation("org.springframework.cloud:spring-cloud-function-context")
to your
build.gradle.kts.
Create a @ViewComponent that implements the functional interface Supplier<ViewContext>
. Instead of the render()
function we will now override the get method of the Supplier interface.
If you start your application the component should be automatically rendered on http://localhost:8080
@ViewComponent
class HomeViewComponent(
private val exampleService: ExampleService,
) : Supplier<ViewContext> {
override fun get() = ViewContext(
"helloWorld" toProperty exampleService.getHelloWorld(),
"coffee" toProperty exampleService.getCoffee()
)
}