codecamp

Express Tutorial Part 6: Working with forms

先决条件: 完成所有以前的教学主题,包括 Express教学第5部分:显示图书馆数据
目的: 了解如何写表单从用户获取数据,以及使用此数据更新数据库。

概述

HTML表单是网络上的一个或多个字段/小部件的组 页面,可用于从用户收集信息以提交到服务器。 表单是用于收集用户输入的灵活机制,因为存在可用于输入许多不同类型的数据(文本框,复选框,单选按钮,日期选择器等)的合适的表单输入。表单也是与服务器共享数据的相对安全的方式 ,因为它们允许我们在具有跨站点请求防止的 POST 请求中发送数据。

使用表单可能很复杂! 开发人员需要为表单编写HTML,验证并正确清理服务器上(以及可能还在浏览器中)输入的数据,重新发布带有错误消息的表单,以通知用户任何无效字段,在成功提交数据时处理数据 ,并最终以某种方式响应用户以指示成功。

在本教程中,我们将向您介绍如何在 Express 中执行上述操作。 一路上,我们将扩展 LocalLibrary 网站,允许用户从库中创建,编辑和删除项目。

注意:我们没有考虑如何将特定路由限制为经过身份验证或授权的用户,因此,此时任何用户都可以对数据库进行更改。

HTML表单

首先简要介绍 HTML表单 考虑一个简单的HTML表单,其中有一个文本字段用于输入一些"团队"的名称及其相关标签:

; width:399px;">

形式在HTML中定义为< form> ...< / form> 标签内的元素集合,其包含至少一个输入 type ="submit"。

<form action="/team_name_url/" method="post">
    <label for="team_name">Enter name: </label>
    <input id="team_name" type="text" name="name_field" value="Default name for team.">
    <input type="submit" value="OK">
</form>

虽然这里我们只包括一个用于输入团队名称的文本字段,但是表单可以包含任何数量的其他输入元素及其关联的标签。 字段的 type 属性定义将显示哪种窗口小部件。 字段的 name id 用于标识JavaScript / CSS / HTML中的字段,而 value 定义字段的初始值 当它第一次显示。 匹配团队标签使用标签标签(参见上面的"输入名称")指定,并使用 包含 id 值的字段的"-style:normal; font-weight:normal;"> code style ="font-style:normal; font-weight:normal;"> input 。

submit 输入将显示为一个按钮(默认情况下) - 用户可以按下它来将其他输入元素包含的数据上传到服务器(在这种情况下,只是 > team_name )。 表单属性定义用于在服务器上发送数据和数据目的地的HTTP 方法( action ):

  • action: The resource/URL where data is to be sent for processing when the form is submitted. If this is not set (or set to an empty string), then the form will be submitted back to the current page URL.
  • method: The HTTP method used to send the data: POST or GET.
    • The POST method should always be used if the data is going to result in a change to the server's database, because this can be made more resistant to cross-site forgery request attacks.
    • The GET method should only be used for forms that don't change user data (e.g. a search form). It is recommended for when you want to be able to bookmark or share the URL.

表单处理过程

表单处理使用了我们学习的用于显示关于我们模型的信息的所有相同的技术:路由将我们的请求发送到控制器功能,该功能执行任何所需的数据库动作,包括从模型读取数据,然后生成并返回HTML页面。 使事情更复杂的是,服务器还需要能够处理由用户提供的数据,并且如果有任何问题,重新显示具有错误信息的表单。

处理表单请求的流程流程如下所示,从包含表单(如绿色所示)的页面请求开始:20form%20handling.png"alt =""style ="height:649px; width:800px;">

