Spring Boot + Bootstrap + Thymeleaf Input Spinner

April 18, 2020 No comments Spring Boot Bootstrap Thymeleaf Input Spinner

Spring Boot + Bootstrap + Thymeleaf Input Spinner

1. Introduction

In this article, we are going to present Thymeleaf Input Spinner component. Although HTML 5 provides a dedicated input spinner (with type="number" attribute) we decided to use the custom one created with JavaScript library bootstrap-input-spinner. Why? because it is responsive and looks great.

You can find more information about Thymeleaf and Forms in the following links:
Thymeleaf Tutorial
Working with Forms in Thymeleaf

2. Dependencies

2.1. Maven dependencies

To show how Input Spinner works, we used a sample Spring Boot application created as a Maven project using three main dependencies:

2.2. Front-End libraries
  • bootstrap - a frontend framework for creating responsive websites.
  • bootstrap-input-spinner - a JavaScript library for creating responsive input spinner components for websites that uses Bootstrap framework.

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-input-spinner</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>
        <font-awesome.version>5.11.2</font-awesome.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.webjars</groupId>
            <artifactId>font-awesome</artifactId>
            <version>${font-awesome.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, and Main Application class

The sample application takes three parameters as input: temp, wind and humidity using spinner component.

The main object in the model layer(Thymeleaf command object) has the following structure:

package com.frontbackend.thymeleaf.bootstrap.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
public class Weather {

    private float temp;

    private float wind;

    private float humidity;

}

GET requests to the root context are handled by IndexController class:

package com.frontbackend.thymeleaf.bootstrap.controller;

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.Weather;

@Controller
@RequestMapping({ "/", "/index" })
public class IndexController {

    @GetMapping
    public String main(Model model) {
        model.addAttribute("weather", new Weather());
        return "index";
    }

    @PostMapping
    public String save(Weather weather, Model model) {
        model.addAttribute("weather", weather);
        return "saved";
    }
}

Main Spring Boot application class has the following structure:

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 presentation layer, we have two Thymeleaf templats:

  • index.html - on this view user can enter wheather parameters using spinner components,
  • saved.html - this view is responsibe for presenting the submitted values.

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 Input Spinner</title>

    <link th:rel="stylesheet" th:href="@{/webjars/bootstrap/4.0.0-2/css/bootstrap.min.css} "/>
    <link th:rel="stylesheet" th:href="@{/webjars/font-awesome/5.11.2/css/all.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 Input Spinner</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-lg-5 mt-5">
            <form method="post" th:object="${weather}">

                <div class="form-group">
                    <label for="temp">Temp</label>
                    <input data-suffix="°C" value="0" min="-50" max="50" type="number" class="form-control" id="temp"
                           autocomplete="off" th:field="*{temp}"/>
                </div>

                <div class="form-group">
                    <label for="wind">wind</label>
                    <input data-suffix="m/s" value="0" min="0" max="50" type="number" class="form-control" id="wind"
                           autocomplete="off" th:field="*{wind}" step="0.5" data-decimals="2"/>
                </div>

                <div class="form-group">
                    <label for="humidity">Humidity</label>
                    <input data-suffix="%" value="0" min="0" max="100" type="number" class="form-control" id="humidity"
                           autocomplete="off" th:field="*{humidity}" step="0.5" data-decimals="2"/>
                </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/bootstrap-input-spinner/bootstrap-input-spinner.js}"></script>

<script>
    var $input = $("input[type='number']");
    $input.inputSpinner();
</script>


</body>
</html>

We used an external JavaScript plugin for creating responsive Input Spinners: https://github.com/shaack/bootstrap-input-spinner.

To initialize bootstrap-input-spinner library, and attach it to all inputs with type = number, we use the following JS code:

var $input = $("input[type='number']");
    $input.inputSpinner();

The saved.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 Input Spinner</title>

    <link th:rel="stylesheet" th:href="@{webjars/bootstrap/4.0.0-2/css/bootstrap.min.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 Input Spinner</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">
            <p>
                <label>Temp: </label><br/>
                <strong th:text="${weather.temp}"></strong> <span>°C</span>
            </p>

            <p>
                <label>Wind: </label><br/>
                <strong th:text="${weather.wind}"></strong> <span>m/s</span>
            </p>

            <p>
                <label>Humidity: </label><br/>
                <strong th:text="${weather.humidity}"></strong> <span>%</span>
            </p>

            <a th:href="@{/}" class="btn btn-primary">Go back</a>
        </div>
    </div>
</div>

</body>
</html>

5. The output

The running application is available under http://locahost:8080 URL and presents the following functionality:

Thymeleaf bootstrap input spinner

6. Conclusion

In this article, we presented Thymeleaf Input Spinner component. Although browsers support spinner input with type=number we choose an external library that allows us to create awesome, responsive and bootstrap-style components.

As usual, the code used in this article is available under our GitHub repository.

{{ message }}

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