Goal: This demonstration is performed on a set of students' data. We have written a method to generate sample data.
Table of Contents
- 1. Initialize the project with the following dependencies
- 2. Set the application properties
- 3. Create the Student entity
- 4. Enum to denote the class of student
- 5. Create JPA repository of entity
- 6. Create the search & filter command object (CO)
- 7. Create a data transfer object (DTO) of the Entity for returning the response
- 8. Create a service for implementing the business login
- 9. Create a controller
- 10. Create a utility class for date conversions
- 11. Create the HTML Data Table design
- 12. Add CSS for data table design
- 14. Add JavaScript code to handle AJAX calls
- 15. Screens
1. Initialize the project with the following dependencies
- Spring Web
- Thymeleaf
- Spring Data JPA
- MySql
- Lombok
- Spring Boot Dev Tools
2. Set the application properties
spring.datasource.url=jdbc:mysql://localhost:3306/paginationTutorial?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false
server.port=8889
3. Create the Student entity
@Getter
@Setter
@NoArgsConstructor
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true)
private String email;
private Klass klass;
private LocalDate dateOfAdmission;
public Student(String name, String email, Klass klass, LocalDate dateOfAdmission) {
this.name=name;
this.email=email;
this.klass=klass;
this.dateOfAdmission = dateOfAdmission;
}
}
4. Enum to denote the class of student
@Getter
public enum Klass {
SIXTH("Sixth"),
SEVENTH("Seventh"),
EIGHTH("Eighth"),
NINTH("Ninth"),
TENTH("Tenth"),
ELEVENTH("Eleventh"),
TWELFTH("Twelfth");
private String std;
Klass(String std){this.std=std;}
}
5. Create JPA repository of entity
public interface StudentRepository extends JpaRepository<Student,Long> {
}
6. Create the search & filter command object (CO)
Command Objects will contain the request parameters. It is used to perform to request with page specifications.
@Getter
@Setter
public class StudentSearchCO {
Optional<Integer> page;
Optional<Integer> size;
private Klass klass;
private String dateOfAdmission;
private String srchTxt;
}
7. Create a data transfer object (DTO) of the Entity for returning the response
DTOs are simple POJO classes that are sent with response entities.
@Getter
@Setter
@NoArgsConstructor
public class StudentDTO {
private Long id;
private String name;
private String email;
private String klass;
private String dateOfAdmission;
public StudentDTO(Student r) {
this.id =r.getId();
this.name=r.getName();
this.email=r.getEmail();
this.klass= r.getKlass().getStd();
this.dateOfAdmission = Utils.convertDateToStr(r.getDateOfAdmission(),"dd/MM/yyyy");
}
}
8. Create a service for implementing the business login
We will perform the filter on student name, class, and date of admission, and use CriteriaBuilder with the help of EntityManager.
@Service
public class StudentService {
@Autowired
private EntityManager entityManager;
@Autowired
private StudentRepository studentRepository;
public Klass[] getKlasses(){
return Klass.values();
}
// it will return paged response
public ResponseEntity getPaginatedStudents(StudentSearchCO srchCo) {
try {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery();
Root root = criteriaQuery.from(Student.class);
List<Predicate> predicateList = new ArrayList<>();
if (Strings.isNotBlank(srchCo.getSrchTxt())) {
predicateList.add(criteriaBuilder.like(root.get("name"), "%" + srchCo.getSrchTxt() + "%"));
}
if (Strings.isNotBlank(srchCo.getDateOfAdmission())) {
predicateList.add(criteriaBuilder.equal(root.get("dateOfAdmission"),Utils.convertStrToDate(srchCo.getDateOfAdmission(),"yyyy-MM-dd")));
}
if (srchCo.getKlass() != null) {
predicateList.add(criteriaBuilder.equal(root.get("klass"), srchCo.getKlass()));
}
List<Order> orderList = new ArrayList<>();
//ordered by id, you can write your own sorting with entity field names
orderList.add(criteriaBuilder.asc(root.get("id")));
criteriaQuery.orderBy(orderList);
Predicate[] predicateArr = new Predicate[predicateList.size()];
Predicate predicate = criteriaBuilder.and(predicateList.toArray(predicateArr));
criteriaQuery = criteriaQuery.where(predicate);
TypedQuery<Student> typedQuery = entityManager.createQuery(criteriaQuery.select(root));
List<Student> resultList = typedQuery.getResultList(); // it may contain large data and some that we do not need too
List<StudentDTO> dtos = new ArrayList<>(); // so we will convert in data transfer objects with required fields
resultList.forEach(r -> {
dtos.add(new StudentDTO(r));
});
int pageSize = srchCo.getSize().orElse(10);
int pageNumber = srchCo.getPage().orElse(1) - 1;
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.Direction.ASC, "name");
Page<StudentDTO> resultPage = null;
if (resultList.size() > 0) {
int from = pageNumber * pageSize;
int to = from + pageSize;
if (resultList.size() < to) {
to = resultList.size();
}
resultPage = new PageImpl<>(dtos.subList(from, to), pageable, dtos.size()); // list is sliced according to page number and size
} else resultPage = new PageImpl<>(new ArrayList<>(), pageable, 0);
return ResponseEntity.status(HttpStatus.CREATED).body(resultPage);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
}
}
// this method will be executed just before services start serving the service.
@PostConstruct
public void booststrapSampleData(){
if(studentRepository.count()==0) {
List<Student> studentList = new ArrayList<>();
List<Klass> klasses = Arrays.asList(getKlasses());
List<String> names = Arrays.asList("Aman", "Sujeet", "Vipin kumar", "Lavkush Verma", "swapnil", "Sakshi", "Sanduana Siva");
IntStream.range(0,klasses.size()).forEach(k -> {
LocalDate date = LocalDate.now();
IntStream.range(0,names.size()).forEach(n->{
studentList.add(new Student(names.get(n)+" "+k+n,names.get(n).toLowerCase()+k+n+"@demo.com",klasses.get(k),date.minusDays(n)));
});
});
studentRepository.saveAll(studentList);
}
}
}
9. Create a controller
@Controller
public class StudentController {
@Autowired
private StudentService studentService;
// This method is to simply open the html page
@RequestMapping(value={"/","/students"})
public String students(){
return "students";
}
// This method will be called through ajax
@PostMapping("/students")
public ResponseEntity articles(StudentSearchCO srchCo){
return studentService.getPaginatedStudents(srchCo);
}
}
10. Create a utility class for date conversions
This conversion will be required to convert date format
public class Utils {
public static LocalDate convertStrToDate(String dateStr, String dateFormat) {
LocalDate date = null;
if (dateStr != null) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
date = LocalDate.parse(dateStr, dateTimeFormatter);
}
return date;
}
public static String convertDateToStr(LocalDate date, String dateFormat) {
String dateStr = null;
if (date != null) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
dateStr = date.format(dateTimeFormatter);
}
return dateStr;
}
}
11. Create the HTML Data Table design
students.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>EasyTutorials - Pagination Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<hr>
<h4 class="text-center text-primary">Custom Data Table with Search & filters</h4>
<hr>
<form id="filterForm" method="post" action="/students">
<input type="hidden" id="pageInput" name="page" value="1">
<div class="row">
<div class="col-md-3">
<div class="pageControl">
<span>Show:</span>
<select style="width: 70px;" id="pageSize" name="size" class="form-control">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
<span>entries</span>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<input type="text" class="form-control" id="srchTxt" name="srchTxt" placeholder="search with name">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<select class="form-control" id="klass" name="klass">
<option value="">--select class--</option>
<option th:each="c : ${@studentService.getKlasses()}" th:value="${c.name}"
th:text="${c.std}"></option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="date" class="form-control" id="dateOfAdmission" name="dateOfAdmission" placeholder="date of admission">
</div>
</div>
</div>
</form>
<div class="loader lstloader"></div>
<div class="table-responsive" style="overflow-y: auto">
<table class="table table-hover table-bordered">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Class</th>
<th>Date of Admission</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="row table-bottom">
<div class="col-md-4 ">
<div class="pageControl">
<span> Showing <strong id="offset">0</strong> to <strong id="numberOfElements">0</strong> of <strong id="totalElements">0</strong> entries</span>
</div>
</div>
<div class="col-md-4">
<div class="pageControl ">
<span>Page No.</span>
<input type="text" id="gotoPage" class="goto form-control" onkeyup="changeJumpPage(this.value)"
value="1">
<button type="button" onclick="jumpToPage()" class="btn btn-primary">Jump</button>
</div>
</div>
<div class="col-md-4">
<div class="pageControl text-center ">
<button disabled="" class="btn btn-primary" id="prevBtn">Prev</button>
<div class="curr" id="currentPage">1/1</div>
<button disabled="" class="btn btn-primary" id="nextBtn">Next</button>
</div>
</div>
</div>
</div>
</body>
</html>
12. Add CSS for data table design
You may add this to the style tag in the above Html
.pageControl{display: flex;flex-direction: row}
.curr{padding: 4px 10px;letter-spacing: 4px;}
.goto{width:70px}
.table td{padding:5px}
.form-control{height:30px;padding:0 10px}
.btn{padding:0 5px;height:30px}
.loader {height: 4px;
width: 100%;
position: relative;
overflow: hidden;
background-color: #ddd
}
.loader:before {
display: block;
position: absolute;
content: "";
left: -200px;
width: 200px;
height: 4px;
background-color: #2980b9;
animation: loading 1s linear infinite
}
@keyframes loading {
from {left:-200px;width:30% }
50%{width:30%}
70%{width:70%}
80%{left:50%}
95%{left:120%}
to{left:100%}
}
14. Add JavaScript code to handle AJAX calls
This will handle the data table events.
$(document).ready(function () {
fetchData();
$("#klass,#dateOfAdmission").on("change", function () {
$("#pageInput").val(1);
fetchData();
});
$("#srchTxt").on("input", function () {
$("#pageInput").val(1);
fetchData();
});
});
var dataUrl = "/students";
var prevBtn = $("#prevBtn");
var nextBtn = $("#nextBtn");
var totalElements = $("#totalElements");
var numberOfElements = $("#numberOfElements");
var offset = $("#offset");
var currentPage = $("#currentPage");
var pageSize = $("#pageSize");
var pageNumber = $("#pageInput");
var from = 0;
var to = 0;
var totalPages=0;
var gotoPage = $("#gotoPage");
var loader = {};
loader.show=function () {
$(".lstloader").show();
};
loader.hide=function () {
$(".lstloader").hide();
};
function fetchData() {
loader.show();
var formData = getFormData($("#filterForm"));
console.log(formData);
$.post(dataUrl, formData,function (data,status) {
console.log(data);
setTableParams(data);
var content = data.content;
if (Object.keys(content).length > 0) {
var tbody = $("<tbody/>", {id: "tbody"});
var count = from;
for (var i in content) {
var rec = content[i];
var tr = $("<tr>").append(
$("<td/>", {text:rec.id }),
$("<td/>", {text:rec.name}),
$("<td/>", {text:rec.email}),
$("<td/>", {text:rec.klass}),
$("<td/>", {text:rec.dateOfAdmission})
);
tbody.append(tr);
count++;
}
$("#tbody").replaceWith(tbody);
} else {
$("#tbody").empty();
$("#tbody").append(
$("<tr/>").append($("<td/>",{
colspan:"20",
class:"text-center text-info",
text:"No records found"
}))
);
}
loader.hide();
}).fail(function (t,e) {
ajaxErr(t,e);
loader.hide();
});
}
function setTableParams(record) {
pageSize.val(record.pageable.pageSize);
totalElements.text(record.totalElements);
if(record.totalElements=="0"){
from = Number(record.pageable.offset);
}else
from = Number(record.pageable.offset) + 1;
offset.text(from);
to = (record.pageable.offset) + Number(record.numberOfElements);
numberOfElements.text(to);
pageNumber.val(Number(record.pageable.pageNumber) + 1);
totalPages = Number(record.totalPages);
currentPage.text(pageNumber.val()+"/"+totalPages);
gotoPage.val(pageNumber.val());
if (record.first == true)
prevBtn.attr("disabled","");
else
prevBtn.removeAttr("disabled");
if (record.last == true)
nextBtn.attr("disabled","");
else
nextBtn.removeAttr("disabled");
}
prevBtn.click(function () {
if (!prevBtn.hasClass("disabled")) {
//console.log("Going to previous page.");
pageNumber.val(Number(pageNumber.val()) - 1);
fetchData();
} else {
console.log("Nothing to do");
}
});
nextBtn.click(function () {
if (!nextBtn.hasClass("disabled")) {
//console.log("Going to next page.");
pageNumber.val(Number(pageNumber.val()) + 1);
fetchData();
} else {
console.log("Nothing to do");
}
});
pageSize.on("change", function () {
$("#pageInput").val(1);
fetchData()
});
function jumpToPage() {
var n = Number(gotoPage.val());
//console.log(n);
if(gotoPage.val()!=='' && n>0 && n<=totalPages){
pageNumber.val(n);
fetchData();
}
}
// this will convert form data to json format
function getFormData(t) {
var e = t.serializeArray(), n = {};
return $.map(e, function (t, e) {
n[t.name] = t.value
}), n
}
// to filter out the response error
function ajaxErr(t, e) {
var n = "";
0 === t.status ? n = "Verify Network and refresh the page." : 404 == t.status ? n = "Requested page not found. [404]" : 500 == t.status ? n = "Internal Server Error [500]." : "parsererror" === e ? n = "Requested JSON parse failed." : "timeout" === e ? n = "Time out error." : "abort" === e ? n = "Ajax request aborted." : (console.log("uncaught error"), n = t.responseText),
alert(n);
}
15. Screens
Thanks for reading this article, I hope you understand well the above. If you have any questions please comment below.
And as always you can find the source code on Github