如上图所示,处理代码需要做的主要事情是:

  1. Display the default form the first time it is requested by the user.
    • The form may contain blank fields (e.g. if you're creating a new record), or it may be pre-populated with initial values (e.g. if you are changing a record, or have useful default initial values).
  2. Receive data submitted by the user, usually in an HTTP POST request.
  3. Validate and sanitise the data.
  4. If any data is invalid, re-display the form — this time with any user populated values and error messages for the problem fields.
  5. If all data is valid, perform required actions (e.g. save the data in the database, send a notification email, return the result of a search, upload a file, etc.)
  6. Once all actions are complete, redirect the user to another page.

通常使用用于初始显示表单的 GET 路由和用于处理表单数据的验证和处理的相同路径的 POST 路由来实现表单处理代码。 这是本教程中将使用的方法!

Express本身不为表单处理操作提供任何特定的支持,但它可以使用中间件从表单处理 POST GET 参数,并验证/清除其值 。

验证和卫生

在存储表单的数据之前,必须对其进行验证和清理:

  • Validation checks that entered values are appropriate for each field (e.g. are in the right range, format, etc.) and that values have been supplied for all required fields.
  • Sanitisation removes/replaces characters in the data that might potentially be used to send malicious content to the server.

在本教程中,我们将使用受欢迎的 express-validator 模块来执行验证和 我们的形式数据的卫生。

Installation

通过在项目的根目录中运行以下命令来安装模块。

npm install express-validator --save

Add the validator to the app middleware

打开 ./ app.js ,然后在其他模块之后导入 express-validator 模块(靠近文件顶部,如图所示)。

...
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var expressValidator = require('express-validator');

进一步向下调用 app.use()以将验证器添加到中间件堆栈。 这应该在将 bodyParser 添加到中间件堆栈( express-validator 使用 body-parser 来访问参数)的代码之后完成。

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(expressValidator() ); // Add this after the bodyParser middlewares!

Using express-validator

在本教程中,我们将主要使用以下API:

  • checkBody(parameter, message): Specifies a body (POST) parameter to validate along with a message to be displayed if it fails the tests. The validation criteria are daisy chained to the checkBody() method. For example, the first check below will test that the "name" parameter is alphanumeric and set an error message "Invalid name" if it is not. The second test checks that the age parameter has an integer value.
    req.checkBody('name', 'Invalid name').isAlpha();
    req.checkBody('age', 'Invalid age').notEmpty().isInt();
    
  • sanitizeBody(parameter): Specifies a body parameter to sanitize. The sanitization operations are then daisy-chained to this method. For example, the escape() sanitization operation below removes HTML characters from the name variable that might be used in JavaScript cross-site scripting attacks.
    req.sanitizeBody('name').escape();

要运行验证,请调用 req.validationErrors()。 这返回一个错误对象数组(如果没有错误,则返回false),通常使用这样的数组

var errors = req.validationErrors();
if (errors) {
    // Render the form using error information
}
else {
   // There are no errors so perform action with valid data (e.g. save record).
}

数组中的每个错误对象都有参数,消息和值的值。

{param: 'name', msg: 'Invalid name', value: '<received input>'}

注意: 该API还具有检查和清理查询和网址参数的方法(不仅仅是显示的正文参数)。 有关详情,请参阅: express-validator (npm)。

我们将在下面实施 LocalLibrary 表单时介绍更多示例。

窗体设计

库中的许多模型是相关的/依赖的 - 例如, Book 需要作者 >也有一个或多个 Genres 。 这提出了如何处理用户希望的情况的问题:

  • Create an object when its related objects do not yet exist (for example, a book where the author object hasn't been defined).
  • Delete an object that is still being used by another object (so for example, deleting a Genre that is still being used by a Book).

对于这个项目,我们将简化实现,声明一个表单只能:

  • Create an object using objects that already exist (so users will have to create any required Author and Genre instances before attempting to create any Book objects).
  • Delete an object if it is not referenced by other objects (so for example, you won't be able to delete a Book until all associated BookInstance objects have been deleted).

注意:更强大的实施可能允许您在创建新对象时创建依赖对象,并随时删除任何对象(例如,删除依赖对象,或删除对 从数据库删除的对象)。

路线

为了实现我们的表单处理代码,我们将需要两个具有相同URL模式的路由。 第一个( GET )路由用于显示用于创建对象的新空表单。 第二个路由( POST )用于验证用户输入的数据,然后保存信息并重定向到详细信息页面(如果数据有效)或重新显示错误的表单(如果 数据无效)。

我们已在 /routes/catalog.js (在 学习/服务器端/ Express_Nodejs / routes">上一个教程)。 例如,类型路由如下所示:

/* GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id) */
router.get('/genre/create', genre_controller.genre_create_get);

/* POST request for creating Genre. */
router.post('/genre/create', genre_controller.genre_create_post);

创建流派表单

这部分显示我们如何定义页面来创建 Genre 对象(这是一个好的开始,因为 Genre 只有一个字段,其 >,没有依赖)。 像任何其他页面一样,我们需要设置路由,控制器和视图。

控制器 - 获取路由

打开 /controllers/genreController.js 。 找到导出的 genre_create_get()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。 这只是呈现 genre_form.pug 视图,传递标题变量。

// Display Genre create form on GET
exports.genre_create_get = function(req, res, next) {       
    res.render('genre_form', { title: 'Create Genre'});
};

控制器 - 路由

找到导出的 genre_create_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Handle Genre create on POST 
exports.genre_create_post = function(req, res, next) {
    
    //Check that the name field is not empty
    req.checkBody('name', 'Genre name required').notEmpty(); 
    
    //Trim and escape the name field. 
    req.sanitize('name').escape();
    req.sanitize('name').trim();
    
    //Run the validators
    var errors = req.validationErrors();

    //Create a genre object with escaped and trimmed data.
    var genre = new Genre(
      { name: req.body.name }
    );
    
    if (errors) {
        //If there are errors render the form again, passing the previously entered values and errors
        res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors});
    return;
    } 
    else {
        // Data from form is valid.
        //Check if Genre with same name already exists
        Genre.findOne({ 'name': req.body.name })
            .exec( function(err, found_genre) {
                 console.log('found_genre: ' + found_genre);
                 if (err) { return next(err); }
                 
                 if (found_genre) { 
                     //Genre exists, redirect to its detail page
                     res.redirect(found_genre.url);
                 }
                 else {
                     
                     genre.save(function (err) {
                       if (err) { return next(err); }
                       //Genre saved. Redirect to genre detail page
                       res.redirect(genre.url);
                     });
                     
                 }
                 
             });
    }

};

该代码的第一部分定义了一个验证器来检查名称字段不为空(如果为空,则显示错误消息),清除和修剪(删除字符串两端的空格)值,然后运行验证器 。

//Check that the name field is not empty
req.checkBody('name', 'Genre name required').notEmpty(); 
    
//Trim and escape the name field. 
req.sanitize('name').escape();
req.sanitize('name').trim();
    
//Run the validators
var errors = req.validationErrors();

如果存在错误,我们再次使用表单呈现模板,此时还传递一个带有用户传递的任何(已清除)值的变量以及包含错误信息的对象。

//Create a genre object with escaped and trimmed data.
var genre = new Genre({ name: req.body.name });

if (errors) {
    res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors});
    return;
    } 

