Learn web development

Express Tutorial Part 6: Working with forms

我们的志愿者还没有将这篇文章翻译为 中文 (简体)加入我们帮助完成翻译!
您也可以阅读此文章的English (US)版。

In this tutorial we'll show you how to work with HTML Forms in Express, using Pug, and in particular how to write forms to create, update, and delete documents from the database.

Prerequisites: Complete all previous tutorial topics, including Express Tutorial Part 5: Displaying library data
Objective: To understand how write forms to get data from users, and update the database with this data.

Overview

An HTML Form is a group of one or more fields/widgets on a web page that can be used to collect information from users for submission to a server. Forms are a flexible mechanism for collecting user input because there are suitable form inputs available for entering many different types of data—text boxes, checkboxes, radio buttons, date pickers, etc. Forms are also a relatively secure way of sharing data with the server, as they allow us to send data in POST requests with cross-site request forgery protection.

Working with forms can be complicated! Developers need to write HTML for the form, validate and properly sanitize entered data on the server (and possibly also in the browser), repost the form with error messages to inform users of any invalid fields, handle the data when it has successfully been submitted, and finally respond to the user in some way to indicate success.

In this tutorial we're going to show you how the above operations may be performed in Express. Along the way we'll extend the LocalLibrary website to allow users to create, edit and delete items from the library.

Note: We haven't looked at how to restrict particular routes to authenticated or authorised users, so at this point any user will be able to make changes to the database.

HTML Forms

First a brief overview of HTML Forms. Consider a simple HTML form, with a single text field for entering the name of some "team", and its associated label:

Simple name field example in HTML form

The form is defined in HTML as a collection of elements inside <form>...</form> tags, containing at least one input element of 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>

While here we have included just one (text) field for entering the team name, a form may contain any number of other input elements and their associated labels. The field's type attribute defines what sort of widget will be displayed. The name and id of the field are used to identify the field in JavaScript/CSS/HTML, while value defines the initial value for the field when it is first displayed. The matching team label is specified using the label tag (see "Enter name" above), with a for field containing the id value of the associated input.

The submit input will be displayed as a button (by default)—this can be pressed by the user to upload the data contained by the other input elements to the server (in this case, just the team_name). The form attributes define the HTTP method used to send the data and the destination of the data on the server (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.

Form handling process

Form handling uses all of the same techniques that we learned for displaying information about our models: the route sends our request to a controller function which performs any database actions required, including reading data from the models, then generates and returns an HTML page. What makes things more complicated is that the server also needs to be able to process data provided by the user, and redisplay the form with error information if there are any problems.

A process flowchart for processing form requests is shown below, starting with a request for a page containing a form (shown in green):

As shown in the diagram above, the main things that form handling code needs to do are are:

  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 sanitize 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.

Often form handling code is implemented using a GET route for the initial display of the form and a POST route to the same path for handling validation and processing of form data. This is the approach that will be used in this tutorial!

Express itself doesn't provide any specific support for form handling operations, but it can use middleware to process POST and GET parameters from the form, and to validate/sanitize their values.

Validation and sanitization

Before the data from a form is stored it must be validated and sanitized:

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

For this tutorial we'll be using the popular express-validator module to perform both validation and sanitization of our form data.

Installation

Install the module by running the following command in the root of the project.

npm install express-validator --save

Add the validator to the app middleware

Open ./app.js and import the express-validator module after the other modules (near the top of the file, as shown).

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

Further down in the file call app.use() to add the validator to the middleware stack. This should be done after the code that adds the bodyParser to the middleware stack (express-validator uses the body-parser to access parameters).

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

Using express-validator

For this tutorial, we'll primarily be using the following APIs:

  • 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();

To run the validation we call req.validationErrors(). This returns an array of error objects (or false if there are no errors) and is typically used like this

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).
}

Each error object in the array has values for the parameter, message and value.

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

Note: The API also has methods to check and sanitize query and url parameters (not just body parameters as shown). For more information see: express-validator (npm).

We'll cover more examples when we implement the LocalLibrary forms below.

Form design

Many of the models in the library are related/dependent—for example, a Book requires an Author, and may also have one or more Genres. This raises the question of how we should handle the case where a user wishes to:

  • 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).

For this project we will simplify the implementation by stating that a form can only:

  • 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).

Note: A more "robust" implementation might allow you to create the dependent objects when creating a new object, and delete any object at any time (for example, by deleting dependent objects, or by removing references to the deleted object from the database).

Routes

