Objectives

Extend the application to employ layouts, templates, and partials. Include simple interaction features to track users and donations.

Exercise Solutions

Incorporate all of the assets from Lab-5.1 exercises step into the project:

(Note that main.html contains additional elements, so replace your version with the copy in the exercises).

Here are revised sources to implement the new views:

routes.js

const Donations = require('./app/controllers/donations');
const Assets = require('./app/controllers/assets');

module.exports = [

  { method: 'GET', path: '/', config: Donations.home },
  { method: 'GET', path: '/signup', config: Donations.signup },
  { method: 'GET', path: '/login', config: Donations.login },

  {
    method: 'GET',
    path: '/{param*}',
    config: { auth: false },
    handler: Assets.servePublicDirectory,
  },

];

app/controllers/donations.js

'use strict';

exports.home = {

  handler: (request, reply) => {
    reply.file('./app/views/main.html');
  },

};

exports.signup = {

  handler: (request, reply) => {
    reply.file('./app/views/signup.html');
  },

};

exports.login = {

  handler: (request, reply) => {
    reply.file('./app/views/login.html');
  },

};

Introduce vision HAPI plugin + handlebars engine

HAPI view support enables more sophisticated user interactions than simply rendering static pages.

To engage, you must install this HAPI module:

via this command:

npm install vision -save

We will use the handlebar engine:

This must be installed separately with another npm command:

npm install handlebars -save

If successful, your package.json should look like this:

package.json

{
  "name": "donation-web",
  "version": "1.0.0",
  "description": "an application to host donations for candidates",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "handlebars": "^4.0.5",
    "hapi": "^14.1.0",
    "inert": "^4.0.1",
    "vision": "^4.1.0"
  }
}

Initialising this plugin requires registration as with inert, but also an additional step to configure thee plugin:

index.js

'use strict';

const Hapi = require('hapi');

var server = new Hapi.Server();
server.connection({ port: process.env.PORT || 4000 });

server.register([require('inert'), require('vision')], err => {

  if (err) {
    throw err;
  }

  server.views({
    engines: {
      hbs: require('handlebars'),
    },
    relativeTo: __dirname,
    path: './app/views',
    isCached: false,
  });

  server.route(require('./routes'));

  server.start((err) => {
    if (err) {
      throw err;
    }

    console.log('Server listening at:', server.info.uri);
  });

});

First Handlebar Expressions

Rename each of the view from .html to .hbs:

Currently the <title> element of each view is hard coded. Replace this element in each view with a handlebars style parameter:

main.hbs

    <title>{{title}}</title>

signup.hbs

    <title>{{title}}</title>

login.hbs

    <title>{{title}}</title>

We can now refactor the controller to pass the actual title to the view when is is being rendered:

app/controllers/donations.js

'use strict';

exports.home = {

  handler: (request, reply) => {
    reply.view('main', { title: 'Welcome to Donations' });
  },

};

exports.signup = {

  handler: (request, reply) => {
    reply.view('signup', { title: 'Sign up for Donations' });
  },

};

exports.login = {

  handler: (request, reply) => {
    reply.view('login', { title: 'Login to Donations' });
  },

};

Test this out now. There should be no noticeable change.

Vision Layout + Handlebar Partials

A key tenet of view structure is DRY (dont repeat yourself):

Currently our views are very verbose and repetitive. We can start to address this via handlebars layouts and partials.

Layouts are part of the vision implementation:

.. and partials are implemented by the handlebars engine:

First, we need to configure the Vision plugin to use layouts and partials. Extend its initalisation as follows:

index.js

  server.views({
    engines: {
      hbs: require('handlebars'),
    },
    relativeTo: __dirname,
    path: './app/views',
    layoutPath: './app/views/layout',
    partialsPath: './app/views/partials',
    layout: true,
    isCached: false,
  });

Layouts

We can now set up a layout to be used by most of our views. Create the layout in a layouts folder in views:

app/views/layout/layout.hbs

<!DOCTYPE html>
<html>
  <head>
    <title>{{title}}</title>
    <meta charset="UTF-8">
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.js"></script>
    <link rel="stylesheet" media="screen" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.css">
    <link rel="stylesheet" media="screen" href="css/main.css">
    <link rel="shortcut icon" type="image/png" href="images/favicon.png">
  </head>
  <body>
    <section class="ui container">
      {{{content}}}
    </section>
  </body>
</html>

This will be the base view for our other views.

Partials

We can also create a reusable partial that we can include explicitly in other views. Create this in partials folder in views:

app/views/partials/welcomemenu.hbs