如果数据有效,那么我们检查是否存在具有相同名称的 Genre (我们不想创建重复的)。 如果它,我们重定向到现有的流派的详细信息页面。 如果没有,我们保存新的流派,并重定向到其详细页面。

//Check if Genre with same name already exists
Genre.findOne({ 'name': req.body.name })
    .exec( function(err, found_genre) {
    console.log('found_genre: '+found_genre)
    if (err) { return next(err); }
                 
        if (found_genre) { 
            //Genre exists, redirect to its detail page
            res.redirect(found_genre.url);
            }
        else {
            genre.save(function (err) {
                if (err) { return next(err); }
                    //Genre saved. Redirect to genre detail page
                    res.redirect(genre.url);
                });
        }
                 
});

视图

GET POST 控制器(路由)中都呈现相同的视图。 在 GET 的情况下,表单是空的,我们只是传递一个标题变量。 在 POST 情况下,用户以前输入了无效数据 - 在 genre 变量中,我们传回输入的数据(已清理)和 errors 变量传回错误消息。

res.render('genre_form', { title: 'Create Genre'});
res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors});

创建 /views/genre_form.pug 并在下面的文本中复制。

extends layout

block content
  h1 #{title}

  form(method='POST' action='')
    div.form-group
      label(for='name') Genre:
      input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name) )
    button.btn.btn-primary(type='submit') Submit

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

这个模板的大部分将从我们以前的教程中熟悉。 首先,我们扩展 layout.pug 基本模板,并覆盖名为"内容"的。 然后我们有一个标题与从控制器(通过 render()方法)传递的 title

接下来,我们使用HTML表单的pug代码,使用 POST 方法将数据发送到服务器,因为 action 空字符串,将数据发送到与页面相同的URL。

表单定义了一个名为"name"的类型为"text"的单个必填字段。 字段的默认取决于是否定义了 genre 变量(参见上面粗体突出显示的文本)。 如果从 GET 路由调用它将是空的,因为这是一个新的形式。 如果从 POST 路由调用,它将包含用户最初输入的(无效)值。

页面的最后一部分是错误代码。 如果已经定义了错误变量(换句话说,当模板在 GET 路由上呈现时,此部分不会出现),这只是打印错误列表。

注意:这只是一种呈现错误的方法。 您还可以从错误变量中获取受影响字段的名称,并使用这些字段来控制错误消息的呈现位置,是否应用自定义CSS等。

它是什么样子的?

运行应用程序,打开浏览器 http:// localhost:3000 / ,然后选择创建新流派 链接。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。 输入值后,应保存该值,您将进入类型详细信息页面。

; width:800px;">

我们对服务器端验证的唯一错误是类型字段不能为空。 下面的屏幕截图显示了如果您没有提供类型(以红色突出显示),错误列表会是什么样子。

margin:0px auto; width:400px;">

请注意:更好的实施将验证该字段在客户端不为空。 将值 required =\'true\'添加到字段定义中:

input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name), required='true' )

创建作者表单

本节说明如何定义一个用于创建 Author 对象的页面。

控制器 - 获取路由

打开 /controllers/authorController.js 。 找到导出的 author_create_get()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。 这只是呈现 author_form.pug 视图,传递 title 变量。

// Display Author create form on GET
exports.author_create_get = function(req, res, next) {       
    res.render('author_form', { title: 'Create Author'});
};

控制器 - 路由

