Spring Boot + Bootstrap + Thymeleaf Code Editor

April 18, 2020 No comments Spring Boot Bootstrap Thymeleaf Code Editor

Spring Boot + Bootstrap + Thymeleaf Code Editor

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:

<?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:

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:

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:

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.

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:

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:

<!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:

<!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:

Thymeleaf bootstrap code editor

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.

{{ message }}

{{ 'Comments are closed.' | trans }}