<nav class="ui inverted menu">
  <header class="header item"> <a href="/"> Donation </a> </header>
  <div class="right menu">
    <a class="item" href="/signup"> Signup</a>
    <a class="item" href="/login">  Login</a>
  </div>
</nav>

Views

We can now adapt all of the views to use these artifacts:

app/views/main.hbs

{{> welcomemenu }}

<section class="ui stacked segment">
  <div class="ui grid">
    <aside class="six wide column">
      <img src="images/homer.png" class="ui medium image">
    </aside>
    <article class="ten wide column">
      <header class="ui  header"> Help Me Run Springfield</header>
      <p> Donate what you can now - No Bitcoins accepted! </p>
    </article>
  </div>
</section>

app/views/signup.hbs

{{> welcomemenu }}

<section class="ui raised segment">
  <div class="ui grid">
    <div class="ui ten wide column">
      <div class="ui stacked fluid form segment">
        <form action="/register" method="POST">
          <h3 class="ui header">Register</h3>
          <div class="two fields">
            <div class="field">
              <label>First Name</label>
              <input placeholder="First Name" type="text" name="firstName">
            </div>
            <div class="field">
              <label>Last Name</label>
              <input placeholder="Last Name" type="text" name="lastName">
            </div>
          </div>
          <div class="field">
            <label>Email</label>
            <input placeholder="Email" type="text" name="email">
          </div>
          <div class="field">
            <label>Password</label>
            <input type="password" name="password">
          </div>
          <button class="ui blue submit button">Submit</button>
        </form>
      </div>
    </div>
    <aside class="ui five wide column">
      <img src="images/homer3.png" class="ui medium image">
    </aside>
  </div>
</section>

app/views/login.hbs

{{> welcomemenu }}

<section class="ui raised segment">
  <div class="ui grid">
    <aside class="ui six wide column">
      <img src="images/homer2.png" class="ui medium image">
    </aside>
    <div class="ui ten wide column fluid form">
      <div class="ui stacked segment">
        <form action="/login" method="POST">
          <h3 class="ui header">Log-in</h3>
          <div class="field">
            <label>Email</label> <input placeholder="Email" type="text"
                                        name="email">
          </div>
          <div class="field">
            <label>Password</label> <input type="password" name="password">
          </div>
          <button class="ui blue submit button">Login</button>
        </form>
      </div>
    </div>
  </div>
</section>

Report and Home Views + partials

With this infrastructure in place, we can incorporate the remaining views for our application. This will involve additional partials + views structured as follows in the project:

Partials

donate.hbs

<section class="ui raised segment">
  <div class="ui grid ">
    <div class="ui form six wide column">
      <div class="ui stacked segment">
        <form action="/donate" method="POST">
          <div class="ui dropdown" name="amount">
            <input type="hidden" name="amount">
            <div class="text">Select Amount</div>
            <i class="ui dropdown icon"></i>
            <div class="menu">
              <div class="item">50</div>
              <div class="item">100</div>
              <div class="item">1000</div>
            </div>
          </div>
          <div class="grouped inline fields">
            <div class="field">
              <div class="ui radio checkbox">
                <input type="radio" name="method" value="paypal">
                <label>Paypal</label>
              </div>
            </div>
            <div class="field">
              <div class="ui radio checkbox">
                <input type="radio" name="method" value="direct">
                <label>Direct</label>
              </div>
            </div>
          </div>
          <button class="ui blue submit button">Donate</button>
        </form>
      </div>
    </div>
    <aside class="six wide column">
      <img src="images/homer4.jpeg" class="ui medium image">
    </aside>
  </div>
</section>

donationlist.hbs

