Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

HTML-формы

API принимает JSON, а обычная HTML-страница отправляет данные формой: браузер собирает поля <input> и <textarea> и шлёт их запросом POST в формате title=...&body=... (тип application/x-www-form-urlencoded). Добавим в блог страницу «Новый пост»: форму, её обработку, показ ошибок и перенаправление на список постов.

Шаблон формы

Создайте шаблон рядом с posts.mustache:

<!-- src/main/resources/templates/new-post.mustache -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Новый пост</title>
</head>
<body>
<h1>Новый пост</h1>
{{#errors}}
    <p style="color: red">{{.}}</p>
{{/errors}}
<form method="post" action="/blog">
    <p><label>Заголовок <input name="title" value="{{post.title}}"></label></p>
    <p><label>Текст <textarea name="body">{{post.body}}</textarea></label></p>
    <p><label>Автор <input name="author" value="{{post.author}}"></label></p>
    <button type="submit">Опубликовать</button>
</form>
</body>
</html>

Атрибут name у поля — это имя, под которым значение придёт на сервер. {{.}} выводит текущий элемент списка errors, а {{post.title}} подставляет введённое значение, чтобы после ошибки не набирать всё заново. В posts.mustache под заголовком добавьте ссылку <p><a href="/blog/new">Написать пост</a></p>.

Обработчики

Добавьте в PostController два метода: показать форму и принять её.

// src/main/java/com/example/blog/PostController.java
public void newForm(Context ctx) {
    ctx.render("templates/new-post.mustache");
}
 
public void submit(Context ctx) {
    var data = new NewPost(field(ctx, "title"), field(ctx, "body"), field(ctx, "author"));
 
    List<String> errors = new ArrayList<>();
    if (data.title().isBlank()) {
        errors.add("Нужен заголовок");
    }
    if (data.body().length() < 5) {
        errors.add("Текст слишком короткий");
    }
 
    if (!errors.isEmpty()) {
        ctx.status(HttpStatus.BAD_REQUEST);
        ctx.render("templates/new-post.mustache", Map.of("errors", errors, "post", data));
        return;
    }
    store.create(data);
    ctx.redirect("/blog", HttpStatus.SEE_OTHER);
}
 
private static String field(Context ctx, String name) {
    String value = ctx.formParam(name);
    return value == null ? "" : value.strip();
}

ctx.formParam("title") возвращает значение поля или null, если его нет. Если есть ошибки, тот же шаблон рисуется снова с кодом 400, списком ошибок и введёнными данными. Импорты: java.util.ArrayList и java.util.List.

Маршруты добавьте в блок apiBuilder:

// src/main/java/com/example/blog/Main.java
get("/blog/new", posts::newForm);
post("/blog", posts::submit);

Post/Redirect/Get

После успешной отправки сервер не рисует страницу, а отвечает перенаправлением (redirect): кодом 303 See Other и заголовком Location: /blog. Браузер сам открывает /blog запросом GET. Этот приём называют Post/Redirect/Get: если пользователь обновит страницу, браузер повторит безопасный GET, а не отправит форму второй раз и не создаст дубликат.

Проверка

Откройте http://localhost:8080/blog/new и отправьте пустую форму — над ней появятся ошибки. То же через curl: ключ -d отправляет поля как форму, а --data-urlencode кодирует русские буквы.

curl -i http://localhost:8080/blog -d "title=" -d "body=abc" --data-urlencode "author=Олег"
HTTP/1.1 400 Bad Request
Content-Type: text/html

...
<h1>Новый пост</h1>
    <p style="color: red">Нужен заголовок</p>
    <p style="color: red">Текст слишком короткий</p>
<form method="post" action="/blog">
    <p><label>Заголовок <input name="title" value=""></label></p>
    <p><label>Текст <textarea name="body">abc</textarea></label></p>
    <p><label>Автор <input name="author" value="Олег"></label></p>
...

Теперь правильные данные:

curl -i http://localhost:8080/blog --data-urlencode "title=Формы" \
     --data-urlencode "body=Форма отправляет поля." --data-urlencode "author=Анна"
HTTP/1.1 303 See Other
Content-Type: text/plain
Location: /blog

Redirected

Откройте http://localhost:8080/blog — новый пост «Формы» стоит последним в списке.

Частая ошибка

Данные формы — не JSON. Если отправить ту же форму на API-маршрут POST /posts, где стоит bodyValidator, Jackson не сможет прочитать тело:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{"REQUEST_BODY":[{"message":"DESERIALIZATION_FAILED","args":{},"value":"title=%D0%A4%D0%BE%D1%80%D0%BC%D1%8B&body=..."}]}

Для форм читайте поля через ctx.formParam, а bodyAsClass и bodyValidator оставьте для JSON.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.