Merge pull request #4451 from cdjole/thymeleaf_list_example
BAEL-1661 - Binding a List in Thymeleaf
This commit is contained in:
commit
096826cc07
|
@ -69,6 +69,7 @@
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
|
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
|
||||||
|
<start-class>com.baeldung.sessionattrs.SessionAttrsApplication</start-class>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
public class Book {
|
||||||
|
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthor(String author) {
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((author == null) ? 0 : author.hashCode());
|
||||||
|
result = prime * result + (int) (id ^ (id >>> 32));
|
||||||
|
result = prime * result + ((title == null) ? 0 : title.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
Book other = (Book) obj;
|
||||||
|
if (author == null) {
|
||||||
|
if (other.author != null)
|
||||||
|
return false;
|
||||||
|
} else if (!author.equals(other.author))
|
||||||
|
return false;
|
||||||
|
if (id != other.id)
|
||||||
|
return false;
|
||||||
|
if (title == null) {
|
||||||
|
if (other.title != null)
|
||||||
|
return false;
|
||||||
|
} else if (!title.equals(other.title))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Book [id=" + id + ", title=" + title + ", author=" + author + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface BookService {
|
||||||
|
|
||||||
|
List<Book> findAll();
|
||||||
|
|
||||||
|
void saveAll(List<Book> books);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BooksCreationDto {
|
||||||
|
|
||||||
|
private List<Book> books;
|
||||||
|
|
||||||
|
public BooksCreationDto() {
|
||||||
|
this.books = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooksCreationDto(List<Book> books) {
|
||||||
|
this.books = books;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Book> getBooks() {
|
||||||
|
return books;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBooks(List<Book> books) {
|
||||||
|
this.books = books;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBook(Book book) {
|
||||||
|
this.books.add(book);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.thymeleaf.templatemode.TemplateMode;
|
||||||
|
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
|
||||||
|
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||||
|
|
||||||
|
@EnableWebMvc
|
||||||
|
@Configuration
|
||||||
|
public class Config implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addViewControllers(ViewControllerRegistry registry) {
|
||||||
|
registry.addViewController("/").setViewName("index");
|
||||||
|
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ITemplateResolver templateResolver() {
|
||||||
|
ClassLoaderTemplateResolver resolver
|
||||||
|
= new ClassLoaderTemplateResolver();
|
||||||
|
resolver.setPrefix("templates/books/");
|
||||||
|
resolver.setSuffix(".html");
|
||||||
|
resolver.setTemplateMode(TemplateMode.HTML);
|
||||||
|
resolver.setCharacterEncoding("UTF-8");
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InMemoryBookService implements BookService {
|
||||||
|
|
||||||
|
static Map<Long, Book> booksDB = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Book> findAll() {
|
||||||
|
return new ArrayList(booksDB.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAll(List<Book> books) {
|
||||||
|
long nextId = getNextId();
|
||||||
|
for (Book book : books) {
|
||||||
|
if (book.getId() == 0) {
|
||||||
|
book.setId(nextId++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, Book> bookMap = books.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
Book::getId, Function.identity()));
|
||||||
|
|
||||||
|
booksDB.putAll(bookMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getNextId(){
|
||||||
|
return booksDB.keySet().stream()
|
||||||
|
.mapToLong(value -> value)
|
||||||
|
.max()
|
||||||
|
.orElse(0) + 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||||
|
|
||||||
|
@SpringBootApplication(
|
||||||
|
exclude = {SecurityAutoConfiguration.class,
|
||||||
|
DataSourceAutoConfiguration.class})
|
||||||
|
public class ListBindingApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ListBindingApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.baeldung.listbindingexample;
|
||||||
|
|
||||||
|
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.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/books")
|
||||||
|
public class MultipleBooksController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BookService bookService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/all")
|
||||||
|
public String showAll(Model model) {
|
||||||
|
model.addAttribute("books", bookService.findAll());
|
||||||
|
|
||||||
|
return "allBooks";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/create")
|
||||||
|
public String showCreateForm(Model model) {
|
||||||
|
BooksCreationDto booksForm = new BooksCreationDto();
|
||||||
|
|
||||||
|
for (int i = 1; i <= 3; i++) {
|
||||||
|
booksForm.addBook(new Book());
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("form", booksForm);
|
||||||
|
|
||||||
|
return "createBooksForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/edit")
|
||||||
|
public String showEditForm(Model model) {
|
||||||
|
List<Book> books = new ArrayList<>();
|
||||||
|
bookService.findAll().iterator().forEachRemaining(books::add);
|
||||||
|
|
||||||
|
model.addAttribute("form", new BooksCreationDto(books));
|
||||||
|
|
||||||
|
return "editBooksForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/save")
|
||||||
|
public String saveBooks(@ModelAttribute BooksCreationDto form, Model model) {
|
||||||
|
bookService.saveAll(form.getBooks());
|
||||||
|
|
||||||
|
model.addAttribute("books", bookService.findAll());
|
||||||
|
|
||||||
|
return "redirect:/books/all";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>All Books</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1> Books </h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th> Title </th>
|
||||||
|
<th> Author </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:if="${books.empty}">
|
||||||
|
<td colspan="2"> No Books Available </td>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="book : ${books}">
|
||||||
|
<td><span th:text="${book.title}"> Title </span></td>
|
||||||
|
<td><span th:text="${book.author}"> Author </span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a class="btn btn-success" href="#" th:href="@{/books/create}"> Add Books </a>
|
||||||
|
<a class="btn btn-primary" href="#" th:href="@{/books/edit}" th:classappend="${books.empty} ? 'disabled' : ''"> Edit Books </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>Add Books</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1> Add Books </h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a class="btn btn-info" href="#" th:href="@{/books/all}"> Back to All Books </a>
|
||||||
|
<form action="#" class="form-horizontal"
|
||||||
|
th:action="@{/books/save}"
|
||||||
|
th:object="${form}"
|
||||||
|
method="post">
|
||||||
|
<fieldset>
|
||||||
|
<span class="pull-right">
|
||||||
|
<input type="submit" id="submitButton" class="btn btn-success" th:value="Save">
|
||||||
|
<input type="reset" id="resetButton" class="btn btn-danger" th:value="Reset"/>
|
||||||
|
</span>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th> Title</th>
|
||||||
|
<th> Author</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="book, itemStat : *{books}">
|
||||||
|
<td><input th:placeholder="Title + ' ' + ${itemStat.count}" th:field="*{books[__${itemStat.index}__].title}" required/></td>
|
||||||
|
<td><input th:placeholder="Author + ' ' + ${itemStat.count}" th:field="*{books[__${itemStat.index}__].author}" required/></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,49 @@
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>Edit Books</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1> Add Books </h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a class="btn btn-info" href="#" th:href="@{/books/all}"> Back to All Books </a>
|
||||||
|
<form action="#" class="form-horizontal"
|
||||||
|
th:action="@{/books/save}"
|
||||||
|
th:object="${form}"
|
||||||
|
method="post">
|
||||||
|
<fieldset>
|
||||||
|
<span class="pull-right">
|
||||||
|
<input type="submit" id="submitButton" class="btn btn-success" th:value="Save">
|
||||||
|
<input type="reset" id="resetButton" class="btn btn-danger" th:value="Reset"/>
|
||||||
|
</span>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th> Title</th>
|
||||||
|
<th> Author</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="book, itemStat : ${form.books}">
|
||||||
|
<td><input hidden th:name="|books[${itemStat.index}].id|" th:value="${book.getId()}"/></td>
|
||||||
|
<td><input th:placeholder="Title + ' ' + ${itemStat.count}" th:name="|books[${itemStat.index}].title|" th:value="${book.getTitle()}"
|
||||||
|
required/></td>
|
||||||
|
<td><input th:placeholder="Author + ' ' + ${itemStat.count}" th:name="|books[${itemStat.index}].author|"
|
||||||
|
th:value="${book.getAuthor()}" required/></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Binding a List in Thymeleaf</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p>
|
||||||
|
<h3>Binding a List in Thymeleaf - Example</h3>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<a class="btn btn-info" href="#" th:href="@{/books/all}"> Books List </a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue