创建书籍表单
本小节介绍如何定义一个页面/表单来创建Book
对象。这比创建等效的Author
或Genre
页面要复杂一些,因为我们需要在Book
表单中获取和显示可用的Author
和Genre
记录。
导入验证和清理方法
打开/controllers/bookController.js,并在文件顶部(路由函数之前)添加以下行。
const { body, validationResult } = require("express-validator");
控制器 - 获取路由
找到导出的book_create_get()
控制器方法并将其替换为以下代码。
// 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,
});
});
这使用await
在Promise.all()
的结果上获取所有Author
和Genre
对象,以并行的方式进行(与Express教程第5部分:显示库数据中使用的相同方法)。然后,这些对象作为名为authors
和genres
的变量(以及页面title
)传递给视图book_form.pug
。
控制器 - 发布路由
找到导出的book_create_post()
控制器方法并将其替换为以下代码。
// 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);
}
}),
];
此代码的结构和行为与Genre
和Author
表单的POST路由函数几乎完全相同。首先,我们验证并清理数据。如果数据无效,我们将重新显示表单,以及用户最初输入的数据和错误消息列表。如果数据有效,我们将保存新的Book
记录,并将用户重定向到书籍详细信息页面。
与其他表单处理代码相比,主要区别在于我们如何清理流派信息。表单返回一个Genre
项数组(而对于其他字段,它返回一个字符串)。为了验证信息,我们首先将请求转换为数组(这是下一步所需的)。
[
// 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
下的所有项目”。
[
// …
body("genre.*").escape(),
// …
];
与其他表单处理代码相比,最后一个区别是,我们需要将所有现有的流派和作者传递给表单。为了标记用户已选择的流派,我们遍历所有流派,并为那些在我们的POST数据中(如以下代码片段所示)的流派添加checked="true"
参数。
// Mark our selected genres as checked.
for (const genre of allGenres) {
if (book.genre.includes(genre._id)) {
genre.checked = "true";
}
}
视图
创建/views/book_form.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模板几乎相同。
主要区别在于我们如何实现选择类型字段:Author
和Genre
。
- 流派集合显示为复选框,并使用我们在控制器中设置的
checked
值来确定是否应选中该框。 - 作者集合显示为一个按字母顺序排列的单选下拉列表(传递给模板的列表已排序,因此我们不需要在模板中进行排序)。如果用户之前选择了书籍作者(例如,在初始表单提交后修复无效字段值或更新书籍详细信息时),当显示表单时,将重新选择该作者。在这里,我们通过将当前作者选项的ID与用户之前输入的值(通过
book
变量传递)进行比较来确定选择哪个作者。
注意:如果提交的表单中存在错误,那么当表单需要重新渲染时,新书籍作者的ID和现有书籍的作者ID将是Schema.Types.ObjectId
类型。因此,要比较它们,我们必须先将它们转换为字符串。
它看起来像什么?
运行应用程序,在浏览器中打开https://127.0.0.1:3000/
,然后选择创建新书籍链接。如果一切设置正确,您的网站应该看起来像以下屏幕截图。提交有效书籍后,它将被保存,您将被带到书籍详细信息页面。
下一步
继续第6部分的下一小节:创建BookInstance表单.