visit
- I'll assume the viewpoint of a backend developer
- No front-end build step: no TypeScript, no minification, etc.
- All dependencies are managed from the backend app, i.e., Maven
WebJars are client-side web libraries (e.g. jQuery & Bootstrap) packaged into JAR (Java Archive) files.
- Explicitly and easily manage the client-side dependencies in JVM-based web applications
- Use JVM-based build tools (e.g. Maven, Gradle, sbt, ...) to download your client-side dependencies
- Know which client-side dependencies you are using
- Transitive dependencies are automatically resolved and optionally loaded via RequireJS
- Deployed on
- Public CDN, generously provided by
--
<dependencies>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>alpinejs</artifactId>
<version>3.14.1</version>
</dependency>
</dependencies>
The framework's responsibility is to expose the assets under a URL. For example, Spring Boot does it in the WebMvcAutoConfiguration
class:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(), //1
"classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
"/webjars/**"
Inside the JAR, you can reach assets by their respective path and name. The agreed-upon structure is to store the assets inside resources/webjars/<library>/<version>
. Here's the structure of the alpinejs-3.14.1.jar
:
META-INF
|_ MANIFEST.MF
|_ maven.org.webjars.npm.alpinejs
|_ resources.webjars.alpinejs.3.14.1
|_ builds
|_ dist
|_ cdn.js
|_ cdn.min.js
|_ src
|_ package.json
Within Spring Boot, you can access the non-minified version with /webjars/alpinejs/3.14.1/dist/cdn.js
.
The WebJars Locator project aims to avoid all these issues by providing a path with no version, i.e., /webjars/alpinejs/dist/cdn.js
. You can achieve this by adding the webjars-locator
JAR to your dependencies:
<dependencies>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>alpinejs</artifactId>
<version>3.14.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.52</version>
</dependency>
</dependencies>
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf's main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.
--
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod: ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
</tr>
</tbody>
</table>
Name
and Price
. When you use it in the server, Thymeleaf kicks in and renders the value computed from th:text
, #{msgs.headers.name}
and #{msgs.headers.price}
.$
operator queries for a Spring bean of the same name passed to the model. ${prod.name}
is equivalent to model.getBean("prod").getName()"
.#
calls a function.th:each
allows for loops.
data class Todo(val id: Int, var label: String, var completed: Boolean = false) //1
fun config() = beans {
bean {
mutableListOf( //2
Todo(1, "Go to the groceries", false),
Todo(2, "Walk the dog", false),
Todo(3, "Take out the trash", false)
)
}
bean {
router {
GET("/") {
ok().render( //3
"index", //4
mapOf("title" to "My Title", "todos" to ref<List<Todo>>()) //5
)
}
}
}
}
Define the Todo
class.
Add an in-memory list to the bean factory. In a regular app, you'd use a Repository
to read from the database.
The template is src/main/resources/templates/index.html
with Thymeleaf attributes.
Thymeleaf offers a th:inline="javascript"
attribute on the <script>
tag. It renders the server-side data as JavaScript variables. The documentation explains it much better than I ever could:
The first thing we can do with script inlining is writing the value of expressions into our scripts, like:
<script th:inline="javascript"> /*<![CDATA[*/ ... var username = /*[[${session.user.name}]]*/ 'Sebastian'; ... /*]]>*/ </script>
The
/*[[...]]*/
syntax, instructs Thymeleaf to evaluate the contained expression. But there are more implications here:
--
Being a javascript comment
(/*...*/)
, our expression will be ignored when displaying the page statically in a browser.The code after the inline expression (
'Sebastian'
) will be executed when displaying the page statically.- Thymeleaf will execute the expression and insert the result, but it will also remove all the code in the line after the inline expression itself (the part that is executed when displayed statically).
<script th:inline="javascript">
/*<![CDATA[*/
window.title = /*[[${title}]]*/ 'A Title'
window.todos = /*[[${todos}]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }]
/*]]>*/
</script>
<script>
/*<![CDATA[*/
window.title = "My title";
window.todos: [{"id":1,"label":"Go to the groceries","completed":false},{"id":2,"label":"Walk the dog","completed":false},{"id":3,"label":"Take out the trash","completed":false}]
/*]]>*/
</script>
Go further:
Originally published at on September 15th, 2024