Written by: Phạm Minh Khải
Gmail: mkpham2000@gmail.com
Bài viết gốc: https://www.baeldung.com/spring-thymeleaf-error-messages#thymeleaf

1. Giới thiệu

Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách hiển thị Error Messages bắt nguồn từ ứng dụng back-end dựa trên Spring trong Thymeleaf templates.
Để dễ nắm bắt, chúng ta sẽ tạo một ứng dụng Đăng ký Người dùng Spring Boot đơn giản và xác thực các trường đầu vào riêng lẻ. Ngoài ra, chúng ta sẽ xem một ví dụ về cách xử lý lỗi cấp độ toàn cầu(global-level errors)
Để bắt đầu chúng tôi sẽ thiết lập ứng dụng back-end và sau đó đến phần giao diện người dùng.

2. Ứng dụng Spring Boot mẫu

Để tạo một ứng dụng Spring Boot đơn giản cho Đăng ký người dùng, chúng tôi sẽ cần controller, repository và entity. Tuy nhiên, chúng ta cần thêm các phụ thuộc Maven dependencies.

2.1.Maven Dependency

Thêm các trình khởi động Spring Boot mà chúng ta cần gồm: Web cho MVC bit, Authentication để xác thực thực thể ngủ đông (hibernate entity authentication), Thymeleaf cho front end and JPA cho repository. Thêm vào đó, chúng ta sẽ cần H2 dependency để có cơ sở dữ liệu trong bộ nhớ.

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
    <version>2.4.3</version> 
</dependency> 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-validation</artifactId> 
    <version>2.4.3</version> 
</dependency> 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-thymeleaf</artifactId> 
    <version>2.4.3</version> 
</dependency> 
<dependency> 

2.2. Định nghĩa Entity

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @NotEmpty(message = "User's name cannot be empty.")
    @Size(min = 5, max = 250)
    private String fullName;
    @NotEmpty(message = "User's email cannot be empty.")
    private String email;
    @NotNull(message = "User's age cannot be null.")
    @Min(value = 18)
    private Integer age;
    private String country;
    private String phoneNumber;
    // getters and setters
}

Như đã thấy, chúng ta đã thêm một số ràng buộc xác thực cho đầu vào của người dùng. Chẳng hạn, các trường không được rỗng hoặc trống và có kích thước hoặc giá trị cụ thể.
Đáng chú ý, chúng ta chưa thêm bất kỳ ràng buộc nào vào trường country hoặc phoneNumber. Đó là bởi vì chúng ta sẽ sử dụng chúng làm ví dụ để tạo ra lỗi chung hoặc lỗi không liên quan đến một trường cụ thể.

2.3. The Repository

Chúng ta sẽ sử dụng JPA repository đơn giản cho trường hợp sử dụng cơ bản:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {}

2.4. The Controller

Cuối cùng, tạo một UserController để kết nối mọi thứ lại với nhau ở back-end

@Controller
public class UserController {
    @Autowired
    private UserRepository repository;
    @GetMapping("/add")
    public String showAddUserForm(User user) {
        return "errors/addUser";
    }
    @PostMapping("/add")
    public String addUser(@Valid User user, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "errors/addUser";
        }
        repository.save(user);
        model.addAttribute("users", repository.findAll());
        return "errors/home";
    }
}

Ở đây chúng tôi sẽ xác định một GetMapping tại đường dẫn / add để hiển thị biểu mẫu đăng ký. PostMapping của tại cùng một đường dẫn xử lý validate khi biểu mẫu được gửi cùng với việc lưu tiếp theo vào repository nếu mọi việc được diễn ra suôn sẻ.

3. Thymeleaf Temples có thông báo lỗi(Error Messages)

Bây giờ chúng ta sẽ đi đến mấu chốt của vấn đề, đó là tạo các mẫu giao diện người dùng và hiển thị thông báo lỗi, nếu có.
Hãy xây dựng từng phần các mẫu dựa trên các loại lỗi chúng ta có thể hiển thị.