<section class="ui raised segment">
  <div class="ui grid">
    <aside class="six wide column">
      <img src="images/homer5.jpg" class="ui medium image">
    </aside>
    <article class="eight wide column">
      <table class="ui celled table segment">
        <thead>
          <tr>
            <th>Amount</th>
            <th>Method donated</th>
          </tr>
        </thead>
        <tbody>
          {{#each donations}}
            <tr>
              <td> {{amount}} </td>
              <td> {{method}} </td>
            </tr>
          {{/each}}
        </tbody>
      </table>
    </article>
  </div>
</section>

Views

home.hbs

<nav class="ui inverted menu">
  <header class="header item"><a href="/"> Donation </a></header>
  <div class="right menu">
    <a class="active item" href="/home"> Donate</a>
    <a class="item" href="/report"> Report</a>
    <a class="item" href="/logout"> Logout</a>
  </div>
</nav>

<section class="ui raised segment">

  {{> donate }}

  <div class="ui  divider"></div>

  <div class="ui teal progress" data-percent="${progress}" id="mainprogress">
    <div class="bar"></div>
  </div>

</section>

report.hbs

<nav class="ui inverted menu">
  <header class="header item"> <a href="/"> Donation </a> </header>
  <div class="right menu">
    <a class="item" href="/home"> Donate</a>
    <a class="active item" href="/report">  Report</a>
    <a class="item" href="/logout">  Logout</a>
  </div>
</nav>

{{> donationlist }}

Layout

We also make this adjustment to the layout (to support dropdown controls)

layout.hbs

<!DOCTYPE html>
<html>
  <head>
    <title>{{title}}</title>
    <meta charset="UTF-8">
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.js"></script>
    <link rel="stylesheet" media="screen" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.css">
    <link rel="stylesheet" media="screen" href="css/main.css">
    <link rel="shortcut icon" type="image/png" href="images/favicon.png">
  </head>
  <body>
    <section class="ui container">
      {{{content}}}
    </section>
    <script>
      $(document).ready(function () {
        $('.ui.dropdown').dropdown({ on: 'hover' });
      });
    </script>
  </body>
</html>

Accounts and Donations Controllers + Routes

We will rework the controllers, creating an new Accounts controller to handle user accounts, login etc...:

accounts.js

'use strict';

exports.main = {

  handler: function (request, reply) {
    reply.view('main', { title: 'Welcome to Donations' });
  },

};

exports.signup = {

  handler: function (request, reply) {
    reply.view('signup', { title: 'Sign up for Donations' });
  },

};

exports.login = {

  handler: function (request, reply) {
    reply.view('login', { title: 'Login to Donations' });
  },

};

exports.authenticate = {

  handler: function (request, reply) {
    reply.redirect('/home');
  },

};

exports.logout = {

  handler: function (request, reply) {
    reply.redirect('/');
  },

};

We can then refactor the donations controller to support making and listing donations:

donations.js

'use strict';

exports.home = {

  handler: function (request, reply) {
    reply.view('home', { title: 'Make a Donation' });
  },

};

exports.report = {

  handler: function (request, reply) {
    reply.view('report', { title: 'Donations to Date', });
  },

};

exports.donate = {

  handler: function (request, reply) {
    reply.redirect('/report');
  },

};

This is the revised routes that serves these controllers:

const Accounts = require('./app/controllers/accounts');
const Donations = require('./app/controllers/donations');
const Assets = require('./app/controllers/assets');

module.exports = [

  { method: 'GET', path: '/', config: Accounts.main },
  { method: 'GET', path: '/signup', config: Accounts.signup },
  { method: 'GET', path: '/login', config: Accounts.login },
  { method: 'POST', path: '/login', config: Accounts.authenticate },
  { method: 'GET', path: '/logout', config: Accounts.logout },

  { method: 'GET', path: '/home', config: Donations.home },
  { method: 'GET', path: '/report', config: Donations.report },

  {
    method: 'GET',
    path: '/{param*}',
    config: { auth: false },
    handler: Assets.servePublicDirectory,
  },

];

You should be able to navigate throgh login to the home screen now. However, if you make a donation you will get an error.

Very Simple Donation Persistence

We can try to store some of the donations the user makes in an simple array. In index.js, include the following just before the plugins are registered:

index.js

server.bind({
  donations: [],
});

Introduce this additional route, which is a POST route to accept donations:

routes.js

  { method: 'POST', path: '/donate', config: Donations.donate },

We can now refactor the donations controller to store and retrieve these donations:

app/controllers/donations.js

exports.report = {

  handler: function (request, reply) {
    reply.view('report', {
      title: 'Donations to Date',
      donations: this.donations,
    });
  },

};

exports.donate = {

  handler: function (request, reply) {
    const data = request.payload;
    this.donations.push(data);
    reply.redirect('/report');
  },

};

Rerun the app again - this time, if you proceed through login, you should be able to make donations, and see them on the report view.

Exercises: Track Users and Associate with Donations

Archive of the project so far:

Exercise 1: Register Users

As well as storing the donations in the server bound objects:

server.bind({
  donations: [],
});

Try also storing a list of users - in a similar manner to to the donations:

server.bind({
  users: [],
  donations: [],
});

Using the donations controller as a guide, see if you can populate this array with new users as they are registered. You will need to write a new route for the signup form:

  { method: 'POST', path: '/register', config: Accounts.register },

and a matching handler:

exports.register = {

  handler: function (request, reply) {
    reply.redirect('/home');
  },

};

Exercise 2: Current User

Try also to keep track of the current user:

server.bind({
  currentUser : {},
  users: [],
  donations: [],
});

Adjust your login controller to update this field.

On the report - include an extra column - donor - which should list the name of the donor (the user who is currently logged in),