In order to implement our form handling code we will need two routes that have the same URL pattern. The first (GET) route is used to display a new empty form for creating the object. The second route (POST) is used for validating data entered by the user, and then saving the information and redirecting to the detail page (if the data is valid) or redisplaying the form with errors (if the data is invalid).

We have already created the routes for all our model's create pages in /routes/catalog.js (in a previous tutorial). For example, the genre routes are shown below:

/* 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);

Create genre form

This section shows how we define our page to create Genre objects (this is a good place to start because the Genre has only one field, its name, and no dependencies). Like any other pages, we need to set up routes, controllers, and views.

Controller—get route

Open /controllers/genreController.js. Find the exported genre_create_get() controller method and replace it with the following code. This simply renders the genre_form.pug view, passing a title variable.

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

Controller—post route

Find the exported genre_create_post() controller method and replace it with the following code.

// 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);
                     });
                     
                 }
                 
             });
    }
};

The first part of this code defines a validator to check that the name field is not empty (and an error message if it is empty), sanitizes and trims (removes whitespace at either end of the string) the value, and then runs the validator.

//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();

If there are errors we render the template with the form again, this time additionally passing a variable with any (sanitized) values passed by the user, and an object containing the error information.

//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;
} 

If the data is valid then we check if a Genre with the same name already exists (we don't want to create duplicates). If it does we redirect to the existing genre's detail page. If not, we save the new Genre and redirect to its detail page.

//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);
                });
        }
                 
});

View

The same view is rendered in both the GET and POST controllers (routes). In the GET case the form is empty and we just pass a title variable. In the POST case the user has previously entered invalid data—in the genre variable we pass back the entered data (sanitized) and in the errors variable we pass back error messages.

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

Create /views/genre_form.pug and copy in the text below.

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

Much of this template will be familiar from our previous tutorials. First we extend the layout.pug base template and override the block named 'content'. We then have a heading with the title we passed in from the controller (via the render() method).

Next we have the pug code for our HTML form that uses the POST method to send the data to the server, and because the action is an empty string, will send the data to the same URL as the page.

The form defines a single required field of type "text" called "name". The default value of the field depends on whether the genre variable is defined. If called from the GET route it will be empty as this is a new form. If called from a POST route it will contain the (invalid) value originally entered by the user.

The last part of the page is the error code. This simply prints a list of errors, if the error variable has been defined (in other words, this section will not appear when the template is rendered on the GET route).

Note: This is just one way to render the errors. You can also get the names of the affected fields from the error variable, and use these to control where the error messages are rendered, whether to apply custom CSS, etc.

What does it look like?

Run the application, open your browser to http://localhost:3000/, then select the Create new genre link. If everything is set up correctly, your site should look something like the following screenshot. After you enter a value, it should be saved and you'll be taken to the genre detail page.

Genre Create Page - Express Local Library site

The only error we validate against server-side is that the genre field must not be empty. The screenshot below shows what the error list would look like if you didn't supply a genre (highlighted in red).

Note: Our validation is not foolproof. Since we check for an empty field and then trim the value, an entry consisting only of spaces will become empty and then cause an error. A better implementation would deal with that case. An even better one would validate that the field is not empty on the client side. Add the value required='true' to the field definition in the form:

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

Create author form

This section shows how to define a page for creating Author objects.

Controller—get route

Open /controllers/authorController.js. Find the exported author_create_get() controller method and replace it with the following code. This simply renders the author_form.pug view, passing a title variable.

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

Controller—post route

Find the exported author_create_post() controller method, and replace it with the following code.

// 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);
            });
    }
};

The structure and behaviour of this code is almost exactly the same as for creating a Genre object. First we validate and sanitize the data. If the data is invalid then we re-display the form along with the data that was originally entered by the user and a list of error messages. If the data is valid then we save the new author record and redirect the user to the author detail page.

Note: Unlike with the Genre post handler, we don't check whether the Author object already exists before saving it. Arguably we should, though as it is now we can have multiple authors with the same name.

The validation code demonstrates two new features:

  • 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()
    

View

Create /views/author_form.pug and copy in the text below.

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

The structure and behaviour for this view is exactly the same as for the genre_form.pug template, so we won't describe it again.

Note: Some browsers don’t support the input type=“date”, so you won’t get the datepicker widget or the default dd/mm/yyyy placeholder, but will instead get an empty plain text field. One workaround is to explicitly add the attribute placeholder='dd/mm/yyyy' so that on less capable browsers you will still get information about the desired text format.

Challenge: The template above is missing a field for entering the date_of_death. Create the field following the same pattern as the date of birth form group!

What does it look like?

Run the application, open your browser to http://localhost:3000/, then select the Create new author link. If everything is set up correctly, your site should look something like the following screenshot. After you enter a value, it should be saved and you'll be taken to the author detail page.

Author Create Page - Express Local Library site

Note: If you experiment with various input formats for the dates, you may find that the format yyyy-mm-dd misbehaves. This is because JavaScript treats date strings as including the time of 0 hours, but additionally treats date strings in that format (the ISO 8601 standard) as including the time 0 hours UTC, rather than the local time. If your time zone is west of UTC, the date display, being local, will be one day before the date you entered. This is one of several complexities (such as multi-word family names and multi-author books) that we are not addressing here.

Create book form

This section shows how to define a page/form to create Book objects. This is a little more complicated than the equivalent Author or Genre pages because we need to get and display available Author and Genre records in our Book form.

Controller—get route

Open /controllers/bookController.js. Find the exported book_create_get() controller method and replace it with the following code.

// 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 });
    });
    
};

This uses the async module (described in Express Tutorial Part 5: Displaying library data) to get all Author and Genre objects. These are then passed to the view book_form.pug as variables named authors and genres (along with the page title).

Controller—post route

Find the exported book_create_post() controller method and replace it with the following code.

// 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);
        });
    }
};

The structure and behaviour of this code is almost exactly the same as for creating a Genre object. First we validate and sanitize the data. If the data is invalid then we re-display the form along with the data that was originally entered by the user and a list of error messages. If the data is valid, we then save the new Book record and redirect the user to the book detail page.

Again, the main difference with respect to the other form handling code is that we need to pass in all existing genres and authors to the form. In order to mark the genres that were checked by the user we iterate through all the genres and add the checked='true' parameter to those that were in our post data (as reproduced in the code fragment below).

// 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';
    }
}

View

Create /views/book_form.pug and copy in the text below.

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 ? '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

The view structure and behaviour is almost the same as for the genre_form.pug template.

The main differences are in how we implement the selection-type fields: Author and Genre.

  • 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!

    Note: If there is an error in the submitted form, then, when the form is to be re-rendered, the new book's author is identified only with a string (the value of the selected option in the list of authors). By contrast, the existing books' authors have _id properties that are not strings. So to compare the new with the existing we must cast each existing book's author's _id to a string, as shown above.

What does it look like?

Run the application, open your browser to http://localhost:3000/, then select the Create new book link. If everything is set up correctly, your site should look something like the following screenshot. After you submit a valid book, it should be saved and you'll be taken to the book detail page.

Create BookInstance form

This section shows how to define a page/form to create BookInstance objects. This is very much like the form we used to create Book objects.

Controller—get route

Open /controllers/bookinstanceController.js.

At the top of the file, require the Book module (needed because each BookInstance is associated with a particular Book).

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

Find the exported bookinstance_create_get() controller method and replace it with the following code.

// 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});
    });
    
};

The controller gets a list of all books (book_list) and passes it to the view bookinstance_form.pug (along with the title)

Controller—post route

Find the exported bookinstance_create_post() controller method and replace it with the following code.

// 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 book titles 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 book-instance record.
               res.redirect(bookinstance.url);
            }); 
    }
};

The structure and behaviour of this code is the same as for creating our other objects. First we validate and sanitize the data. If the data is invalid, we then re-display the form along with the data that was originally entered by the user and a list of error messages. If the data is valid, we save the new BookInstance record and redirect the user to the detail page.

View

Create /views/bookinstance_form.pug and copy in the text below.

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
          if bookinstance
            option(value=book._id selected=(bookinstance.book.toString()==book._id.toString() ? 'selected' : false)) #{book.title}
          else
            option(value=book._id) #{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

The view structure and behaviour is almost the same as for the book_form.pug template, so we won't go over it again.

Note: The above template hard-codes the Status values (Maintenance, Available, etc.) and does not "remember" the user's entered values. Should you so wish, consider reimplementing the list, passing in option data from the controller and setting the selected value when the form is re-displayed.

What does it look like?

Run the application and open your browser to http://localhost:3000/. Then select the Create new book instance (copy) link. If everything is set up correctly, your site should look something like the following screenshot. After you submit a valid BookInstance, it should be saved and you'll be taken to the detail page.

Delete author form

This section shows how to define a page to delete Author objects.

As discussed in the form design section, our strategy will be to only allow deletion of objects that are not referenced by other objects (in this case that means we won't allow an Author to be deleted if it is referenced by a Book). In terms of implementation this means that the form needs to confirm that there are no associated books before the author is deleted. If there are associated books, it should display them, and state that they must be deleted before the Author object can be deleted.

Controller—get route

Open /controllers/authorController.js. Find the exported author_delete_get() controller method and replace it with the following code.

// 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 } );
    });
    
};

The controller gets the id of the Author instance to be deleted from the URL parameter (req.params.id). It uses the async.parallel() method to get the author record and all associated books in parallel. When both operations have completed it renders the author_delete.pug view, passing variables for the title, author, and author_books.

Controller—post route

Find the exported author_delete_post() controller method, and replace it with the following code.

// 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.length > 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');
            });
        }
    });
};

First we validate that an id has been provided (this is sent via the form body parameters, rather than using the version in the URL). Then we get the author and their associated books in the same way as for the GET route. If there are no books then we delete the author object and redirect to the list of all authors. If there are still books then we just re-render the form, passing in the author and list of books to be deleted.

View

Create /views/author_delete.pug and copy in the text below.

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

The view extends the layout template, overriding the block named content. At the top it displays the author details. It then includes a conditional statement based on the number of author_books (the if and else clauses).

  • 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 that author's record will be deleted.

Add a delete control

Next we will add a Delete control to the Author detail view (the detail page is a good place from which to delete a record).

Note: In a full implementation the control would be made visible only to authorised users. However at this point we haven't got an authorisation system in place!

Open the author_detail.pug view and add the following lines at the bottom.

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

The control should now appear as a link, as shown below on the Author detail page.

What does it look like?

Run the application and open your browser to http://localhost:3000/. Then select the All authors link, and then select a particular author. Finally select the Delete author link.

If the author has no books, you'll be presented with a page like this. After pressing delete, the server will delete the author and redirect to the author list.

If the author does have books, then you'll be presented with a view like the following. You can then delete the books from their detail pages (once that code is implemented!).

Note: The other pages for deleting objects can be implemented in much the same way. We've left that as a challenge.

Update Book form

This section shows how to define a page to update Book objects. Form handling when updating a book is much like that for creating a book, except that you must populate the form in the GET route with values from the database.

Controller—get route

Open /controllers/bookController.js. Find the exported book_update_get() controller method and replace it with the following code.

// 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 });
    });
    
};

The controller gets the id of the Book to be updated from the URL parameter (req.params.id). It uses the async.parallel() method to get the specified Book record (populating its genre and author fields) and lists of all the Author and Genre objects. When all operations have completed it marks the currently selected genres as checked and then renders the book_form.pug view, passing variables for title, book, all authors, and all genres.

Controller—post route

Find the exported book_update_post() controller method, and replace it with the following code.

// 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);
        });
    }
};

This is very similar to the post route used when creating a Book. First we validate and sanitize the book data from the form and use it to create a new Book object (setting its _id value to the id of the object to update). If there are errors when we validate the data then we re-render the form, additionally displaying the data entered by the user, the errors, and lists of genres and authors. If there are no errors then we call Book.findByIdAndUpdate() to update the Book document, and then redirect to its detail page.

View

Open /views/book_form.pug and update the section where the author form control is set to have the conditional code shown below.

    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
            //- Handle GET form, where book.author is an object, and POST form, where it is a string.
            option(
              value=author._id
              selected=(
                author._id.toString()==book.author._id
                || author._id.toString()==book.author
              ) ? 'selected' : false
            ) #{author.name}
          else
            option(value=author._id) #{author.name}

Note: This code change is required so that the book_form can be used for both creating and updating book objects (without this, there is an error on the GET route when creating a form).

Add an update button

Open the book_detail.pug view and make sure there are links for both deleting and updating books at the bottom of the page, as shown below.

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

You should now be able to update books from the Book detail page.

What does it look like?

Run the application, open your browser to http://localhost:3000/, select the All books link, then select a particular book. Finally select the Update Book link.

The form should look just like the Create book page, only with a title of 'Update book', and pre-populated with record values.

Note: The other pages for updating objects can be implemented in much the same way. We've left that as a challenge.

Challenge yourself

Implement the delete pages for the Book, BookInstance, and Genre models, linking them from the associated detail pages in the same way as our Author delete page. The pages should follow the same design approach:

  • 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.

A few tips:

  • Deleting a Genre is just like deleting an Author as both objects are dependencies of Books (so in both cases you can delete the object only 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.

Implement the update pages for the BookInstance, Author, and Genre models, linking them from the associated detail pages in the same way as our Book update page.

A few tips:

  • 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.

Summary

Express, node, and third party packages on NPM provide everything you need to add forms to your website. In this article you've learned how to create forms using Pug, validate and sanitize input using express-validator, and add, delete, and modify records in the database.

You should now understand how to add basic forms and form-handling code to your own node websites!

See also

文档标签和贡献者