Spring Boot + Bootstrap + Thymeleaf Price Range Slider

July 06, 2020 No comments Spring Boot Bootstrap Thymeleaf Price Range Slider

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:

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 }}