1. Introduction
In this article, we are going to present a Thymeleaf Code Editor embedded in the Spring Boot application. The sample application showed in this article will simulate creating code snippet functionality.
For more infor about Thymeleaf and Forms check below links:
Spring Boot with Thymeleaf
Forms in Thymeleaf
2. Dependencies
2.1. Maven dependencies
The sample application use three common Maven dependencies:
2.2. Front-End libraries
bootstrap - frontend framework for building responsive websites,
codemirror - great JavaScript library for code editor,
prism js - JavaScript syntax highlighter library.
Maven pom.xml
file have the following structure:
Copy
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>thymeleaf-bootstrap-code-editor</artifactId>
<properties>
<bootstrap.version>4.0.0-2</bootstrap.version>
<webjars-locator.version>0.30</webjars-locator.version>
<lombok.version>1.18.2</lombok.version>
</properties>
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>${bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>${webjars-locator.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3. Model, Controller, Service and Main Application class
In the model layer we have two classes:
Snippet - to store code snippet,
ModeInfo - Java representation of the CodeMirror.modeInfo
object.
The Snippet
class has the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class Snippet {
private String mime;
private String code;
private String lang;
}
The ModeInfo
has the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap.model;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class ModeInfo {
private String name;
private String mode;
private List<String> mimes;
private String mime;
private List<String> ext;
private List<String> alias;
}
HTTP GET and POST requests to the root context are handled by IndexController
class that has the following structure:
Copy
package com.frontbackend.thymeleaf.bootstrap.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.frontbackend.thymeleaf.bootstrap.model.Snippet;
import com.frontbackend.thymeleaf.bootstrap.util.MimeToLang;
@Controller
@RequestMapping({ "/", "/index" })
public class IndexController {
private final MimeToLang mimeToLang;
@Autowired
public IndexController(MimeToLang mimeToLang) {
this.mimeToLang = mimeToLang;
}
@GetMapping
public String main(Model model) {
model.addAttribute("snippet", new Snippet());
return "index";
}
@PostMapping
public String save(Snippet snippet, Model model) {
snippet.setLang("language-" + mimeToLang.getLangByMime(snippet.getMime()));
model.addAttribute("snippet", snippet);
return "saved";
}
}
In the save
method we determine the code language using mime type.
The MimeToLang
class is reponsible for mapping mime type into language. It uses CodeMirror.modeInfo
JSON that placed in /resources
folder.
Copy
package com.frontbackend.thymeleaf.bootstrap.util;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.frontbackend.thymeleaf.bootstrap.model.ModeInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MimeToLang {
public String getLangByMime(String mime) {
return loadModeInfo().stream()
.filter(modeInfo -> {
if (modeInfo.getMimes() != null && !modeInfo.getMimes()
.isEmpty()) {
return modeInfo.getMimes()
.contains(mime);
} else {
return modeInfo.getMime()
.equals(mime);
}
})
.map(modeInfo -> modeInfo.getName()
.toLowerCase())
.findFirst()
.orElse("Plain Text")
.toLowerCase();
}
private List<ModeInfo> loadModeInfo() {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(getClass().getClassLoader()
.getResourceAsStream("mime.json"),
new TypeReference<List<ModeInfo>>() {
});
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return Collections.emptyList();
}
}
The Application
is annotated with @SpringBootApplication to become the main class that starts the Spring Boot application server:
Copy
package com.frontbackend.thymeleaf.bootstrap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4. Templates
In the the presentation layer we have two Thymeleaf templates:
index.html - template where user can create code snippet,
saved.html - template for presenting formatted and highlighted syntax of the code submitted on the previous page.
The index.html
template has the following structure:
Copy
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot Thymeleaf Application - Bootstrap Code Editor</title>
<link th:rel="stylesheet" th:href="@{/webjars/bootstrap/4.0.0-2/css/bootstrap.min.css} "/>
<link th:rel="stylesheet" th:href="@{/assets/codemirror/codemirror.css} "/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container">
<a class="navbar-brand" href="/">Thymeleaf - Bootstrap Code Editor</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-sm-8 mt-5">
<form method="post" th:object="${snippet}">
<div class="form-group">
<label for="language">Select language</label>
<select class="form-control" th:name="mime" id="language" th:field="*{mime}">
</select>
</div>
<div class="form-group">
<label for="code">Code</label>
<div class="form-control">
<textarea id="code" rows="25" th:name="code" th:field="*{code}"></textarea>
</div>
</div>
<button class="btn btn-primary" type="submit">Submit form</button>
</form>
</div>
</div>
</div>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/webjars/popper.js/umd/popper.min.js}"></script>
<script th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
<script th:src="@{/assets/codemirror/codemirror.js}"></script>
<script th:src="@{/assets/codemirror/mode/meta.js}"></script>
<script th:src="@{/assets/codemirror/addon/mode/loadmode.js}"></script>
<script>
CodeMirror.modeURL = "/assets/codemirror/mode/%N/%N.js";
var textArea = document.getElementById('code');
var editor = CodeMirror.fromTextArea(textArea, {
lineNumbers: true
});
var languageSelect = $('#language');
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
var modeInfo = CodeMirror.modeInfo[i];
var mime = null;
if (modeInfo.hasOwnProperty('mimes')) {
mime = modeInfo.mimes[0];
} else {
mime = Array.isArray(modeInfo.mime) ? modeInfo.mime[0] : modeInfo.mime;
}
languageSelect.append("<option value='" + modeInfo.mime + "'>" + modeInfo.name + "</option>")
}
languageSelect.change(function (el) {
var mime = languageSelect.val();
var info = CodeMirror.findModeByMIME(mime);
if (info) {
var mode = info.mode;
editor.setOption("mode", mime);
CodeMirror.autoLoadMode(editor, mode);
}
});
</script>
</body>
</html>
The saved.html
view simple presents submitted values:
Copy
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot Thymeleaf Application - Bootstrap Code Editor</title>
<link th:rel="stylesheet" th:href="@{/webjars/bootstrap/4.0.0-2/css/bootstrap.min.css} "/>
<link th:rel="stylesheet" th:href="@{/assets/prism/prism.css} "/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container">
<a class="navbar-brand" href="/">Thymeleaf - Bootstrap Code Editor</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-sm-12 mt-5">
<pre><code th:class="${snippet.lang}" th:text="${snippet.code}"></code></pre>
</div>
</div>
<a th:href="@{/}" class="btn btn-primary">Go back</a>
</div>
<script th:src="@{/assets/prism/prism.js}"></script>
</body>
</html>
5. The output
The running application is available under http://locahost:8080
URL and presents the following functionality:
6. Conclusion
In this article, we presented how to create Thymeleaf Code Editor .
As usual, the code used in this article is available under our GitHub repository.
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}