Descarga el proyecto de GitHub.
Partiendo del proyecto anterior, vamos a realizar un autocompletado, una paginación, lo vamos a internacionalizar y por último realizaremos algunos test para las clases nuevas que vayan surgiendo.
Esta es la página principal (obviamente con mi base de datos) del proyecto:
Desglosemos el proyecto en partes.
Para permitir al usuario escoger el idioma en que quiere ver la página debemos cambiar el LocaleResolver de la aplicación y convertirlo en un SessionLocaleResolver. Para ello implementamos la clase MessageConfiguration
package com.wanchopi.config;
import java.util.Locale;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@Configuration
public class MessageConfiguration implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.getDefault());
return localeResolver;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
localeInterceptor.setIgnoreInvalidLocale(true);
localeInterceptor.setParamName("idioma");
return localeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
package com.wanchopi.controller;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.wanchopi.model.Player;
import com.wanchopi.model.Team;
import com.wanchopi.service.PlayerServiceAPI;
import com.wanchopi.service.TeamServiceAPI;
/**
*
* @author Wanchopi
*
*/
@Controller
public class AppController {
@Autowired
private PlayerServiceAPI playerService;
@Autowired
private TeamServiceAPI teamService;
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
}
@RequestMapping(value = "/")
public String home(@RequestParam Map param, Model model) {
int page = param.get("page") != null ? (Integer.valueOf(param.get("page").toString()) -1) : 0;
PageRequest pageRequest = PageRequest.of(page, 5);
Page pagePlayer = playerService.findAll(pageRequest);
int totalPage = pagePlayer.getTotalPages();
if(totalPage > 0) {
List pages = IntStream.rangeClosed(1, totalPage)
.boxed()
.collect(Collectors.toList());
model.addAttribute("pages", pages);
}
model.addAttribute("players", pagePlayer.getContent());
model.addAttribute("current", page + 1);
model.addAttribute("next", page + 2);
model.addAttribute("prev", page);
model.addAttribute("last", totalPage);
return "index";
}
@RequestMapping(value = "/players/{id}")
public String findByTeam(@PathVariable("id") long id, Model model) {
Team team = teamService.get(id);
List players = playerService.findPlayersByTeam(team);
model.addAttribute("players", players);
return "/players-team";
}
@RequestMapping(value = "/search", method = RequestMethod.GET)
@ResponseBody
public List search(HttpServletRequest request) {
return playerService.findNamePlayer(request.getParameter("term"));
}
@RequestMapping(value = "/search")
public String searchPlayerByName(@RequestParam String keyword, Model model) {
List players = playerService.findByName(keyword);
model.addAttribute("players", players);
return "/player";
}
@RequestMapping(value = "/players/search")
public String searchPlayerByNameFromPlayers(@RequestParam String keyword, Model model) {
List players = playerService.findByName(keyword);
model.addAttribute("players", players);
return "/player";
}
@RequestMapping(value = "/edit/search")
public String searchPlayerByNameReRe(@RequestParam String keyword, Model model) {
return "redirect:/?q= " + keyword;
}
@GetMapping(value = "/create")
public String showForm(ModelMap mp) {
mp.put("player", new Player());
mp.put("teams", teamService.getAll());
return "/player-form";
}
@PostMapping(value = "/save")
public String save(@Valid Player player, BindingResult br, ModelMap mp) {
if (br.hasErrors()) {
mp.put("teams", teamService.getAll());
return "/player-form";
} else {
playerService.save(player);
return "redirect:/";
}
}
@GetMapping(value = "/edit/{id}")
public String showEditForm(@PathVariable("id") long id, ModelMap mp) {
Player player = playerService.get(id);
mp.put("player", player);
mp.put("teams", teamService.getAll());
return "/update-form";
}
@PostMapping(value = "/update")
public String update(@Valid Player player, BindingResult br) {
if (br.hasErrors()) {
return "/update-form";
} else {
playerService.save(player);
return "redirect:/";
}
}
@GetMapping(value = "/delete/{id}")
public String delete(@PathVariable("id") long id) {
playerService.delete(id);
return "redirect:/";
}
}
Atención al método home(), pues es el de la paginación.
package com.wanchopi.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.wanchopi.model.Player;
import com.wanchopi.model.Team;
/**
* Player repository
*
* @author Wanchopi
*
*/
@Repository
public interface PlayerRepository extends JpaRepository<Player, Long>{
@Query("SELECT p FROM Player p WHERE p.name like %:keyword%")
public List<Player> searchPlayers(@Param("keyword") String keyword);
@Query("SELECT name FROM Player WHERE name like %:keyword%")
public List<String> search(@Param("keyword") String keyword);
@Query("SELECT p FROM Player p WHERE p.team like :team")
public List<Player> findByTeam(Team team);
}
package com.wanchopi.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.wanchopi.model.Team;
/**
* Team repository
*
* @author Wanchopi
*
*/
@Repository
public interface TeamRepository extends JpaRepository<Team, Long>{
@Query("SELECT name FROM Team where name like %:keyword%")
public List<String> search(@Param("keyword") String keyword);
}
Aparte de los métodos que heredamos de JpaRepository necesitamos implementar nuestros propios métodos para el autocompletado.
Vamos a utilizar el concepto Java Generic Service muy utilizado cuando trabajamos con tecnologías de persistencia.
package com.wanchopi.service;
import java.io.Serializable;
import java.util.List;
/**
* GenericServiceAPI, interface where the basic methods are declared.
*
* @author Wanchopi
*
* @param <T>
* @param <ID>
*/
public interface GenericServiceAPI<T, ID extends Serializable> {
T save(T entity);
void delete(ID id);
T get(ID id);
List<T> getAll();
}
package com.wanchopi.service;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
/**
* GenericServiceImpl, implements the GenericServiceAPI interface
*
* @author Wanchopi
*
* @param <T>
* @param <ID>
*/
@Service
public abstract class GenericServiceImpl<T, ID extends Serializable> implements GenericServiceAPI<T, ID> {
@Override
public T save(T entity) {
return getRepository().save(entity);
}
@Override
public void delete(ID id) {
getRepository().deleteById(id);
}
@Override
public T get(ID id) {
Optional<T> obj = getRepository().findById(id);
if(obj.isPresent()) return obj.get();
return null;
}
@Override
public List<T> getAll() {
List<T> list = new ArrayList<>();
getRepository().findAll().forEach(obj -> list.add(obj));
return list;
}
public abstract JpaRepository<T, ID> getRepository();
}
package com.wanchopi.service;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import com.wanchopi.model.Player;
import com.wanchopi.model.Team;
/**
* PlayerServiceImpl, Player model's own interface
*
* @author Wanchopi
*
*/
public interface PlayerServiceAPI extends GenericServiceAPI<Player, Long>{
public List<Player> findPlayersByTeam(Team team);
public List<String> findNamePlayer(String keyword);
public List<Player> findByName(String keyword);
public Page<Player> findAll(PageRequest pageRequest);
}
package com.wanchopi.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import com.wanchopi.model.Player;
import com.wanchopi.model.Team;
import com.wanchopi.repository.PlayerRepository;
/**
* PlayerServiceImpl, implements the PLayerServiceAPI interface
*
* @author Wanchopi
*
*/
@Service
public class PlayerServiceImpl extends GenericServiceImpl<Player, Long> implements PlayerServiceAPI {
@Autowired
private PlayerRepository playerRepository;
@Override
public JpaRepository<Player, Long> getRepository() {
return playerRepository;
}
@Override
public Page<Player> findAll(PageRequest pageable) {
return playerRepository.findAll(pageable);
}
@Override
public List<String> findNamePlayer(String keyword) {
List<String> players = playerRepository.search(keyword);
return players;
}
@Override
public List<Player> findByName(String keyword) {
List<Player> players = playerRepository.searchPlayers(keyword);
return players;
}
@Override
public List<Player> findPlayersByTeam(Team team) {
List<Player> players = playerRepository.findByTeam(team);
return players;
}
}
package com.wanchopi.service;
import java.util.List;
import com.wanchopi.model.Team;
/**
* TeamServiceAPI, Team model's own interface
*
* @author Wanchopi
*
*/
public interface TeamServiceAPI extends GenericServiceAPI<Team, Long>{
public List<String> findNameTeams(String keyword);
}
package com.wanchopi.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import com.wanchopi.model.Team;
import com.wanchopi.repository.TeamRepository;
/**
* TeamServiceImpl, implements the TeamServiceAPI interface
*
* @author Wanchopi
*
*/
@Service
public class TeamServiceImpl extends GenericServiceImpl<Team, Long> implements TeamServiceAPI {
@Autowired
private TeamRepository teamRepository;
@Override
public List<String> findNameTeams(String keyword) {
List<String> teams = teamRepository.search(keyword);
return teams;
}
@Override
public JpaRepository<Team, Long> getRepository() {
return teamRepository;
}
}
package com.wanchopi.repository;
import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.wanchopi.model.Player;
import com.wanchopi.model.Team;
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class PlayerRepositoryTest {
@Autowired
private PlayerRepository playerRepository;
@Autowired
private TeamRepository teamRespository;
@Test
public void testSearchPlayers() {
String keyword = "gasol";
List<Player> players = playerRepository.searchPlayers(keyword);
// not null
assertNotNull(players);
// two players
assertEquals(2, players.size());
keyword = "lll";
List<Player> players2 = playerRepository.searchPlayers(keyword);
// not null
assertNotNull(players2);
// zero players
assertEquals(0, players2.size());
}
@Test
public void testFindByTeam() {
List<Team> teams = teamRespository.findAll();
Team team = teams.get(0);
List<Player> players = playerRepository.findByTeam(team);
// three players
assertEquals(3, players.size());
}
}
package com.wanchopi.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import com.wanchopi.model.Player;
import com.wanchopi.model.Team;
import com.wanchopi.repository.PlayerRepository;
@SpringBootTest
public class PlayerServiceTest {
@InjectMocks
private PlayerServiceImpl playerService;
@Mock
private PlayerRepository playerRepository;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testFindNamePlayer() {
List<Player> players = new ArrayList<Player>();
Player player1 = new Player(1L, "player1", "player1@gmail.com", new Team());
Player player2 = new Player(2L, "player2", "player2@gmail.com", new Team());
players.add(player1);
players.add(player2);
when(playerRepository.searchPlayers("gasol")).thenReturn(players);
assertEquals(players.size(), playerService.findByName("gasol").size());
}
}
package com.wanchopi.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import com.wanchopi.repositories.PlayerRepository;
import com.wanchopi.repositories.TeamRepository;
@RunWith(SpringRunner.class)
@WebMvcTest(AppController.class)
@AutoConfigureMockMvc
public class AppControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private AppController controller;
@MockBean
private TeamRepository teamRepository;
@MockBean
private PlayerRepository playerRepository;
@Test
public void contexLoads() throws Exception {
assertThat(controller).isNotNull();
}
@Test
public void testHome() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andDo(print())
.andExpect(view().name("index"));
}
}
<header th:fragment="header" xmlns:th="http://www.w3.org/1999/xhtml">
<!-- Static navbar -->
<nav class="navbar navbar-light bg-light">
<a th:href="@{/}"><img alt="logo" th:src="@{/assets/images/mini-logo.png}"></a>
<form class="form-inline my-2 my-lg-0" action="#" method="post">
<input class="form-control mr-sm-2" type="text" name="keyword" placeholder="Team search"
aria-label="Search" id="teamName">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</nav>
<!-- header -->
<div class="container">
<div class="row">
<div class="col-lg-12" align="center">
<img alt="logo" th:src="@{/assets/images/main-logo.png}">
</div>
</div>
</div>
</header>
<div th:fragment="footer" class="footer" xmlns:th="http://www.w3.org/1999/xhtml">
<div class="container">
<p><span th:text="#{text.footer}" class="text-muted">App footer</span></p>
</div>
</div>
<header th:fragment="header" xmlns:th="http://www.w3.org/1999/xhtml">
<!-- header -->
<div class="container">
<div class="row">
<div class="col-lg-12" align="center">
<img alt="logo" th:src="@{/assets/images/main-logo.png}">
</div>
</div>
</div>
</header>
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">SpringBootThymeleaf</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport" />
<link rel="stylesheet" type="text/css" href="webjars/bootstrap/4.1.0/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="/webjars/jquery/3.4.1/jquery.min.css" />
<link rel="stylesheet" type="text/css" href="webjars/jquery-ui/1.12.1/jquery-ui.min.css" />
<link rel="stylesheet" type="text/css" href="webjars/jquery-ui-themes/1.12.1/blitzer/theme.css" />
<link rel="stylesheet" type="text/css" href="webjars/font-awesome/5.11.2/css/all.min.css" />
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/main.css}"/>
</head>
<body>
<!--header.html that's here -->
<header>
<div th:replace="fragments/header :: header"></div>
</header>
<div layout:fragment="content">
<!-- Your Page Content Here -->
</div>
<!--footer.html that's here -->
<footer>
<div th:replace="fragments/footer :: footer"></div>
</footer>
<!-- JS -->
<script type="text/javascript" src="/webjars/bootstrap/4.1.0/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/webjars/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/jquery-ui/1.12.1/jquery-ui.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$('.fa-times').css('color', '#ff0000');
$('.fa-edit').css('color', '#0000ff');
});
</script>
<!-- blinking effect -->
<script>
$(document).ready(function blink() {
$('#a-save').fadeIn(750).delay(250).fadeOut(1000, blink);
});
</script>
<script>
$(document).ready(function() {
$('#name').focus();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">SpringBootThymeleaf</title>
<meta
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
name="viewport" />
<link rel="stylesheet" type="text/css" href="webjars/bootstrap/4.1.0/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/main.css}"/>
</head>
<body>
<!--header.html that's here -->
<header>
<div th:replace="fragments/header-error :: header"></div>
</header>
<div layout:fragment="content">
<!-- Your Page Content Here -->
</div>
<!--footer.html that's here -->
<footer>
<div th:replace="fragments/footer :: footer"></div>
</footer>
</body>
</html>
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/layout">
<head>
<title>index :: n.b.a.</title>
</head>
<body>
<!-- body -->
<div layout:fragment="content">
<div class="container-fluid" align="center">
<div class="col-lg-9" style="margin-top: 50px;">
<div th:if="${!players.isEmpty()}">
<table class="table">
<thead class="thead-dark">
<tr align="center">
<th scope="col">ID</th>
<th scope="col">NAME</th>
<th scope="col">E-MAIL</th>
<th scope="col">TEAM</th>
<th scope="col">EDIT</th>
<th scope="col">DELETE</th>
</tr>
</thead>
<tbody>
<tr align="center" th:each="player: ${players}">
<td th:text="${player.id}"></td>
<td th:text="${player.name}"></td>
<td th:text="${player.email}"></td>
<td th:text="${player.team.name}"></td>
<td><a th:href="@{/edit/{id}(id=${player.id})}"><i class="fa fa-edit"></i></a></td>
<td><a th:href="@{/delete/{id}(id=${player.id})}"><i class="fa fa-times"></i></a></td>
</tr>
</tbody>
</table>
</div>
<div th:unless="${!players.isEmpty()}">
<p>no items to show</p>
</div>
<a class="a-save" id="a-save" th:href="@{/create}">save a new Player</a>
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/layout">
<head>
<!-- fonts googleapis-->
<link href="https://fonts.googleapis.com/css?family=Raleway&display=swap" rel="stylesheet">
<title>form :: springboot</title>
</head>
<body>
<!-- body -->
<div layout:fragment="content">
<div class="container-fluid" align="center"> <!-- main container -->
<div class="col-lg-9 player-form">
<form th:action="@{save}" th:object="${player}" method="post" id="player-form">
<!-- name -->
<div class="form-group">
<label for="name" class="col-lg-4 control-label font-weight-bold">Name</label>
<div class="col-lg-4">
<input type="text" th:field="*{name}" class="form-control" id="name" placeholder="Name" required="required"/>
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="alert text-danger"></span>
</div>
</div>
<!-- email -->
<div class="form-group">
<label for="email" class="col-lg-4 control-label font-weight-bold">Email</label>
<div class="col-lg-4">
<input type="text" th:field="*{email}" class="form-control" id="email" placeholder="Email" required="required"/>
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert text-danger"></span>
</div>
</div>
<!-- team -->
<div class="form-group">
<label for="team" class="col-lg-4 control-label font-weight-bold">Team</label>
<div class="col-lg-4" align="left">
<select th:field="*{team}" class="form-control">
<option value=""> Select team </option>
<option th:each="team : ${teams}"
th:value="${team.id}"
th:text="${team.name}"
th:selected="false"
/>
</select>
<span th:if="${#fields.hasErrors('team')}" th:errors="*{team}" class="alert text-danger"></span>
</div>
</div>
<!-- Button -->
<div class="form-group">
<div class="col-lg-4" align="left">
<input type="submit" class="btn" value="Save">
</div>
</div>
</form>
</div>
</div> <!-- /main container -->
</div> <!-- /content -->
</body>
</html>
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/layout">
<head>
<!-- fonts googleapis-->
<link href="https://fonts.googleapis.com/css?family=Raleway&display=swap" rel="stylesheet">
<title>form :: springboot</title>
</head>
<body>
<!-- body -->
<div layout:fragment="content">
<div class="container-fluid" align="center"> <!-- main container -->
<div class="col-lg-9 player-form">
<form th:action="@{/update}" th:object="${player}" method="post" id="player-form">
<!-- id -->
<div class="form-group">
<label for="id" class="col-md-4 control-label font-weight-bold">ID</label>
<div class="col-md-4">
<input type="text" th:field="*{id}" class="form-control" id="id" placeholder="" readonly="readonly"/>
<span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="alert text-danger"></span>
</div>
</div>
<!-- name -->
<div class="form-group">
<label for="name" class="col-md-4 control-label font-weight-bold">Name</label>
<div class="col-md-4">
<input type="text" th:field="*{name}" class="form-control" id="name" placeholder="" required="required"/>
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="alert text-danger"></span>
</div>
</div>
<!-- email -->
<div class="form-group">
<label for="email" class="col-md-4 control-label font-weight-bold">Email</label>
<div class="col-md-4">
<input type="text" th:field="*{email}" class="form-control" id="email" placeholder="" required="required"/>
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert text-danger"></span>
</div>
</div>
<!-- team -->
<div class="form-group">
<label for="team" class="col-md-4 control-label font-weight-bold">Team</label>
<div class="col-lg-4" align="left">
<select th:field="*{team}" class="form-control">
<option value=""></option>
<option th:each="team : ${teams}"
th:value="${team.id}"
th:text="${team.name}"
th:selected="false"
/>
</select>
<span th:if="${#fields.hasErrors('team')}" th:errors="*{team}" class="alert text-danger"></span>
</div>
</div>
<!-- Button -->
<div class="form-group">
<div class="col-md-4" align="left">
<input type="submit" class="btn" value="Update">
</div>
</div>
</form>
</div>
</div> <!-- /main container -->
</div>
</body>
</html>
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/layout-error">
<head>
<title>error :: n.b.a.</title>
</head>
<body>
<div layout:fragment="content">
<div class="container-fluid" align="center">
<h1>Custom error page</h1>
<div class="messages-error">
<ul>
<li><span>Status : </span><span th:text="${status}">status</span></li>
<li><span>Error : </span><span th:text="${error}">error</span></li>
<li><span>Message : </span><span th:text="${message}">message</span></li>
<li><span>Path : </span><span th:text="${path}">path</span></li>
<!-- <li><span>Trace : </span><span th:text="${trace}">trace</span></li> -->
</ul>
</div>
<a id="a-error" th:href="@{/}"> - Return main page - </a>
</div>
</div>
</body>
</html>
De esta manera tenemos una aplicación totalmente funcional.
La página principal imprime hasta un máximo de cinco jugadores, permite borrar jugadores, editar jugadores, posee un autocompletado totalmente funcional, una paginación y permite al usuario escoger entre dos idiomas.