Spring Boot Dinamik Fieldlarda Validation

Yağız Gazibaba
Kod Gemisi
Published in
5 min readMay 6, 2019

Merhaba, bu yazıda JS ile dinamik oluşturulan ( hede.[index] şeklinde dizi veya List vb. gibi) inputları nasıl validate edebileceğimizi göreceğiz.

Error interface belirli bir nesne için data binding ve doğrulama bilgileri hakkında bilgi tutar ve sunar; aynı zamanda Error interfaceinin rejectValue methodu ile belirli bir field için error kaydetmesini sağlarız[1].

Burada örnek olarak dinamik inputlar için Custom Validation yazacağız ve kullanıcıya hataları döneceğiz.

1- Model’lerimizin Oluşturulması

1–1 Kütüphane Sınıfının Oluşturulması

Bibliotheca.java:

public class Bibliotheca implements Serializable {

private String name;

private LocalDate yearOfFoundation;
//New ArrayList because of Book parameterized constructor
private List<Book> books = new ArrayList<>();
public Bibliotheca(Book book) {
//For first book fields in newBibliotheca page
books.add(book);
}
// getters & setters}

Kütüphane’nin ismi, kurulma tarihi ve kitaplar fieldlarımızı oluşturduk.

Book.java:

public class Book implements Serializable {

@NotNull
private String name;

@NotNull
private String writer;

private LocalDate dateOfIssue;
// getters & setters}

Kitap sınıfımız için isim, yazar, ve basım tarihi fieldlarını oluşturduk.

2- BiblothecaValidator Yazılması

@Component
public class BibliothecaValidator implements Validator {

@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(Bibliotheca.class);
}

@Override
public void validate(Object target, Errors errors) {
Bibliotheca bibliotheca = (Bibliotheca) target;
//Year Of Foundation check
if (bibliotheca.getYearOfFoundation() != null && bibliotheca.getYearOfFoundation().isAfter(LocalDate.now())) {
errors.rejectValue("yearOfFoundation", "error.book.bookCreating.yearOfFoundationError", "Library Foundation year cannot be in future.");
}

//For annotation constraints
javax.validation.Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

List<Book> books = bibliotheca.getBooks();

for (int i = 0; i < books.size(); i++) {
Set<ConstraintViolation<Book>> violations = validator.validate(books.get(i));
final int index = i;
//For constraint annotations.
violations.forEach(error -> {
errors.rejectValue("books[" +index+ "]." + error.getPropertyPath(), "error.book.bookCreatingError", error.getMessage());
});

//For custom validations
if (books.get(i).getDateOfIssue() != null && books.get(i).getDateOfIssue().isAfter(LocalDate.now())) {
errors.rejectValue("books[" +i+ "].dateOfIssue", "error.book.bookCreatingError.dateError", "Date of issue cannot be in future.");
}
}
}
}

Year Of Foundation:

İlk olarak kütüphanenin kurulma yılının gelecek tarihte olmadığını kontrol ediyoruz:

bibliotheca.getYearOfFoundation().isAfter(LocalDate.now())

Constraint Annotations:

for (int i = 0; i < books.size(); i++) {
Set<ConstraintViolation<Book>> violations = validator.validate(books.get(i));
final int index = i;
//For constraint annotations.
violations.forEach(error -> {
errors.rejectValue("books[" +index+ "]." + error.getPropertyPath(), "error.book.bookCreatingError", error.getMessage());
});
//I removed the date of issue part in here for the sake of brevity.
}

Daha sonra javax.validation.Validator sınıfından validator objemizi factory methodumuzla elde ediyoruz. List’teki her bir kitabı validate etmemiz gerekiyor (Not blank constraintleri koyduk). Peki bunu nasıl yapacağız; işte burada, listedeki her bir kitap için validator ile validate işlemini yapıyoruz ve yaptığımız işlem bize Set<ConstraintViolations<T>> seti dönüyor. Daha sonra bu set üstünde iteration yapıp (Şu anki indexteki kitabın violation constraint seti) her bir eleman (violation constraint set’indeki her eleman) için fieldname ve index’i books[“ +index+ “].” + error.getPropertyPath() ile error’a register ediyoruz.

Custom Validation Kısmı:

if (books.get(i).getDateOfIssue() != null && books.get(i).getDateOfIssue().isAfter(LocalDate.now())) {
errors.rejectValue("books[" +i+ "].dateOfIssue", "error.book.bookCreatingError.dateError", "Date of issue cannot be in future.");

Book List’inin iterationınındaki her bir book için DateOfIssue kontrolü yaptık.

3- Bibliotheca Controller Yazılması

Get methodunda modelimize Bibliotheca objesi oluşturup ekleyeceğiz. Post methodunda ise bindingResult’a bakıp buna göre İ yapacağız veya doğrudan view döndüreceğiz.

//Imports are removed for the sake of consistency.
@Controller
@RequestMapping("/")
@AllArgsConstructor
public class BibliothecaController {

private final BibliothecaValidator bibliothecaValidator;

@InitBinder
void setBibliothecaValidator(WebDataBinder binder) {
binder.addValidators(bibliothecaValidator);
}

@GetMapping
String newBibliotheca(Model model) {
model.addAttribute("bibliotheca", new Bibliotheca(new Book()));
return "newBibliotheca";
}

@PostMapping("bibliothecas")
String createBibliotheca(@Valid Bibliotheca bibliotheca, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "newBibliotheca";
}
return "redirect:/";
}
}

4-View’ın Oluşturulması

<div class="col-md-6 mb-3">
<label >Library Name</label>
<input th:field="*{name}" type="text" class="form-control" placeholder="" value="" required>
<div th:errors="*{name}" class="invalid-feedback">
Error message of name field
</div>
</div>
<div class="col-md-3 mb-3">
<label>Date of Foundation</label>
<input th:field="*{yearOfFoundation}" type="date" class="form-control" placeholder="" value="" required>
<div th:errors="*{yearOfFoundation}" class="invalid-feedback">
Error message of yearOfFoundation field
</div>
</div>

Gelelim esas kısma; Bibliotheca sınıfının içindeki Book List’ine, bu kısımda th:each ile kitap sayısı kadar kitap fieldları ekrana bastırmamız gerekiyor. Böylelikle error oluştuğu zaman dinamik olarak oluşturulan fieldlar kadar dönüp gösterebileceğiz. Bu arada unutmadan Modelimizle view’a gönderilen bibliotheca objesi oluşurken, constructor’da List<Book> property’sine yeni bir kitap eklemiştik, bu işlem kütüphane oluşturma sayfası ilk talep edildiğinde bir kitap fieldı oluşabilmesi içindi.

<div class="books">
<div class="book" th:each="book, bookStat : *{books}">
<div class="row">
<div class="col-md-6 mb-3">
<label >Book Name</label>
<input type="text" class="form-control" placeholder="" th:field="*{books[__${bookStat.index}__].name}">
<div th:errors="*{books[__${bookStat.index}__].name}" class="invalid-feedback">
Name is required.
</div>
</div>
<div class="col-md-6 mb-3">
<label >Writer of Book</label>
<input type="text" class="form-control" placeholder="" th:field="*{books[__${bookStat.index}__].writer}">
<div th:errors="*{books[__${bookStat.index}__].writer}" class="invalid-feedback">
Writer is required.
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<label >Date Of Issue</label>
<input type="date" class="form-control" th:field="*{books[__${bookStat.index}__].dateOfIssue}" placeholder="">
<div class="invalid-feedback" th:errors="*{books[__${bookStat.index}__].dateOfIssue}">
Date is from future.
</div>
</div>
</div>
</div>
</div>

Üstte gördüğünüz gibi th:each ile kitabın fieldlarında döndük ve th:field, th:errors attributelarını yazdık.

Şimdi sırada dinamik olarak input oluşturmak var. Bunun için jQuery kullanacağız.

<script>
$(function () {
$('#addBook').click(function () {
bookIndex = $('.book').length;
$(".books").append("<hr>")
$(".books").append(createNewBookForm(bookIndex));
})
});

function createNewBookForm(index) {
let book = $("<div class='book'></div>");
let nameAndWriterRow = $("<div class='row'></div>");
let dateOfIssueRow = $("<div class='row'></div>");
let bookNameCol = $("<div class='col-md-6 mb-3'></div>");
let bookWriterCol = $("<div class='col-md-6 mb-3'></div>");
let bookIssueDateCol = $("<div class='col-md-3 mb-3'></div>");
let bookNameInput = $("<label >Book Name</label> " + "<input type='text' class='form-control' name='books[" + index + "].name'>");
let bookWriterInput = $("<label >Writer of Book</label> " + "<input type='text' class='form-control' placeholder='' name='books[" + index + "].writer'>");
let bookIssueDateInput = $("<label >Date Of Issue</label><input type='date' class='form-control' " + "name='books[" + index + "].dateOfIssue' placeholder=''>");
//Append process
//Row appends
book.append(nameAndWriterRow);
book.append(dateOfIssueRow);
//Col appends
nameAndWriterRow.append(bookNameCol);
nameAndWriterRow.append(bookWriterCol);
dateOfIssueRow.append(bookIssueDateCol);
//Input appends
bookNameCol.append(bookNameInput);
bookWriterCol.append(bookWriterInput);
bookIssueDateCol.append(bookIssueDateInput);
return book;
}
</script>

Document ready function’ında “addBook’’ id’li butonun click event’i tetiklenince book classına sahip elemanların sayısına bakılıyor. Buradan index alınıp bu index ile yeni form fieldları oluşturulması için createNewBookForm(index) fonksiyonu çağırılıyor. Bu fonksiyonun içinde elementler iç içe eklenip döndürülüyor ve document ready function’da books form elemanlarının altına append ediliyor.

Örnekler:

Formu bu şekilde dolduruyoruz, gelen cevaba bakalım:

Gelen istekte inputların altında hataları görüyoruz.

Şimdi dinamik olarak oluşturulmuş bir kitap daha olsun ve öyle dolduralım formu:

Gördüğünüz gibi dinamik oluşturulan inputları validate ettik.

Projenin çalışan halini buradaki git repo’sunda bulabilirsiniz: https://github.com/argmnt/SpingBoot_DynamicInputsValidation

Ersan Ceylan ve Merve Sarpkaya’ya teşekkürler.

Referanslar

[1] https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/validation/Errors.html

[2] https://www.baeldung.com/thymeleaf-list

--

--