Learn web development

Express Tutorial Part 5: Displaying library data

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

We're now ready to add the pages that display the LocalLibrary website books and other data. The pages will include a home page that shows how many records we have of each model type, and list and detail pages for all of our models. Along the way we'll gain practical experience in getting records from the database, and using templates.

Prerequisites: Complete previous tutorial topics (including Express Tutorial Part 4: Routes and controllers).
Objective: To understand how to use the async module and Pug template language, and how to get data from the URL in our controller functions.

Overview

In our previous tutorial articles we defined Mongoose models that we can use to interact with a database and created some initial library records. We then created all the routes needed for the LocalLibrary website, but with "dummy controller" functions (these are skeleton controller functions that just return a "not implemented" message when a page is accessed).

The next step is to provide proper implementations for the pages that display our library information (we'll look at implementing pages featuring forms to create, update, or delete information in later articles). This includes updating the controller functions to fetch records using our models, and defining templates to display this information to users.

We will start by providing overview/primer topics explaining how to manage asynchronous operations in controller functions and how to write templates using Pug. Then we'll provide implementations for each of our main "read only" pages with a brief explanation of any special or new features that they use.

At the end of this article you should have a good end-to-end understanding of how routes, asynchronous functions, views, and models work in practice.

Asynchronous flow control using async

The controller code for some of our LocalLibrary pages will depend on the results of multiple asynchronous requests, which may be required to run either in some particular order or in parallel. In order to manage flow control, and render pages when we have all the required information available, we'll use the popular node async module.

Note: There are a number of other ways to manage asynchronous behaviour and flow control in JavaScript, including recent JavaScript language features like Promises.

Async has a lot of useful methods (check out the documentation). Some of the more important functions are:

  • async.parallel() to execute any operations that must be performed in parallel.
  • async.series() for when we need to ensure that asynchronous operations are performed in series.
  • async.waterfall() for operations that must be run in series, with each operation depending on the results of preceding operations.

Why is this needed?

Most of the methods we use in Express are asynchronous—you specify an operation to perform, passing a callback. The method returns immediately, and the callback is invoked when the requested operation completes. By convention in Express, callback functions pass an error value as the first parameter (or null on success) and the results from the function (if there are any) as the second parameter.

If a controller only needs to perform one asynchronous operation to get the information required to render a page then the implementation is easy—we simply render the template in the callback. The code fragment below shows this for a function that renders the count of a model SomeModel (using the Mongoose count() method):

exports.some_model_count = function(req, res, next) {
  SomeModel.count({ a_model_field: 'match_value' }, function (err, count) {
    // ... do something if there is an err
    // On success, render the result by passing count into the render function (here, as the variable 'data')
    res.render('the_template', { data: count } );
  });
}

However what if you need to make multiple asynchronous queries, and you can't render the page until all the operations have completed? A naive implementation could "daisy chain" the requests, kicking off subsequent requests in the callback of a previous request, and rendering the response in the final callback. The problem with this approach is that our requests would have to be run in series, even though it might be more efficient to run them in parallel. This could also result in complicated nested code, commonly referred to as callback hell.

A much better solution would be to execute all the requests in parallel and then have a single callback that executes when all of the queries have completed. This is the sort of flow operation that the Async module makes easy!

Asynchronous operations in parallel

The method async.parallel() is used to run multiple asynchronous operations in parallel.

The first argument to async.parallel() is a collection of the asynchronous functions to run (an array, object or other iterable). Each function is passed a callback(err, result) which it must call on completion with an error err (which can be null) and an optional results value.

The optional second argument to  async.parallel() is a callback that will be run when all the functions in the first argument have completed. The callback is invoked with an error argument and a result collection that contains the results of the individual asynchronous operations. The result collection is of the same type as the first argument (i.e. if you pass an array of asynchronous functions, the final callback will be invoked with an array of results). If any of the parallel functions reports an error the callback is invoked early (with the error value).

The example below shows how this works when we pass an object as the first argument. As you can see, the results are returned in an object with the same property names as the original functions that were passed in.

async.parallel({
  one: function(callback) { ... },
  two: function(callback) { ... },
  ...
  something_else: function(callback) { ... }
  },
  // optional callback
  function(err, results) {
    // 'results' is now equal to: {one: 1, two: 2, ..., something_else: some_value}
  }
);

If you instead pass an array of functions as the first argument, the results will be an array (the array order results will match the original order that the functions were declared—not the order in which they completed).

Asynchronous operations in series

The method async.series() is used to run multiple asynchronous operations in sequence, when subsequent functions do not depend on the output of earlier functions. It is essentially declared and behaves in the same way as async.parallel().

async.series({
  one: function(callback) { ... },
  two: function(callback) { ... },
  ...
  something_else: function(callback) { ... }
  },
  // optional callback after the last asynchronous function completes.
  function(err, results) {
    // 'results' is now equals to: {one: 1, two: 2, ..., something_else: some_value}
  }
);

Note: The ECMAScript (JavaScript) language specification states that the order of enumeration of an object is undefined, so it is possible that the functions will not be called in the same order as you specify them on all platforms. If the order really is important, then you should pass an array instead of an object, as shown below.

async.series([
  function(callback) {
    // do some stuff ...
    callback(null, 'one');
  },
  function(callback) {
    // do some more stuff ...
    callback(null, 'two');
  }
 ],
  // optional callback
  function(err, results) {
  // results is now equal to ['one', 'two']
  }
);

Dependent asynchronous operations in series

The method async.waterfall() is used to run multiple asynchronous operations in sequence when each operation is dependent on the result of the previous operation.

The callback invoked by each asynchronous function contains null for the first argument and results in subsequent arguments. Each function in the series takes the results arguments of the previous callback as the first parameters, and then a callback function. When all operations are complete, a final callback is invoked with the result of the last operation. The way this works is more clear when you consider the code fragment below (this example is from the async documentation):

async.waterfall([
  function(callback) {
    callback(null, 'one', 'two');
  },
  function(arg1, arg2, callback) {
    // arg1 now equals 'one' and arg2 now equals 'two'
    callback(null, 'three');
  },
  function(arg1, callback) {
    // arg1 now equals 'three'
    callback(null, 'done');
  }
], function (err, result) {
  // result now equals 'done'
}
);

Installing async

Install the async module using the NPM package manager so that we can use it in our code. You do this in the usual way, by opening a prompt in the root of the LocalLibrary project and enter the following command:

npm install async --save

Template primer

A template is a text file defining the structure or layout of an output file, with placeholders used to represent where data will be inserted when the template is rendered (in Express, templates are referred to as views).

Express can be used with many different template rendering engines. In this tutorial we use Pug (formerly known as Jade) for our templates. This is the most popular Node template language, and describes itself as a "clean, whitespace-sensitive syntax for writing HTML, heavily influenced by Haml".

Different template languages use different approaches for defining the layout and marking placeholders for data—some use HTML to define the layout while others use different markup formats that can be compiled to HTML. Pug is of the second type; it uses a representation of HTML where the first word in any line usually represents an HTML element, and indentation on subsequent lines is used to represent any content nested within those elements. The result is a page definition that translates directly to HTML, but is arguably more concise and easier to read.

Note: The downside of using Pug is that it is sensitive to indentation and whitespace (if you add an extra space in the wrong place you may get an unhelpful error code). However once you have your templates in place, they are very easy to read and maintain.

Template configuration

The LocalLibrary was configured to use Pug when we created the skeleton website. You should see the pug module included as a dependency in the website's package.json file, and the following configuration settings in the app.js file. The settings tell us that we're using pug as the view engine, and that Express should search for templates in the /views subdirectory.

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

If you look in the views directory you will see the .pug files for the project's default views. These include the view for the home page (index.pug) and base template (layout.pug) that we will need to replace with our own content.

/express-locallibrary-tutorial  //the project root
  /views
    error.pug
    index.pug
    layout.pug

Template syntax

The example template file below shows off many of Pug's most useful features.

The first thing to notice is that the file maps the structure of a typical HTML file, with the first word in (almost) every line being an HTML element, and indentation being used to indicate nested elements. So for example, the body element is inside an html element, and paragraph elements (p) are within the body element, etc. Non-nested elements (e.g. individual paragraphs) are on separate lines.

doctype html
html(lang="en")
  head
    title= title
    script(type='text/javascript').
  body
    h1= title
    p This is a line with #[em some emphasis] and #[strong strong text] markup.
    p This line has un-escaped data: !{'<em> is emphasised</em>'} and escaped data: #{'<em> is not emphasised</em>'}.
      | This line follows on.
    p= 'Evaluated and <em>escaped expression</em>:' + title
    <!-- You can add HTML comments directly -->
    // You can add single line JavaScript comments and they are generated to HTML comments
    //- Introducing a single line JavaScript comment with "//-" ensures the comment isn't rendered to HTML
    p A line with a link
      a(href='/catalog/authors') Some link text
      |  and some extra text.
    #container.col
      if title
        p A variable named "title" exists.
      else
        p A variable named "title" does not exist.
      p.
        Pug is a terse and simple template language with a
        strong focus on performance and powerful features.
    h2 Generate a list
    ul
      each val in [1, 2, 3, 4, 5]
        li= val

Element attributes are defined in parentheses after their associated element. Inside the parentheses, the attributes are defined in comma- or whitespace- separated lists of the pairs of attribute names and attribute values, for example:

  • script(type='text/javascript'), link(rel='stylesheet', href='/stylesheets/style.css')
  • meta(name='viewport' content='width=device-width initial-scale=1')

The values of all attributes are escaped (e.g. characters like ">" are converted to their HTML code equivalents like "&gt;") to prevent injection of JavaScript/cross-site scripting attacks.

If a tag is followed by the equals sign, the following text is treated as a JavaScript expression. So for example, in the first line below, the content of the h1 tag will be variable title (either defined in the file or passed into the template from Express). In the second line the paragraph content is a text string concatented with the title variable. In both cases the default behaviour is to escape the line.

h1= title
p= 'Evaluated and <em>escaped expression</em>:' + title

If there is no equals symbol after the tag then the content is treated as plain text. Within the plain text you can insert escaped and unescaped data using the #{} and !{} syntax, as shown below. You can also add raw HTML within the plain text.

p This is a line with #[em some emphasis] and #[strong strong text] markup.
p This line has an un-escaped string: !{'<em> is emphasised</em>'}, an escaped string: #{'<em> is not emphasised</em>'}, and escaped variables: #{title}.

Tip: You will almost always want to escape data from users (via the #{} syntax). Data that can be trusted (e.g. generated counts of records, etc.) may be be displayed without escaping the values.

You can use the pipe ('|') character at the beginning of a line to indicate "plain text". For example, the additional text shown below will be displayed on the same line as the preceding anchor, but will not be linked.

a(href='http://someurl/') Link text
| Plain text

Pug allows you to perform conditional operations using if, else , else if and unless—for example:

if title
  p A variable named "title" exists
else
  p A variable named "title" does not exist

You can also perform loop/iteration operations using each-in or while syntax. In the code fragment below we've looped through an array to display a list of variables (note the use of the 'li=' to evaluate the "val" as a variable below. The value you iterate across can also be passed into the template as a variable!

ul
  each val in [1, 2, 3, 4, 5]
    li= val

The syntax also supports comments (that can be rendered in the output—or not—as you choose), mixins to create reusable blocks of code, case statements, and many other features. For more detailed information see The Pug docs.

Extending templates

Across a site, it is usual for all pages to have a common structure, including standard HTML markup for the head, footer, navigation, etc. Rather than forcing developers to duplicate this "boilerplate" in every page, Pug allows you to declare a base template and then extend it, replacing just the bits that are different for each specific page.

For example, the base template layout.pug created in our skeleton project looks like this:

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content

The block tag is used to mark up sections of content that may be replaced in a derived template (if the block is not redefined then its implementation in the base class is used).

The default index.pug (created for our skeleton project) shows how we override the base template. The extends tag identifies the base template to use, and then we use block section_name to indicate the new content of the section that we will override.

extends layout
block content
  h1= title
  p Welcome to #{title}

The LocalLibrary base template

Now that we understand how to extend templates using Pug, let's start by creating a base template for the project. This will have a sidebar with links for the pages we hope to create across the tutorial articles (e.g. to display and create books, genres, authors, etc.) and a main content area that we'll override in each of our individual pages.

Open /views/layout.pug and replace the content with the code below.

doctype html
html(lang='en')
  head
    title= title
    meta(charset='utf-8')
    meta(name='viewport', content='width=device-width, initial-scale=1')
    link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css')
    script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js')
    script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    div(class='container-fluid')
      div(class='row')
        div(class='col-sm-2')
          block sidebar
            ul(class='sidebar-nav')
              li
                a(href='/catalog') Home
              li
                a(href='/catalog/books') All books
              li
                a(href='/catalog/authors') All authors
              li
                a(href='/catalog/genres') All genres
              li
                a(href='/catalog/bookinstances') All book-instances
              li
                hr
              li
                a(href='/catalog/author/create') Create new author
              li
                a(href='/catalog/genre/create') Create new genre
              li
                a(href='/catalog/book/create') Create new book
              li
                a(href='/catalog/bookinstance/create') Create new book instance (copy)
        div(class='col-sm-10')
          block content

The template uses (and includes) JavaScript and CSS from Bootstrap to improve the layout and presentation of the HTML page. Using Bootstrap or another client-side web framework is a quick way to create an attractive page that can scale well on different browser sizes, and it also allows us to deal with the page presentation without having to get into any of the details—we just want to focus on the server-side code here!

The layout should be fairly obvious if you've read our above Template primer. Note the use of block content as a placeholder for where the content for our individual pages will be placed.

The base template also references a local css file (style.css) that provides a little additional styling. Open /public/stylesheets/style.css and replace its content with the following CSS code:

.sidebar-nav {
    margin-top: 20px;
    padding: 0;
    list-style: none;
}

When we get round to running our site, we should see the sidebar appear! In the next sections we will use the above layout to define the individual pages.

Home page

The first page we'll create will be the website home page, which is accessible from either the site ('/') or catalog (catalog/) root. This will display some static text describing the site, along with dynamically calculated "counts" of different record types in the database.

We've already created a route for the home page. In order to complete the page we need to update our controller function to fetch "counts" of records from the database, and create a view (template) that we can use to render the page.

Route

We created our index page routes in a previous tutorial. As a reminder, all the route functions are defined in /routes/catalog.js:

/* GET catalog home page. */
router.get('/', book_controller.index);  //This actually maps to /catalog/ because we import the route with a /catalog prefix

Where the callback function parameter (book_controller.index) is defined in /controllers/bookController.js:

exports.index = function(req, res, next) {
    res.send('NOT IMPLEMENTED: Site Home Page');
}

It is this controller function that we extend to get information from our models and then render it using a template (view).

Controller

The index controller function needs to fetch information about how many Book, BookInstance, available BookInstance, Author, and Genre records we have in the database, render this data in a template to create an HTML page, and then return it in an HTTP response.

Note: We use the count() method to get the number of instances of each model. This is called on a model with an optional set of conditions to match against in the first argument and a callback in the second argument (as discussed in Using a Database (with Mongoose), and you can also return a Query and then execute it with a callback later. The callback will be returned when the database returns the count, with an error value (or null) as the first parameter and the count of records (or null if there was an error) as the second parameter.

SomeModel.count({ a_model_field: 'match_value' }, function (err, count) {
 // ... do something if there is an err
 // ... do something with the count if there was no error
 });

Open /controllers/bookController.js. Near the top of the file you should see the exported index() function.

var Book = require('../models/book')
exports.index = function(req, res, next) {
 res.send('NOT IMPLEMENTED: Site Home Page');
}

Replace all the code above with the following code fragment. The first thing this does is import (require()) all the models (highlighted in bold). We need to do this because we'll be using them to get our counts of records. It then imports the async module.

var Book = require('../models/book');
var Author = require('../models/author');
var Genre = require('../models/genre');
var BookInstance = require('../models/bookinstance');
var async = require('async');
exports.index = function(req, res) {
    async.parallel({
        book_count: function(callback) {
            Book.count(callback);
        },
        book_instance_count: function(callback) {
            BookInstance.count(callback);
        },
        book_instance_available_count: function(callback) {
            BookInstance.count({status:'Available'}, callback);
        },
        author_count: function(callback) {
            Author.count(callback);
        },
        genre_count: function(callback) {
            Genre.count(callback);
        },
    }, function(err, results) {
        res.render('index', { title: 'Local Library Home', error: err, data: results });
    });
};

The async.parallel() method is passed an object with functions for getting the counts for each of our models. These functions are all started at the same time. When all them have completed the final callback is invoked with the counts in the results parameter (or an error).

On success the callback function calls res.render(), specifying a view (template) named 'index' and an object containing the data that is to be inserted into it (this includes the results object that contains our model counts). The data is supplied as key-value pairs, and can be accessed in the template using the key.

Note: The callback function from async.parallel() above is a little unusual in that we render the page whether or not there was an error (normally you might use a separate execution path for handling the display of errors).

View

Open /views/index.pug and replace its content with the text below.

extends layout
block content
  h1= title
  p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network.
  h1 Dynamic content
  if error
    p Error getting dynamic content.
  else
    p The library has the following record counts:
    ul
      li #[strong Books:] !{data.book_count}
      li #[strong Copies:] !{data.book_instance_count}
      li #[strong Copies available:] !{data.book_instance_available_count} 
      li #[strong Authors:] !{data.author_count}
      li #[strong Genres:] !{data.genre_count}

The view is straightforward. We extend the layout.pug base template, overriding the block named 'content'. The first h1 heading will be the escaped text for the title variable that was passed into the render() function—note the use of the 'h1=' so that the following text is treated as a JavaScript expression. We then include a paragraph introducing the LocalLibrary.

Under the Dynamic content heading we check whether the error variable passed in from the render() function has been defined. If so, we note the error. If not, we get and list the number of copies of each model from the data variable.

Note: We didn't escape the count values (i.e. we used the !{} syntax) because the count values are calculated. If the information was supplied by end-users then we'd escape the variable for display.

What does it look like?

At this point we should have created everything needed to display the index page. Run the application and open your browser to http://localhost:3000/. If everything is set up correctly, your site should look something like the following screenshot.

Home page - Express Local Library site

Note: You won't be able to use the sidebar links yet because the urls, views, and templates for those pages haven't been defined. If you try you'll get errors like "NOT IMPLEMENTED: Book list" for example, depending on the link you click on.  These string literals (which will be replaced with proper data) were specified in the different controllers that live inside your "controllers" file.

Book list page

Next we'll implement our book list page. This page needs to display a list of all books in the database along with their author, with each book title being a hyperlink to its associated book detail page.

Controller

The book list controller function needs to get a list of all Book objects in the database, and then pass these to the template for rendering.

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

// Display list of all Books
exports.book_list = function(req, res, next) {
  Book.find({}, 'title author ')
    .populate('author')
    .exec(function (err, list_books) {
      if (err) { return next(err); }
      //Successful, so render
      res.render('book_list', { title: 'Book List', book_list: list_books });
    });
    
};

The method uses the model's find() function to return all Book objects, selecting to return the only the title and author as we don't need the other fields (it will also return the _id and virtual fields). Here we also call populate() on Book, specifying the author field—this will replace the stored book author id with the full author details.

On success, the callback passed to the query renders the book_list(.pug) template, passing the title and book_list (list of books with authors) as variables.

View

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

extends layout
block content
  h1= title
  
  ul
  each book in book_list
    li 
      a(href=book.url) #{book.title} 
      | (#{book.author.name})
  else
    li There are no books.

The view extends the layout.pug base template and overrides the block named 'content'. It displays the title we passed in from the controller (via the render() method) and then iterates through the book_list variable using the each-in-else syntax. A list item is created for each book displaying the book title as a link to the book's detail page followed by the author name. If there are no books in the book_list then the else clause is executed, and displays the text 'There are no books.'

Note: We use book.url to provide the link to the detail record for each book (we've implemented this route, but not the page yet). This is a virtual property of the Book model which uses the model instance's _id field to produce a unique URL path.

Of interest here is that each book is defined as two lines, using the pipe for the second line (highlighted above). This approach is needed because if the author name were on the previous line then it would be part of the hyperlink.

What does it look like?

Run the application (see Testing the routes for the relevant commands) and open your browser to http://localhost:3000/. Then select the All books link. If everything is set up correctly, your site should look something like the following screenshot.

Book List Page - Express Local Library site

BookInstance list page

Next we'll implement our list of all book copies (BookInstance) in the library. This page needs to include the title of the Book associated with each BookInstance (linked to its detail page) along with other information in the BookInstance model, including the status, imprint, and unique id of each copy. The unique id text should be linked to the BookInstance detail page.

Controller

The BookInstance list controller function needs to get a list of all book instances, populate the associated book information, and then pass the list to the template for rendering.

Open /controllers/bookinstanceController.js. Find the exported bookinstance_list() controller method and replace it with the following code (the changed code is shown in bold).

// Display list of all BookInstances
exports.bookinstance_list = function(req, res, next) {
  BookInstance.find()
    .populate('book')
    .exec(function (err, list_bookinstances) {
      if (err) { return next(err); }
      //Successful, so render
      res.render('bookinstance_list', { title: 'Book Instance List', bookinstance_list: list_bookinstances });
    });
    
};

The method uses the model's find() function to return all BookInstance objects. It then daisy-chains a call to populate() with the book field—this will replace the book id stored for each BookInstance with a full Book document.

On success, the callback passed to the query renders the bookinstance_list(.pug) template, passing the title and bookinstance_list as variables.

View

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

extends layout
block content
  h1= title
  ul
  each val in bookinstance_list
    li 
      a(href=val.url) #{val.book.title} : #{val.imprint} - 
      if val.status=='Available'
        span.text-success #{val.status}
      else if val.status=='Maintenance'
        span.text-danger #{val.status}
      else
        span.text-warning #{val.status} 
      if val.status!='Available'
        span  (Due: #{val.due_back} )
  else
    li There are no book copies in this library.

This view is much the same as all the others. It extends the layout, replacing the content block, displays the title passed in from the controller, and iterates through all the book copies in bookinstance_list. For each copy we display its status (colour coded) and if the book is not available, its expected return date. One new feature is introduced—we can use dot notation after a tag to assign a class. So span.text-success will be compiled to <span class="text-success"> (and might also be written in Pug as span(class="text-success").

What does it look like?

Run the application, open your browser to http://localhost:3000/, then select the All book-instances link. If everything is set up correctly, your site should look something like the following screenshot.

BookInstance List Page - Express Local Library site

Date formatting using moment

The default rendering of dates from our models is very ugly: Tue Dec 06 2016 15:49:58 GMT+1100 (AUS Eastern Daylight Time). In this section we'll show how you can update the BookInstance List page from the previous section to present the due_date field in a more friendly format: December 6th, 2016. 

The approach we will use is to create a virtual property in our BookInstance model that returns the formatted date. We'll do the actual formatting using moment, a lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates.

Note: It is possible to use moment to format the strings directly in our Pug templates, or we could format the string in a number of other places. Using a virtual property allows us to get the formatted date in exactly the same way as we get the due_date currently. 

Install moment

Enter the following command in the root of the project:

npm install moment --save

Create the virtual property

  1. Open ./models/bookinstance.js.
  2. At the top of the page, import moment.
    var moment = require('moment');
  3. Add the virtual property due_back_formatted just after the url property.
    BookInstanceSchema
    .virtual('due_back_formatted')
    .get(function () {
      return moment(this.due_back).format('MMMM Do, YYYY');
    });

Note: The format method can display a date using almost any pattern. The syntax for representing different date components can be found in the moment documentation.

Update the view

Open /views/bookinstance_list.pug and replace due_back with due_back_formatted.

      if val.status!='Available'
        //span  (Due: #{val.due_back} )
        span  (Due: #{val.due_back_formatted} )       

That's it. If you go to All book-instances in the sidebar, you should now see all the due dates are far more attractive!

Author list page

The author list page needs to display a list of all authors in the database, with each author name linked to its associated author detail page. The date of birth and date of death should be listed after the name on the same line.

Controller

The author list controller function needs to get a list of all Author instances, and then pass these to the template for rendering.

Open /controllers/authorController.js. Find the exported author_list() controller method near the top of the file and replace it with the following code (the changed code is shown in bold).

// Display list of all Authors
exports.author_list = function(req, res, next) {
  Author.find()
    .sort([['family_name', 'ascending']])
    .exec(function (err, list_authors) {
      if (err) { return next(err); }
      //Successful, so render
      res.render('author_list', { title: 'Author List', author_list: list_authors });
    });
};

The method uses the model's find(), sort() and exec() functions to return all Author objects sorted by family_name in alphabetic order. The callback passed to the exec() method is called with any errors (or null) as the first parameter, or a list of all authors on success. If there is an error it calls the next middleware function with the error value, and if not it renders the author_list(.pug) template, passing the page title and the list of authors (author_list).

View

Create /views/author_list.pug and replace its content with the text below.

extends layout
block content
  h1= title
  
  ul
  each author in author_list
    li 
      a(href=author.url) #{author.name} 
      | (#{author.date_of_birth} - #{author.date_of_death})
  else
    li There are no authors.

The view follows exactly the same pattern as our other templates.

What does it look like?

Run the application and open your browser to http://localhost:3000/. Then select the All authors link. If everything is set up correctly, the page should look something like the following screenshot.

Author List Page - Express Local Library site

Note: The appearance of the author lifespan dates is ugly! You can improve this using the same approach as we used for the BookInstance list (adding the virtual property for the lifespan to the Author model). This time, however, there are missing dates, and references to nonexistent properties are ignored unless strict mode is in effect. moment() returns the current time, and you don't want missing dates to be formatted as if they were today. One way to deal with this is to define the body of the function that returns a formatted date so it returns a blank string unless the date actually exists. For example:

return this.date_of_birth ? moment(this.date_of_birth).format('YYYY-MM-DD') : '';

Genre list page—challenge!

In this section you should implement your own genre list page. The page should display a list of all genres in the database, with each genre linked to its associated detail page. A screenshot of the expected result is shown below.

Genre List - Express Local Library site

The genre list controller function needs to get a list of all Genre instances, and then pass these to the template for rendering.

  1. You will need to edit genre_list() in /controllers/genreController.js
  2. The implementation is almost exactly the same as the author_list() controller.
    • Sort the results by name, in ascending order.
  3. The template to be rendered should be named genre_list.pug.
  4. The template to be rendered should be passed the variables title ('Genre List') and genre_list (the list of genres returned from your Genre.find() callback.
  5. The view should match the screenshot/requirements above (this should have a very similar structure/format to the Author list view, except for the fact that genres do not have dates).

Genre detail page

The genre detail page needs to display the information for particular genre instance, identified using its (automatically generated) _id field value. The page should display the genre name, and a list of all books in the genre (each linked to the book's detail page).

Controller

Open /controllers/genreController.js and import the async and Book modules at the top of the file.

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

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

// Display detail page for a specific Genre
exports.genre_detail = function(req, res, next) {
  async.parallel({
    genre: function(callback) {
      Genre.findById(req.params.id)
        .exec(callback);
    },
    genre_books: function(callback) {
      Book.find({ 'genre': req.params.id })
      .exec(callback);
    },
  }, function(err, results) {
    if (err) { return next(err); }
    //Successful, so render
    res.render('genre_detail', { title: 'Genre Detail', genre: results.genre, genre_books: results.genre_books } );
  });
};

The method uses async.parallel() to query the genre name and its associated books in parallel, with the callback rendering the page when (if) both requests complete successfully.

The ID of the required genre record is encoded at the end of the URL and extracted automatically based on the route definition (/genre/:id). The ID is accessed within the controller via the request parameters: req.params.id. It is used in Genre.findById() to get the current genre. It is also used to get all Book objects that have the genre ID in their genre field: Book.find({ 'genre': req.params.id })

The rendered view is genre_detail and it is passed variables for the title, genre and the list of books in this genre (genre_books).

View

Create /views/genre_detail.pug and fill it with the text below:

extends layout
block content
  h1 Genre: #{genre.name}
  
  div(style='margin-left:20px;margin-top:20px')
    h4 Books
    
    dl
    each book in genre_books
      dt 
        a(href=book.url) #{book.title}
      dd #{book.summary}
    else
      p This genre has no books

The view is very similar to all our other templates. The main difference is that we don't use the title passed in for the first heading (though it is it is used in the underlying layout.pug template to set the page title).

What does it look like?

Run the application and open your browser to http://localhost:3000/. Select the All genres link, then select one of the genres (e.g. "Fantasy"). If everything is set up correctly, your page should look something like the following screenshot.

 

Genre Detail Page - Express Local Library site

You might get an error similar to this:
Cast to ObjectId failed for value " 59347139895ea23f9430ecbb" at path "_id" for model "Genre"

This is a mongoose error coming from the req.params.id. To solve this problem, first you need to require mongoose on the genreController.js page like this:

 var mongoose = require('mongoose');

Then use mongoose.Types.ObjectId() on the req.params.id and pass it to a new variable id as shown below:

exports.genre_detail = function(req, res, next) {
    var id = mongoose.Types.ObjectId(req.params.id.trim());
//using trim() on req.params.id helps to remove any spacing before or after the id string
  async.parallel({
    genre: function(callback) {
      Genre.findById(id)
        .exec(callback);
    },
    genre_books: function(callback) {
      Book.find({ 'genre': id })
      .exec(callback);
    },
  }, function(err, results) {
    if (err) { return next(err); }
    //Successful, so render
    res.render('genre_details', { title: 'Genre Detail', genre: results.genre, genre_books: results.genre_books } );
  });
};

 

Book detail page

The Book detail page needs to display the information for a specific Book, identified using its (automatically generated) _id field value, along with information about each associated copy in the libary (BookInstance). Wherever we display an author, genre, or book instance, these should be linked to the associated detail page for that item.

Controller

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

// Display detail page for a specific book
exports.book_detail = function(req, res, next) {
  async.parallel({
    book: function(callback) {     
        
      Book.findById(req.params.id)
        .populate('author')
        .populate('genre')
        .exec(callback);
    },
    book_instance: function(callback) {
      BookInstance.find({ 'book': req.params.id })
        //.populate('book')
        .exec(callback);
    },
  }, function(err, results) {
    if (err) { return next(err); }
    //Successful, so render
    res.render('book_detail', { title: 'Title', book: results.book, book_instances: results.book_instance } );
  });
    
};

Note: We don't need to require async and BookInstance, as we already imported those modules when we implemented the home page controller.

The method uses async.parallel() to find the Book and its associated copies (BookInstances) in parallel. The approach is exactly the same as described for the Genre detail page above.

View

Create /views/book_detail.pug and add the text below.

extends layout
block content
  h1 #{title}: #{book.title}
  
  p #[strong Author:] 
    a(href=book.author.url) #{book.author.name}
  p #[strong Summary:] #{book.summary}
  p #[strong ISBN:] #{book.isbn}
  p #[strong Genre:]&nbsp;
    each val in book.genre
      a(href=val.url) #{val.name}
      |, 
  
  div(style='margin-left:20px;margin-top:20px')
    h4 Copies
    
    each val in book_instances
      hr
      if val.status=='Available'
        p.text-success #{val.status}
      else if val.status=='Maintenance'
        p.text-danger #{val.status}
      else
        p.text-warning #{val.status} 
      p #[strong Imprint:] #{val.imprint}
      if val.status!='Available'
        p #[strong Due back:] #{val.due_back}
      p #[strong Id:]&nbsp;
        a(href=val.url) #{val._id}
 
    else
      p There are no copies of this book in the library.

Almost everything in this template has been demonstrated in previous sections.

Note: The list of genres associated with the book is implemented in the template as below. This adds a comma after every genre associated with the book, which means that there will also be a comma after the last item.

  p #[strong Genre:]
    each val in book.genre
      a(href=val.url) #{val.name}
      |, 

There is no default way to get information about the last iteration in Pug, so to remove this comma you would have to construct this string in your JavaScript and pass it to your template (perhaps as a virtual property, associated with the current book).

What does it look like?

Run the application and open your browser to http://localhost:3000/. Select the All books link, then select one of the books. If everything is set up correctly, your page should look something like the following screenshot.

Book Detail Page - Express Local Library site

Author detail page

The author detail page needs to display the information about the specified Author, identified using their (automatically generated) _id field value, along with a list of all the Book objects associated with that Author.

Controller

Open /controllers/authorController.js.

Add the following lines to the top of the file to import the async and Book modules (these are needed for our author detail page).

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

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

// Display detail page for a specific Author
exports.author_detail = 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 },'title summary')
        .exec(callback);
    },
  }, function(err, results) {
    if (err) { return next(err); }
    //Successful, so render
    res.render('author_detail', { title: 'Author Detail', author: results.author, author_books: results.authors_books });
  });
    
};

The method uses async.parallel() to query the Author and their associated Book instances in parallel, with the callback rendering the page when (if) both requests complete successfully. The approach is exactly the same as described for the Genre detail page above.

View

Create /views/author_detail.pug and copy in the following text.

extends layout
block content
  h1 Author: #{author.name}
  p #{author.date_of_birth} - #{author.date_of_death}
  
  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 This author has no books.

Everything in this template has been demonstrated in previous sections.

What does it look like?

Run the application and open your browser to http://localhost:3000/. Select the All Authors link, then select one of the authors. If everything is set up correctly, your site should look something like the following screenshot.

Author Detail Page - Express Local Library site

Note: The appearance of the author lifespan dates is ugly! We'll address that in the final challenge in this artice.

BookInstance detail page

The BookInstance detail page needs to display the information for each BookInstance, identified using its (automatically generated) _id field value. This will include the Book name (as a link to the Book detail page) along with other information in the record.

Controller

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

// Display detail page for a specific BookInstance
exports.bookinstance_detail = function(req, res, next) {
  BookInstance.findById(req.params.id)
    .populate('book')
    .exec(function (err, bookinstance) {
      if (err) { return next(err); }
      //Successful, so render
      res.render('bookinstance_detail', { title: 'Book:', bookinstance: bookinstance });
    });
    
};

The method calls BookInstance.findById() with the ID of a specific book instance extracted from the URL (using the route), and accessed within the controller via the request parameters: req.params.id). It then calls populate() to get the details of the associated Book.

View

Create /views/bookinstance_detail.pug and copy in the content below.

extends layout
block content
  h1 ID: #{bookinstance._id}
  
  p #[strong Title:] 
    a(href=bookinstance.book.url) #{bookinstance.book.title}
  p #[strong Imprint:] #{bookinstance.imprint}
  p #[strong Status:] 
    if bookinstance.status=='Available'
      span.text-success #{bookinstance.status}
    else if bookinstance.status=='Maintenance'
      span.text-danger #{bookinstance.status}
    else
      span.text-warning #{bookinstance.status} 
      
  if bookinstance.status!='Available'
    p #[strong Due back:] #{bookinstance.due_back}

Everything in this template has been demonstrated in previous sections.

What does it look like?

Run the application and open your browser to http://localhost:3000/. Select the All book-instances link, then select one of the items. If everything is set up correctly, your site should look something like the following screenshot.

BookInstance Detail Page - Express Local Library site

Challenge

Currently most dates displayed on the site use the default JavaScript format (e.g. Tue Dec 06 2016 15:49:58 GMT+1100 (AUS Eastern Daylight Time). The challenge for this article is to improve the appearance of the date display for Author lifespan information (date of death/birth) and for BookInstance detail pages to use the format: December 6th, 2016.

Note: You can use the same approach as we used for the Book Instance List (adding the virtual property for the lifespan to the Author model and use moment to format the date strings).

The requirements to meet this challenge:

  1. Replace the variable due_back with due_back_formatted in the BookInstance detail page.
  2. Update the Author module to add a lifespan virtual property. The lifespan should look like: date_of_birth - date_of_death, where both values have the same date format as BookInstance.due_back_formatted.
  3. Use Author.lifespan in all views where you currently explicitly use date_of_birth and date_of_death.

Summary

We've now created all the "read-only" pages for our site: a home page that displays counts of instances of each of our models, and list and detail pages for our books, book instances, authors, and genres. Along the way we've gained a lot of fundamental knowledge about controllers, managing flow control when using asynchronous operations, creating views using Pug, querying the database using our models, how to pass information to a template from your view, and how to create and extend templates. Those who completed the challenge will also have learned a little about date handling using moment.

In our next article we'll build on our knowledge, creating HTML forms and form handling code to start modifying the data stored by the site.

See also

文档标签和贡献者