创建书籍表单

本小节介绍如何定义一个页面/表单来创建Book对象。这比创建等效的AuthorGenre页面要复杂一些,因为我们需要在Book表单中获取和显示可用的AuthorGenre记录。

导入验证和清理方法

打开/controllers/bookController.js,并在文件顶部(路由函数之前)添加以下行。

js
const { body, validationResult } = require("express-validator");

控制器 - 获取路由

找到导出的book_create_get()控制器方法并将其替换为以下代码。

js
// Display book create form on GET.
exports.book_create_get = asyncHandler(async (req, res, next) => {
  // Get all authors and genres, which we can use for adding to our book.
  const [allAuthors, allGenres] = await Promise.all([
    Author.find().sort({ family_name: 1 }).exec(),
    Genre.find().sort({ name: 1 }).exec(),
  ]);

  res.render("book_form", {
    title: "Create Book",
    authors: allAuthors,
    genres: allGenres,
  });
});

这使用awaitPromise.all()的结果上获取所有AuthorGenre对象,以并行的方式进行(与Express教程第5部分:显示库数据中使用的相同方法)。然后,这些对象作为名为authorsgenres的变量(以及页面title)传递给视图book_form.pug

控制器 - 发布路由

找到导出的book_create_post()控制器方法并将其替换为以下代码。

js
// Handle book create on POST.
exports.book_create_post = [
  // Convert the genre to an array.
  (req, res, next) => {
    if (!Array.isArray(req.body.genre)) {
      req.body.genre =
        typeof req.body.genre === "undefined" ? [] : [req.body.genre];
    }
    next();
  },

  // Validate and sanitize fields.
  body("title", "Title must not be empty.")
    .trim()
    .isLength({ min: 1 })
    .escape(),
  body("author", "Author must not be empty.")
    .trim()
    .isLength({ min: 1 })
    .escape(),
  body("summary", "Summary must not be empty.")
    .trim()
    .isLength({ min: 1 })
    .escape(),
  body("isbn", "ISBN must not be empty").trim().isLength({ min: 1 }).escape(),
  body("genre.*").escape(),
  // Process request after validation and sanitization.

  asyncHandler(async (req, res, next) => {
    // Extract the validation errors from a request.
    const errors = validationResult(req);

    // Create a Book object with escaped and trimmed data.
    const book = new Book({
      title: req.body.title,
      author: req.body.author,
      summary: req.body.summary,
      isbn: req.body.isbn,
      genre: req.body.genre,
    });

    if (!errors.isEmpty()) {
      // There are errors. Render form again with sanitized values/error messages.

      // Get all authors and genres for form.
      const [allAuthors, allGenres] = await Promise.all([
        Author.find().sort({ family_name: 1 }).exec(),
        Genre.find().sort({ name: 1 }).exec(),
      ]);

      // Mark our selected genres as checked.
      for (const genre of allGenres) {
        if (book.genre.includes(genre._id)) {
          genre.checked = "true";
        }
      }
      res.render("book_form", {
        title: "Create Book",
        authors: allAuthors,
        genres: allGenres,
        book: book,
        errors: errors.array(),
      });
    } else {
      // Data from form is valid. Save book.
      await book.save();
      res.redirect(book.url);
    }
  }),
];

此代码的结构和行为与GenreAuthor表单的POST路由函数几乎完全相同。首先,我们验证并清理数据。如果数据无效,我们将重新显示表单,以及用户最初输入的数据和错误消息列表。如果数据有效,我们将保存新的Book记录,并将用户重定向到书籍详细信息页面。

与其他表单处理代码相比,主要区别在于我们如何清理流派信息。表单返回一个Genre项数组(而对于其他字段,它返回一个字符串)。为了验证信息,我们首先将请求转换为数组(这是下一步所需的)。

js
[
  // Convert the genre to an array.
  (req, res, next) => {
    if (!Array.isArray(req.body.genre)) {
      req.body.genre =
        typeof req.body.genre === "undefined" ? [] : [req.body.genre];
    }
    next();
  },
  // …
];

然后,我们在清理器中使用通配符(*)来分别验证每个流派数组条目。以下代码展示了如何实现 - 这相当于“清理键为genre下的所有项目”。

js
[
  // …
  body("genre.*").escape(),
  // …
];

与其他表单处理代码相比,最后一个区别是,我们需要将所有现有的流派和作者传递给表单。为了标记用户已选择的流派,我们遍历所有流派,并为那些在我们的POST数据中(如以下代码片段所示)的流派添加checked="true"参数。

js
// Mark our selected genres as checked.
for (const genre of allGenres) {
  if (book.genre.includes(genre._id)) {
    genre.checked = "true";
  }
}

视图

创建/views/book_form.pug,并将以下文本复制到其中。

pug
extends layout

block content
  h1= title

  form(method='POST')
    div.form-group
      label(for='title') Title:
      input#title.form-control(type='text', placeholder='Name of book' name='title' required value=(undefined===book ? '' : book.title) )
    div.form-group
      label(for='author') Author:
      select#author.form-control(name='author' required)
        option(value='') --Please select an author--
        for author in authors
          if book
            if author._id.toString()===book.author._id.toString()
              option(value=author._id selected) #{author.name}
            else
              option(value=author._id) #{author.name}
          else
            option(value=author._id) #{author.name}
    div.form-group
      label(for='summary') Summary:
      textarea#summary.form-control(placeholder='Summary' name='summary' required)= undefined===book ? '' : book.summary
    div.form-group
      label(for='isbn') ISBN:
      input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required)
    div.form-group
      label Genre:
      div
        for genre in genres
          div(style='display: inline; padding-right:10px;')
            if genre.checked
              input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked)
            else
              input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id)
            label(for=genre._id)  #{genre.name}
    button.btn.btn-primary(type='submit') Submit

  if errors
    ul
      for error in errors
        li!= error.msg

视图结构和行为与genre_form.pug模板几乎相同。

主要区别在于我们如何实现选择类型字段:AuthorGenre

  • 流派集合显示为复选框,并使用我们在控制器中设置的checked值来确定是否应选中该框。
  • 作者集合显示为一个按字母顺序排列的单选下拉列表(传递给模板的列表已排序,因此我们不需要在模板中进行排序)。如果用户之前选择了书籍作者(例如,在初始表单提交后修复无效字段值或更新书籍详细信息时),当显示表单时,将重新选择该作者。在这里,我们通过将当前作者选项的ID与用户之前输入的值(通过book变量传递)进行比较来确定选择哪个作者。

注意:如果提交的表单中存在错误,那么当表单需要重新渲染时,新书籍作者的ID和现有书籍的作者ID将是Schema.Types.ObjectId类型。因此,要比较它们,我们必须先将它们转换为字符串。

它看起来像什么?

运行应用程序,在浏览器中打开https://127.0.0.1:3000/,然后选择创建新书籍链接。如果一切设置正确,您的网站应该看起来像以下屏幕截图。提交有效书籍后,它将被保存,您将被带到书籍详细信息页面。

Screenshot of empty Local library Create Book form on localhost:3000. The page is divided into two columns. The narrow left column has a vertical navigation bar with 10 links separated into two sections by a light-colored horizontal line. The top section link to already created data. The bottom links go to create new data forms. The wide right column has the create book form with a 'Create Book' heading and four input fields labeled 'Title', 'Author', 'Summary', 'ISBN' and 'Genre' followed by four genre checkboxes: fantasy, science fiction, french poetry and action. There is a 'Submit' button at the bottom of the form.

下一步

返回Express教程第6部分:处理表单.

继续第6部分的下一小节:创建BookInstance表单.