Spring Boot + Bootstrap + Thymeleaf Price Range Slider

January 15, 2020 No comments Spring Boot Bootstrap Thymeleaf Price Range Slider

1. Introduction

In this article, we are going to present Thymeleaf Price Range Slider embedded in a Spring Boot application. We will use the Bootstrap framework to create a responsive website and bootstrap-slider library for the awesome slider.

Looking for more information about Thymeleaf and Spring Boot? check below links:
Spring Boot with Thymeleaf
Working with forms in Thymeleaf

2. Maven dependencies

This example Spring Boot application will use two dependencies:

  • bootstrap webjar - to create a responsive website easily,
  • springframework - for embedded Spring Boot web container.

Our Maven pom.xml file will 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-price-range-slider</artifactId>

    <properties>
        <bootstrap.version>4.0.0-2</bootstrap.version>
        <webjars-locator.version>0.30</webjars-locator.version>
        <font-awesome.version>5.11.2</font-awesome.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.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 Application class

To present how to create and handle Price Range Slider in Thymeleaf we used two controllers:

  • IndexController - handle GET request (to the root context /) that returns main website and POST request that will process input price range,
  • ProductController - this controller will return a rendered list of products filtered by the selected price range.

The IndexController have 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.PriceRange;
import com.frontbackend.thymeleaf.bootstrap.service.ProductService;

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

    private final ProductService productService;

    @Autowired
    public IndexController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public String main(Model model) {
        model.addAttribute("priceRange", new PriceRange(5, 100));
        model.addAttribute("products", productService.getMockedProducts());
        return "index";
    }

    @PostMapping
    public String save(PriceRange priceRange, Model model) {
        model.addAttribute("range", priceRange);
        return "saved";
    }
}

And the ProductController 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.RequestMapping;

import com.frontbackend.thymeleaf.bootstrap.model.PriceRange;
import com.frontbackend.thymeleaf.bootstrap.service.ProductService;

@Controller
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public String filterProducts(PriceRange priceRange, Model model) {
        model.addAttribute("products", productService.filterProducts(priceRange.getMin(), priceRange.getMax()));
        return "products";
    }
}

The ProductService will return filtered products stored in mockedProducts.json file:

package com.frontbackend.thymeleaf.bootstrap.service;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.frontbackend.thymeleaf.bootstrap.model.Product;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class ProductService {

    public List<Product> filterProducts(int min, int max) {
        return getMockedProducts().stream()
                                  .filter(product -> product.getPrice() >= min && product.getPrice() <= max)
                                  .collect(Collectors.toList());
    }

    public List<Product> getMockedProducts() {
        ObjectMapper objectMapper = new ObjectMapper();

        try {
            return objectMapper.readValue(getClass().getClassLoader()
                                                    .getResourceAsStream("mockedProducts.json"),
                    new TypeReference<List<Product>>() {
                    });
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }

        return Collections.emptyList();
    }
}

In the model layer we used two POJO objects:

package com.frontbackend.thymeleaf.bootstrap.model;

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

@Setter
@Getter
@NoArgsConstructor
public class Product {

    private String name;
    private String material;
    private String brand;
    private Double price;
}

We use lombok to generate setters, getters and contructors int our POJO classes.

package com.frontbackend.thymeleaf.bootstrap.model;

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

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PriceRange {

    private int min;
    private int max;
}

PriceRange class will be the main class used in our Thymeleaf form (command object).

4. Templates

This example Spring Boot application on the root context path serves a index.html file which have 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 Price Range Slider</title>

    <link th:rel="stylesheet" th:href="@{assets/bootstrap-slider/css/bootstrap-slider.css}"/>
    <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 Price Range Slider</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">

            <form method="post" th:object="${priceRange}">
                <div class="form-group mt-5">
                    <label for="priceRange">Filter products by price</label>
                    <div class="form-control">
                        <b class="mr-2">€ 10</b> <input id="priceRange" type="text" class="span2" value=""
                                                        data-slider-min="1"
                                                        data-slider-max="100" data-slider-step="2"
                                                        data-slider-value="[5,100]" data-slider-tooltip="show"/> <b
                            class="ml-2">€ 100</b>
                        <input type="hidden" id="rangeMin" th:field="*{min}"/>
                        <input type="hidden" id="rangeMax" th:field="*{max}"/>
                    </div>
                </div>

                <div id="products" class="mb-3">
                    <div th:replace="products :: list"></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:rel="stylesheet" th:src="@{assets/bootstrap-slider/bootstrap-slider.js}"></script>

<script>
    $("#priceRange").slider({});
    $("#priceRange").on("slideStop", function (stopEvent) {
        var range = stopEvent.value;
        $("#rangeMin").val(range[0]);
        $("#rangeMax").val(range[1]);

        $.get("/products?min=" + range[0] + "&max=" + range[1], function (data) {
            $("#products").html(data);
        });
    });
</script>

</body>
</html>

We used bootstrap-slider plugin to make a custom slider use to filter products by specified price range.

products.html is a special Thymeleaf template that can be used as a fragment. Fragments in Thymeleaf are a special piece of HTML code that could be included in different Thymeleaf templates. We use it to define a list of products just once and include it where we want to.

The products.html template will have the following structure:

<div class="row" th:fragment="list" xmlns:th="http://www.thymeleaf.org">
    <div th:each="product, stat : ${products}" class="col-sm-4">
        <div class="card mb-3">
            <div class="card-body">
                <h5 class="card-title" th:text="${product.name}">Product name</h5>
                <p class="card-text" th:text="${product.price}">Price</p>
                <a href="#" class="btn btn-primary">Buy</a>
            </div>
        </div>
    </div>
</div>

5. The output

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

Thymeleaf bootstrap price range slider

6. Conclusion

In this article, we showcased how to create Thymeleaf Price Range Slider in a Spring Boot application.

As usual, the full code used in this example is available under our GitHub Repository.

{{ message }}

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