找到导出的 author_create_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Handle Author create on POST 
exports.author_create_post = function(req, res, next) {
   
    req.checkBody('first_name', 'First name must be specified.').notEmpty(); //We won't force Alphanumeric, because people might have spaces.
    req.checkBody('family_name', 'Family name must be specified.').notEmpty();
    req.checkBody('family_name', 'Family name must be alphanumeric text.').isAlpha();
    req.checkBody('date_of_birth', 'Invalid date').optional({ checkFalsy: true }).isDate();
    req.checkBody('date_of_death', 'Invalid date').optional({ checkFalsy: true }).isDate();
    
    req.sanitize('first_name').escape();
    req.sanitize('family_name').escape();
    req.sanitize('first_name').trim();     
    req.sanitize('family_name').trim();
    req.sanitize('date_of_birth').toDate();
    req.sanitize('date_of_death').toDate();

    var errors = req.validationErrors();
    
    var author = new Author(
      { first_name: req.body.first_name, 
        family_name: req.body.family_name, 
        date_of_birth: req.body.date_of_birth,
        date_of_death: req.body.date_of_death
       });
       
    if (errors) {
        res.render('author_form', { title: 'Create Author', author: author, errors: errors});
    return;
    } 
    else {
    // Data from form is valid
    
        author.save(function (err) {
            if (err) { return next(err); }
               //successful - redirect to new author record.
               res.redirect(author.url);
            });
    }

};

找到导出的 author_create_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。...

注意:与Genre帖子处理程序不同,我们不会在保存之前检查 Author 对象是否已存在。 可以说,我们应该,虽然现在我们可以有多个作者有相同的名字。

验证代码演示了两个新功能:

  • We can use the optional() function to run a subsequent validation only if a field has been entered (this allows us to validate optional fields). For example, below we check that the optional date of birth is a date (the checkFalsy flag means that we'll accept either an empty string or null as an empty value).
    req.checkBody('date_of_birth', 'Invalid date').optional({ checkFalsy: true }).isDate();
    
  • Parameters are recieved from the request as strings. We can use toDate() (or toBoolean(), etc.) to cast these to the proper JavaScript types.
    req.sanitize('date_of_birth').toDate()
    

视图

创建 /views/author_form.pug 并在下面的文字中复制。

extends layout

block content
  h1=title

  form(method='POST' action='')
    div.form-group
      label(for='first_name') First Name:
      input#first_name.form-control(type='text', placeholder='First name (Christian) last' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) )
      label(for='family_name') Family Name:
      input#family_name.form-control(type='text', placeholder='Family name (surname)' name='family_name' required='true' value=(undefined===author ? '' : author.family_name))
    div.form-group
      label(for='date_of_birth') Date of birth:
      input#date_of_birth.form-control(type='date', name='date_of_birth', value=(undefined===author ? '' : author.date_of_birth) )
    button.btn.btn-primary(type='submit') Submit

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

此视图的结构和行为与 genre_form.pug 模板完全相同,因此我们不再对其进行说明。

注意:某些浏览器不支持输入 type ="date",因此您将无法使用datepicker小部件或默认的 mm / yyyy 占位符,而是得到一个空的纯文本字段。一个解决方法是明确添加属性 placeholder =\'dd / mm / yyyy\' 不够功能的浏览器,你仍然会得到所需的文本格式的信息。

挑战:上述范本缺少输入 date_of_death 的栏位。 创建字段遵循与出生表格组的日期相同的模式!

它是什么样子的?

运行应用程序,打开浏览器 http:// localhost:3000 / ,然后选择创建新作者 链接。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。 输入值后,应保存该值,您将转到作者详细信息页面。

创建书形式

本节说明如何定义一个页面/表单来创建 Book 对象。 这比等效的 Author Genre 页面复杂一些,因为我们需要获取并显示可用的 Author 代码>记录存储在我们的 Book 表单中。

控制器 - 获取路由

打开 /controllers/bookController.js 。 找到导出的 book_create_get()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Display book create form on GET
exports.book_create_get = function(req, res, next) { 
      
    //Get all authors and genres, which we can use for adding to our book.
    async.parallel({
        authors: function(callback) {
            Author.find(callback);
        },
        genres: function(callback) {
            Genre.find(callback);
        },
    }, function(err, results) {
        if (err) { return next(err); }
        res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres });
    });
    
};

这使用异步模块(如 Express教程第5部分:显示库数据中所述)以获取所有作者类型 对象。 然后将它们作为名为 authors genres 的变量传递到视图 book_form.pug code> title )。

控制器 - 路由