3.1. Hiển thị lỗi trường (Displaying Field Errors)

Thymeleaf cung cấp một phương thức inbuilt field.hasErrors trả về một boolean tùy thuộc vào việc có bất kỳ lỗi nào tồn tại cho một trường nhất định hay không. Kết hợp nó với một th: if chúng ta có thể chọn hiển thị lỗi nếu nó tồn tại:

<p th:if="${#fields.hasErrors('age')}">Invalid Age</p>

Tiếp theo, nếu chúng ta muốn thêm bất kỳ kiểu nào, chúng ta có thể sử dụng th: class có điều kiện:

<p th:if="${#fields.hasErrors('age')}"
 th:class="${#fields.hasErrors('age')}? error">
 Invalid Age</p>

Lỗi lớp CSS nhúng đơn giản khiến phần tử có màu đỏ:

<style>
    .error {
        color: red;
    }
</style>

Một thuộc tính Thymeleaf khác là th: error cung cấp cho chúng tôi khả năng hiển thị tất cả các lỗi trên bộ chọn được chỉ định, chẳng hạn như email:

<div>
    <label for="email">Email</label> <input type="text" th:field="*{email}" />
    <p th:if="${#fields.hasErrors('email')}" th:errorclass="error" th:errors="*{email}" />
</div>

Trong đoạn mã trên, chúng ta cũng có thể thấy một biến thể trong việc sử dụng kiểu CSS. Ở đây chúng tôi đang sử dụng th: errorclass, giúp loại bỏ việc chúng tôi phải sử dụng bất kỳ thuộc tính điều kiện nào để áp dụng CSS.
Ngoài ra, chúng tôi có thể chọn lặp lại tất cả các thông báo xác thực trên một trường nhất định bằng cách sử dụng th: each:

<div>
    <label for="fullName">Name</label> <input type="text" th:field="*{fullName}" 
      id="fullName" placeholder="Full Name">
    <ul>
        <li th:each="err : ${#fields.errors('fullName')}" th:text="${err}" class="error" />
    </ul>
</div>

