建立書籍表單

本小節介紹如何定義一個頁面/表單來建立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://: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表單.