找到导出的 book_create_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Handle book create on POST 
exports.book_create_post = function(req, res, next) {

    req.checkBody('title', 'Title must not be empty.').notEmpty();
    req.checkBody('author', 'Author must not be empty').notEmpty();
    req.checkBody('summary', 'Summary must not be empty').notEmpty();
    req.checkBody('isbn', 'ISBN must not be empty').notEmpty();
    
    req.sanitize('title').escape();
    req.sanitize('author').escape();
    req.sanitize('summary').escape();
    req.sanitize('isbn').escape();
    req.sanitize('title').trim();     
    req.sanitize('author').trim();
    req.sanitize('summary').trim();
    req.sanitize('isbn').trim();
    req.sanitize('genre').escape();
    
    var book = new Book(
      { title: req.body.title, 
        author: req.body.author, 
        summary: req.body.summary,
        isbn: req.body.isbn,
        genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre.split(",")
       });
       
    console.log('BOOK: '+book);
    
    var errors = req.validationErrors();
    if (errors) {
        // Some problems so we need to re-render our book

        //Get all authors and genres for form
        async.parallel({
            authors: function(callback) {
                Author.find(callback);
            },
            genres: function(callback) {
                Genre.find(callback);
            },
        }, function(err, results) {
            if (err) { return next(err); }
            
            // Mark our selected genres as checked
            for (i = 0; i < results.genres.length; i++) {
                if (book.genre.indexOf(results.genres[i]._id) > -1) {
                    //Current genre is selected. Set "checked" flag.
                    results.genres[i].checked='true';
                }
            }

            res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres, book: book, errors: errors });
        });

    } 
    else {
    // Data from form is valid.
    // We could check if book exists already, but lets just save.
    
        book.save(function (err) {
            if (err) { return next(err); }
               //successful - redirect to new book record.
               res.redirect(book.url);
            });
    }

};

这段代码的结构和行为几乎完全一样创建的类型/ code>对象。 首先,我们验证和清理数据。如果数据无效,那么我们与最初由用户和错误消息的列表中输入的数据一起重新显示的形式。 如果数据有效,我们保存新的 Book 记录,并将用户重定向到图书详细信息页面。

同样,与其他表单处理代码的主要区别是,我们需要将所有现有的流派和作者传递给表单。 为了标记用户检查的类型,我们遍历所有类型,并将 checked =\'true\'参数添加到我们的帖子数据中(如下面的代码片段 )。

// Mark our selected genres as checked
for (i = 0; i < results.genres.length; i++) {
    if (book.genre.indexOf(results.genres[i]._id) > -1) {
        //Current genre is selected. Set "checked" flag.
        results.genres[i].checked='true';
    }
}

视图

创建 /views/book_form.pug 并在下面的文字中复制。

extends layout

block content
  h1= title

  form(method='POST' action='')
    div.form-group
      label(for='title') Title:
      input#title.form-control(type='text', placeholder='Name of book' name='title' required='true' value=(undefined===book ? '' : book.title) )
    div.form-group
      label(for='author') Author:
      select#author.form-control(type='select', placeholder='Select author' name='author' required='true' )
        for author in authors
          if book
            option(value=author._id selected=(author._id.toString()==book.author._id.toString() ? 'selected' : false) ) #{author.name}
          else
            option(value=author._id) #{author.name}
    div.form-group
      label(for='summary') Summary:
      input#summary.form-control(type='textarea', placeholder='Summary' name='summary' value=(undefined===book ? '' : book.summary) required='true')
    div.form-group
      label(for='isbn') ISBN:
      input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required='true') 
    div.form-group
      label Genre:
      div
        for genre in genres
          div(style='display: inline; padding-right:10px;')
            input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked=genre.checked )
            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 模板几乎相同。

主要区别在于我们如何实现选择类型字段:作者和类型。

  • The set of genres are displayed as checkboxes, using the checked value we set in the controller to determine whether or not the box should be selected.
  • The set of authors are displayed as a single-selection drop-down list. In this case we determine what author to display by comparing the id of the current author option with the value previously entered by the user (passed in as the book variable). This is highlighted above!

    请注意:必须使用字符串值进行比较,如上所示。 直接比较id值不起作用!

它是什么样子的?

运行应用程序,打开浏览器 http:// localhost:3000 / ,然后选择创建新书 链接。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。 提交有效的书籍后,系统会储存书籍,系统会将您导向书籍详细资料页面。

创建BookInstance表单

本节说明如何定义一个页面/表单来创建 BookInstance 对象。 这非常像我们用来创建 Book 对象的形式。

控制器 - 获取路由

打开 /controllers/bookinstanceController.js

在文件顶部,需要 Book 模块(因为每个 BookInstance 与特定的 Book 相关联)。

var Book = require('../models/book');

找到导出的 bookinstance_create_get()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Display BookInstance create form on GET
exports.bookinstance_create_get = function(req, res, next) {       

    Book.find({},'title')
    .exec(function (err, books) {
      if (err) { return next(err); }
      //Successful, so render
      res.render('bookinstance_form', {title: 'Create BookInstance', book_list:books } );
    });
    
};

控制器会获取所有图书的列表( book_list ),并将其传递到视图 bookinstance_form.pug / code>)

控制器 - 路由