Đáng chú ý, chúng tôi đã sử dụng một phương thức Thymeleaf khác là fields.errors () để thu thập tất cả các thông báo xác thực được trả về bởi ứng dụng back-end cho trường fullName.
Để kiểm tra điều này, hãy kích hoạt Boot app và nhấn vào [](http: // localhost: 8080 / add).
Đây là giao diện hiển thị của trang khi không cung cấp bất kỳ thông tin đầu vào nào:
Displaying Field Errors

3.2. Hiển thị tất cả các lỗi cùng một lúc (Displaying All Errors at Once)

Thay vì hiển thị từng thông báo lỗi một, chúng ta có thể hiển thị tất cả các lỗi cùng lúc. Để làm điều này, chúng tôi sẽ sử dụng phương thức fields.hasAnyErrors () của Thymeleaf:

<div th:if="${#fields.hasAnyErrors()}">
   <ul>
       <li th:each="err : ${#fields.allErrors()}" th:text="${err}" />
   </ul>
</div>

Như chúng ta có thể thấy, chúng tôi đã sử dụng một biến thể khác fields.allErrors () ở đây để lặp lại tất cả các lỗi trên tất cả các trường trên biểu mẫu HTML.
Thay vì fields.hasAnyErrors (), chúng ta có thể sử dụng #fields.hasErrors(‘'). Tương tự, #fields.errors(‘') là một thay thế cho #fields.allErrors () đã được sử dụng ở trên.
Đây là hiệu ứng:
Displaying All Errors at Once

3.3. Hiển thị lỗi bên ngoài biểu mẫu (Displaying Errors Outside Forms)

Xem xét một tình huống trong đó chúng tôi muốn hiển thị các thông báo xác thực bên ngoài một biểu mẫu HTML.
Trong trường hợp đó, thay vì sử dụng các vùng chọn hoặc (* {….}), Chúng ta chỉ cần sử dụng tên biến đủ điều kiện ở định dạng ($ {….}):

<h4>Errors on a single field:</h4>
<div th:if="${#fields.hasErrors('${user.email}')}"
 th:errors="*{user.email}"></div>
<ul>
    <li th:each="err : ${#fields.errors('user.*')}" th:text="${err}" />
</ul>

Điều này sẽ hiển thị tất cả các thông báo lỗi trên trường email.
Bây giờ, hãy xem cách chúng ta có thể hiển thị tất cả các thông báo cùng một lúc:

<h4>All errors:</h4>
<ul>
<li th:each="err : ${#fields.errors('user.*')}" th:text="${err}" />
</ul>

Và đây là những gì được hiển thị trên trang web
Displaying Errors Outside Forms

3.4. Hiển thị các lỗi toàn cục (Displaying Global Errors)

Trong một tình huống thực tế, có thể có lỗi không liên quan cụ thể đến một trường cụ thể. Chúng tôi có thể có một trường hợp sử dụng trong đó chúng tôi cần xem xét nhiều đầu vào để xác thực một điều kiện kinh doanh. Đây được gọi là lỗi toàn cục.
Hãy xem xét một ví dụ đơn giản để chứng minh điều này. Đối với trường country và phoneNumber, chúng tôi có thể thêm kiểm tra đối với một quốc gia nhất định, các số điện thoại phải bắt đầu bằng một tiền tố cụ thể.
Chúng tôi sẽ cần thực hiện một số thay đổi ở back-end để thêm xác thực này.
Đầu tiên, chúng tôi sẽ thêm Service để thực hiện xác thực này:

@Service
public class UserValidationService {
    public String validateUser(User user) {
        String message = "";
        if (user.getCountry() != null && user.getPhoneNumber() != null) {
            if (user.getCountry().equalsIgnoreCase("India") 
              && !user.getPhoneNumber().startsWith("91")) {
                message = "Phone number is invalid for " + user.getCountry();
            }
        }
        return message;
    }
}

Có thể thấy, chúng tôi đã thêm một trường hợp nhỏ. Đối với quốc gia Ấn Độ, số điện thoại phải bắt đầu bằng tiền tố 91.
Tiếp theo, chúng tôi sẽ cần một chỉnh sửa đối với PostMapping của controller:

@PostMapping("/add")
public String addUser(@Valid User user, BindingResult result, Model model) {
    String err = validationService.validateUser(user);
    if (!err.isEmpty()) {
        ObjectError error = new ObjectError("globalError", err);
        result.addError(error);
    }
    if (result.hasErrors()) {
        return "errors/addUser";
    }
    repository.save(user);
    model.addAttribute("users", repository.findAll());
    return "errors/home";
}

Cuối cùng, trong mẫu Thymeleaf, chúng tôi sẽ thêm hằng số toàn cục để hiển thị loại lỗi:

<div th:if="${#fields.hasErrors('global')}">
    <h3>Global errors:</h3>
    <p th:each="err : ${#fields.errors('global')}" th:text="${err}" class="error" />
</div>

Ngoài ra, thay vì hằng số, chúng ta có thể sử dụng các phương thức #fields.hasGlobalErrors() và #fields.globalErrors() để đạt được điều tương tự.
Đây là những gì hiển thị khi nhập một đầu vào không hợp lệ:
Displaying Global Errors

4. Kết luận

Trong hướng dẫn này, chúng ta đã xây dựng một ứng dụng Spring Boot đơn giản để trình bày cách hiển thị các loại lỗi khác nhau trong Thymeleaf.
Chúng tôi đã xem xét việc hiển thị từng lỗi trường một và sau đó là tất cả lần lượt, lỗi bên ngoài biểu mẫu HTML và lỗi toàn cục.