找到导出的 bookinstance_create_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Handle BookInstance create on POST 
exports.bookinstance_create_post = function(req, res, next) {

    req.checkBody('book', 'Book must be specified').notEmpty(); //We won't force Alphanumeric, because people might have spaces.
    req.checkBody('imprint', 'Imprint must be specified').notEmpty();
    req.checkBody('due_back', 'Invalid date').optional({ checkFalsy: true }).isDate();
    
    req.sanitize('book').escape();
    req.sanitize('imprint').escape();
    req.sanitize('status').escape();
    req.sanitize('book').trim();
    req.sanitize('imprint').trim();   
    req.sanitize('status').trim();
    req.sanitize('due_back').toDate();
    
    var bookinstance = new BookInstance(
      { book: req.body.book,
        imprint: req.body.imprint, 
        status: req.body.status,
        due_back: req.body.due_back
       });

    var errors = req.validationErrors();
    if (errors) {
        
        Book.find({},'title')
        .exec(function (err, books) {
          if (err) { return next(err); }
          //Successful, so render
          res.render('bookinstance_form', { title: 'Create BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors, bookinstance:bookinstance });
        });
        return;
    } 
    else {
    // Data from form is valid
    
        bookinstance.save(function (err) {
            if (err) { return next(err); }
               //successful - redirect to new author record.
               res.redirect(bookinstance.url);
            }); 
    }

};

此代码的结构和行为与创建其他对象的结构和行为相同。 首先,我们验证和清理数据。 如果数据无效,我们将重新显示表单以及用户最初输入的数据和错误消息列表。 如果数据有效,我们保存新的 BookInstance 记录,并将用户重定向到详细信息页面。

视图

创建 /views/bookinstance_form.pug 并在下面的文字中复制。

extends layout

block content
  h1=title

  form(method='POST' action='')
    div.form-group
      label(for='book') Book:
      select#book.form-control(type='select', placeholder='Select book' name='book' required='true' )
        for book in book_list
          option(value=book._id, selected=(selected_book==book._id ? 'selected' : false) ) #{book.title}
        
    div.form-group
      label(for='imprint') Imprint:
      input#imprint.form-control(type='text', placeholder='Publisher and date information' name='imprint' required='true' value=(undefined===bookinstance ? '' : bookinstance.imprint) )
    div.form-group
      label(for='due_back') Date when book available:
      input#due_back.form-control(type='date', name='due_back' value=(undefined===bookinstance ? '' : bookinstance.due_back))
            
    div.form-group
      label(for='status') Status:
      select#status.form-control(type='select', placeholder='Select status' name='status' required='true' )
        option(value='Maintenance') Maintenance
        option(value='Available') Available
        option(value='Loaned') Loaned
        option(value='Reserved') Reserved

    button.btn.btn-primary(type='submit') Submit

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

视图结构和行为与 book_form.pug 模板几乎相同,因此我们不再重复。

注意:上述模板会对状态值(维护,可用等)进行硬编码,但不会"记住"用户输入的值。 如果您愿意,请考虑重新实现列表,从控制器传递选项数据,并在重新显示表单时设置所选值。

它是什么样子的?

运行应用程序并打开浏览器以 http:// localhost:3000 / 。 然后选择创建新图书实例(复制)链接。 如果一切设置正确,您的网站应该看起来像下面的屏幕截图。 提交有效的 BookInstance 后,应保存,您将进入详细信息页面。

删除作者表单

此部分显示如何定义要删除作者对象的页面。

表单设计部分中所述,我们的策略将是只允许删除未被其他对象引用的对象(在这种情况下,我们不允许 作者如果被 Book 引用就被删除)。 在实现方面,这意味着表单需要确认在删除作者之前没有关联的图书。 如果有相关联的图书,它应该显示它们,并声明它们必须在删除 Author 对象之前删除。

控制器 - 获取路由

打开 /controllers/authorController.js 。 找到导出的 author_delete_get()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Display Author delete form on GET
exports.author_delete_get = function(req, res, next) {       

    async.parallel({
        author: function(callback) {     
            Author.findById(req.params.id).exec(callback);
        },
        authors_books: function(callback) {
          Book.find({ 'author': req.params.id }).exec(callback);
        },
    }, function(err, results) {
        if (err) { return next(err); }
        //Successful, so render
        res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books } );
    });
    
};

控制器获取要从URL参数( req.params.id )中删除的 Author 实例的ID。 它使用 async.parallel()方法来并行获取作者记录和所有相关联的图书。 当两个操作完成后,它会呈现 author_delete .pug 视图,传递 title 作者和 author_books

控制器 - 路由

找到导出的 author_delete_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Handle Author delete on POST 
exports.author_delete_post = function(req, res, next) {

    req.checkBody('authorid', 'Author id must exist').notEmpty();  
    
    async.parallel({
        author: function(callback) {     
            Author.findById(req.body.authorid).exec(callback);
        },
        authors_books: function(callback) {
          Book.find({ 'author': req.body.authorid },'title summary').exec(callback);
        },
    }, function(err, results) {
        if (err) { return next(err); }
        //Success
        if (results.authors_books>0) {
            //Author has books. Render in same way as for GET route.
            res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books } );
            return;
        }
        else {
            //Author has no books. Delete object and redirect to the list of authors.
            Author.findByIdAndRemove(req.body.authorid, function deleteAuthor(err) {
                if (err) { return next(err); }
                //Success - got to author list
                res.redirect('/catalog/authors');
            });

        }
    });

};

首先,我们验证是否已经提供了一个ID(通过表单主体参数发送,而不是使用URL中的版本)。 然后,我们以与 GET 路由相同的方式获取作者及其相关联的书籍。 如果没有书,那么我们删除作者对象并重定向到所有作者的列表。 如果还有书,我们只需重新呈现表单,传入作者和要删除的书籍列表。

视图

创建 /views/author_delete.pug 并在下面的文字中复制。

extends layout

block content
  h1 #{title}: #{author.name}
  p= author.lifespan
  
  if author_books.length
  
    p #[strong Delete the following books before attempting to delete this author.]
  
    div(style='margin-left:20px;margin-top:20px')

      h4 Books
    
      dl
      each book in author_books
        dt 
          a(href=book.url) #{book.title}
        dd #{book.summary}

  else
    p Do you really want to delete this Author?
    
    form(method='POST' action='')
      div.form-group
        input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id )

      button.btn.btn-primary(type='submit') Delete

视图扩展了布局模板,覆盖了名为 content 的块。 在顶部显示作者详细信息。 然后它包括基于 author_books ( if else )。

  • If there are books associated with the author then the page lists the books and states that these must be deleted before this Author may be deleted.
  • If there are no books, then the page displays a confirmation prompt. If the Delete button is clicked then the author id is sent to the server in a POST request and it will be deleted.

添加删除按钮

接下来,我们将删除按钮添加到作者详细信息视图中(详细信息页是删除记录的好地方)。

注意:在完整实施中,只有授权用户才能看到此按钮。 但是在这一点上,我们没有一个授权系统到位!

打开 author_detail.pug 视图,然后在底部添加以下几行。

  hr
  p
    a(href=author.url+'/delete') Delete author

此时,该按钮应如下所示出现在作者详细信息页面上。

margin:0px auto; width:500px;">

它是什么样子的?

运行应用程序并打开浏览器以 http:// localhost:3000 / 。 然后选择所有作者链接,然后选择特定作者。 最后,选择删除作者链接。

如果作者没有书,你会看到一个这样的页面。 按下删除后,服务器将删除作者并重定向到作者列表。

margin:0px auto; width:600px;">

如果作者有书,那么你将看到如下的视图。 然后,您可以从其详细信息页面中删除图书(一旦代码实施!)

margin:0px auto; width:500px;">

注意:删除对象的其他页面可以以大致相同的方式实施。 我们已经离开了这是一个挑战。

更新书形式

本节说明如何定义一个页面来更新 Book 对象。 更新图书时的表单处理非常类似于创建图书,只不过您必须使用数据库中的值填充 GET 路由中的表单。

控制器 - 获取路由

打开 /controllers/bookController.js 。 找到导出的 book_update_get()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Display book update form on GET
exports.book_update_get = function(req, res, next) {

    req.sanitize('id').escape();
    req.sanitize('id').trim();

    //Get book, authors and genres for form
    async.parallel({
        book: function(callback) {
            Book.findById(req.params.id).populate('author').populate('genre').exec(callback);
        },
        authors: function(callback) {
            Author.find(callback);
        },
        genres: function(callback) {
            Genre.find(callback);
        },
    }, function(err, results) {
        if (err) { return next(err); }
            
        // Mark our selected genres as checked
        for (var all_g_iter = 0; all_g_iter < results.genres.length; all_g_iter++) {
            for (var book_g_iter = 0; book_g_iter < results.book.genre.length; book_g_iter++) {
                if (results.genres[all_g_iter]._id.toString()==results.book.genre[book_g_iter]._id.toString()) {
                    results.genres[all_g_iter].checked='true';
                }
            }
        }
        res.render('book_form', { title: 'Update Book', authors:results.authors, genres:results.genres, book: results.book });
    });
    
};

控制器从URL参数( req.params.id )获取要更新的 Book 的id。 它使用 async.parallel()方法来获取指定的 Book 记录(填充其流派和作者字段)以及所有Author和Genre对象的列表。 当所有操作完成后,将当前选定的类型标记为选中,然后呈现 author_form.pug 视图,传递 title 代码>和所有 genres

控制器 - 路由

找到导出的 book_update_post()控制器方法,并将其替换为以下代码(更改的代码以粗体显示)。

// Handle book update on POST 
exports.book_update_post = function(req, res, next) {
    
    //Sanitize id passed in. 
    req.sanitize('id').escape();
    req.sanitize('id').trim();
    
    //Check other data
    req.checkBody('title', 'Title must not be empty.').notEmpty();
    req.checkBody('author', 'Author must not be empty').notEmpty();
    req.checkBody('summary', 'Summary must not be empty').notEmpty();
    req.checkBody('isbn', 'ISBN must not be empty').notEmpty();
    
    req.sanitize('title').escape();
    req.sanitize('author').escape();
    req.sanitize('summary').escape();
    req.sanitize('isbn').escape();
    req.sanitize('title').trim();
    req.sanitize('author').trim(); 
    req.sanitize('summary').trim();
    req.sanitize('isbn').trim();
    req.sanitize('genre').escape();
    
    var book = new Book(
      { title: req.body.title, 
        author: req.body.author, 
        summary: req.body.summary,
        isbn: req.body.isbn,
        genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre.split(","),
        _id:req.params.id //This is required, or a new ID will be assigned!
       });
    
    var errors = req.validationErrors();
    if (errors) {
        // Re-render book with error information
        // Get all authors and genres for form
        async.parallel({
            authors: function(callback) {
                Author.find(callback);
            },
            genres: function(callback) {
                Genre.find(callback);
            },
        }, function(err, results) {
            if (err) { return next(err); }
            
            // Mark our selected genres as checked
            for (i = 0; i < results.genres.length; i++) {
                if (book.genre.indexOf(results.genres[i]._id) > -1) {
                    results.genres[i].checked='true';
                }
            }
            res.render('book_form', { title: 'Update Book',authors:results.authors, genres:results.genres, book: book, errors: errors });
        });

    } 
    else {
        // Data from form is valid. Update the record.
        Book.findByIdAndUpdate(req.params.id, book, {}, function (err,thebook) {
            if (err) { return next(err); }
               //successful - redirect to book detail page.
               res.redirect(thebook.url);
            });
    }

};

这与创建图书时使用的路径非常相似。 首先,我们从表单中验证和清理图书数据,并使用它来创建一个新的 Book 对象(将其 _id 值设置为要更新的对象的id)。 如果在验证数据时存在错误,那么我们重新呈现表单,另外显示用户输入的数据,错误以及类型和作者列表。 如果没有错误,我们调用 Book.findByIdAndUpdate()来更新 Book 文档,然后重定向到它的详细页面。

视图

打开 /views/book_form.pug 并更新作者表单控件设置为具有下面粗体显示的条件代码的部分。

    div.form-group
      label(for='author') Author:
      select#author.form-control(type='select', placeholder='Select author' name='author' required='true' )
        for author in authors
          if book
            option(value=author._id selected=(author._id.toString()==book.author._id.toString() ? 'selected' : false) ) #{author.name}
          else
            option(value=author._id) #{author.name}

注意:需要更改此代码,以便book_form可用于创建和更新图书对象(如果没有这一点,则在创建图书对象时, GET 形成)。

添加更新按钮

打开 book_detail.pug 视图,并确保在页面底部有用于删除和更新图书的链接,如下所示。

  hr
  p
    a(href=book.url+'/delete') Delete Book
  p
    a(href=book.url+'/update') Update Book

您现在应该可以从图书详细信息页面更新图书。

它是什么样子的?

运行应用程序,打开浏览器 http:// localhost:3000 / ,选择所有图书 em>链接,然后选择一本特定的书。 最后,选择更新图书链接。

表单应该像创建图书页面,只有标题为"更新书籍",并且预先填充了记录值。

margin:0px auto; width:1000px;">

注意:用于更新对象的其他页面可以以大致相同的方式实施。 我们已经离开了这是一个挑战。

挑战自己

实现 Book BookInstance Genre 模型的删除页面,以相同的方式将它们从相关联的详细信息页面链接 >作者删除页面。 页面应该遵循相同的设计方法:

  • If there are references to the object from other objects, then these other objects should be displayed along with a note that this record can't be deleted until the listed objects have been deleted.
  • If there are no other references to the object then the view should prompt to delete it. If the user presses the Delete button, the record should then be deleted.

几个提示:

  • Deleting a Genre is just like deleting an Author as both objects are dependencies of Books (so in both cases you can only delete the object when the associated books are deleted.
  • Deleting a Book is also similar, but you need to check that there are no associated BookInstances.
  • Deleting a BookInstance is the easiest of all, because there are no dependent objects. In this case you can just find the associated record and delete it.

实现 BookInstance 作者类型模型的更新页面,以与我们的 >预订更新页面。

几个提示:

  • The Book update page we just implemented is the hardest! The same patterns can be used for the update pages for the other objects.
  • The Author date of death and date of birth fields, and the BookInstance due_date field are the wrong format to input into the date input field on the form (it requires data in form "YYYY-MM-DD"). The easiest way to get around this is to define a new virtual property for the dates that formats the dates appropriately, and then use this field in the associated view templates.
  • If you get stuck, there are examples of the update pages in the example here.

概要

NPM上的 Express ,节点和第三方软件包提供您在网站中添加表单所需的一切。 在本文中,您学习了如何使用 Pug 创建表单,使用 express-validator 验证和清理输入,以及添加,删除和修改数据库中的记录。

您现在应该了解如何向您自己的节点网站添加基本表单和表单处理代码!

也可以看看

Express/Node introduction
Setting up a Node development environment
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录
CSS

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }