Objectives

Rework the donation-service class to communicate with the donation-web application. This will involve incorporating http components into the app and refactoring DonationService to use them.

Lab Aurelia 4 Exercise Solutions

Exercises 1: Stats Viewmodel

Run the app and look at the stats view. It seems to be perpetually set to 0. Why is this? Can you find a way of it displaying the correct view?

Solution

This is a revised implementation of stats.js, which handles the attached event. This is triggered when the component becomes active. We retrieve the current value from the donation-serivce at this this point.

import {inject} from 'aurelia-framework';
import {TotalUpdate} from '../../services/messages';
import {EventAggregator} from 'aurelia-event-aggregator';
import DonationService from '../../services/donation-service';

@inject(EventAggregator, DonationService)
export class Stats {

  total = 0;

  constructor(ea, ds) {
    this.ds = ds;
    ea.subscribe(TotalUpdate, msg => {
      this.total = msg.total;
    });
  }

  attached() {
    this.total = this.ds.total;
  }
}

In particular, the attached event/method.

Exercise 2: Composite View/Viewmodel.

Build a new view/view model - to appear on the menu as 'dashboard' - which combines all existing views/viewmodels

Solution

src/viewmodels/dashboard/dashboard.html

<template>
  <section class="ui grid segment">
    <div class="four wide column">
      <compose view-model="../donate/donate"></compose>
    </div>
    <div class="four wide column">
      <compose class="four wide column" view-model="../report/report"></compose>
    </div>
    <div class="four wide column">
      <compose class="four wide column" view-model="../candidates/candidates"></compose>
    </div>
    <div class="four wide  column">
      <compose class="ui column" view-model="../stats/stats"></compose>
    </div>
  </section>
</template>

src/viewmodels/dashboard/dashboard.js

export class Dashboard {
}

src/home.js

  ...
    { route: 'dashboard', name: 'dashboard', moduleId: 'viewmodels/dashboard/dashboard', nav: true, title: 'Dashboard' },
  ...

Configuration - donation-client and donation-web

We need some preparatory steps before trying to contacting the donation-web api from donation-client.

donation-client

Aurelia has some convenient http components:

This is not included by default - and will need to be installed. Installing additional components is a two step process:

Step 1: Install the component via npm

npm install aurelia-http-client -save

This will install the module into our local node-modules folder.

Step 2: Incorporate into Aurelia client build

This must be manual inserted into the following configuration file:

aurelia_project/aurelia.json

     "dependencies": [
          ...
          "aurelia-history-browser",
          "aurelia-http-client",
          "aurelia-loader",
          ...

When this is complete, make sure to rebuild the app:

au run --watch

donation-web

Access to the donation-web api may be restricted if we are attempting access from another domain. This is the Cross Origin Sharing issue, discussed here:

and

If we wish our app to be accessible from an SPA, then we may need to explicitly enable it. This package here can make it easier:

First install it:

npm install hapi-cors-headers

Then in index.html, import it:

index.js

const corsHeaders = require('hapi-cors-headers');


Finally, we can enable the facility, with the default options:

...
  server.ext('onPreResponse', corsHeaders);
  server.route(require('./routes'));
  server.route(require('./routesapi'));
...

async-http-client

This is a new class, which will encapsulate access to the aurelia-http-client we installed in the last step:

src/services/async-http-client.js

import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-http-client';
import Fixtures from './fixtures';

@inject(HttpClient, Fixtures)
export default class AsyncHttpClient {

  constructor(httpClient, fixtures) {
    this.http = httpClient;
    this.http.configure(http => {
      http.withBaseUrl(fixtures.baseUrl);
    });
  }

  get(url) {
    return this.http.get(url);
  }

  post(url, obj) {
    return this.http.post(url, obj);
  }

  delete(url) {
    return this.http.delete(url);
  }
}

Notice that this loads the fixtures - looking for a baseUrl field:

src/services-fixtures

export default class Fixtures {

  baseUrl = 'http://localhost:4000';
  methods = ['Cash', 'PayPal'];
}

In the above, remove all the donations, users, candidates - as we not longed need this test data. Leave the fixture looking exactly as above.

donation-service - Login

We can now try first contact from the donation-client to donation-web. Make sure donation-web is running.

src/services/donation-service.js

First modify the constructor to load the async-client we have just introduced + remove the loading of data from the fixtures:

...
import AsyncHttpClient from './async-http-client';

@inject(Fixtures, EventAggregator, AsyncHttpClient)
export default class DonationService {

  donations = [];
  methods = [];
  candidates = [];
  users = [];
  total = 0;

  constructor(data, ea, ac) {
    this.methods = data.methods;
    this.ea = ea;
    this.ac = ac;
    this.getCandidates();
    this.getUsers();
  }

In the above constructor - note that we are calling two new methods. These will retrieve the users and candidates list from donation-web:

  getCandidates() {
    this.ac.get('/api/candidates').then(res => {
      this.candidates = res.content;
    });
  }

  getUsers() {
    this.ac.get('/api/users').then(res => {
      this.users = res.content;
    });
  }

Now we can rewrite login to authenticate using the retrieved users list.

  login(email, password) {
    const status = {
      success: false,
      message: 'Login Attempt Failed'
    };
    for (let user of this.users) {
      if (user.email === email && user.password === password) {
        status.success = true;
        status.message = 'logged in';
      }
    }
    this.ea.publish(new LoginStatus(status));
  }

Try all this now - you should be able to log in with a user credentials as stored on the server. Also, you should be retrieving the candidate list from donation-web.

Do not proceed till the next step unless you have confirmed the above.

donation-service - Donate

We can now have a go at creating a donation:

src/donation-service/DonationService.js

  donate(amount, method, candidate) {
    const donation = {
      amount: amount,
      method: method
    };
    this.ac.post('/api/candidates/' + candidate._id + '/donations', donation).then(res => {
      const returnedDonation = res.content;
      this.donations.push(returnedDonation);
      console.log(amount + ' donated to ' + candidate.firstName + ' ' + candidate.lastName + ': ' + method);

      this.total = this.total + parseInt(amount, 10);
      console.log('Total so far ' + this.total);
      this.ea.publish(new TotalUpdate(this.total));
    });
  }

Not a major change - and notice we do not need to change any of out view-models. They are data-bound to our donation-service, and we are now updating this with donation we have retrieved from the server.

Make sure this works before proceeding to the next step. Check the donation-web UI and verify the donations are getting through.

addCandidate & register implementations

Because we have centralised all access to the donation-web in the donation-service class, we include candidate and user creation in here now:

src/donation-service

  addCandidate(firstName, lastName, office) {
    const candidate = {
      firstName: firstName,
      lastName: lastName,
      office: office
    };
    this.ac.post('/api/candidates', candidate).then(res => {
      this.candidates.push(res.content);
    });
  }
  register(firstName, lastName, email, password) {
    const newUser = {
      firstName: firstName,
      lastName: lastName,
      email: email,
      password: password
    };
    this.ac.post('/api/users', newUser).then(res => {
      this.getUsers();
    });
  }

Again, hopefully our view-models require no changes.

Register new users & candidates now - and also verify with the donation-web that they are in fact created.

Here is the donation-service class at this stage:

import {inject} from 'aurelia-framework';
import Fixtures from './fixtures';
import {TotalUpdate, LoginStatus} from './messages';
import {EventAggregator} from 'aurelia-event-aggregator';
import AsyncHttpClient from './async-http-client';

@inject(Fixtures, EventAggregator, AsyncHttpClient)
export default class DonationService {

  donations = [];
  methods = [];
  candidates = [];
  users = [];
  total = 0;

  constructor(data, ea, ac) {
    this.methods = data.methods;
    this.ea = ea;
    this.ac = ac;
    this.getCandidates();
    this.getUsers();
  }

  getCandidates() {
    this.ac.get('/api/candidates').then(res => {
      this.candidates = res.content;
    });
  }

  getUsers() {
    this.ac.get('/api/users').then(res => {
      this.users = res.content;
    });
  }

  donate(amount, method, candidate) {
    const donation = {
      amount: amount,
      method: method
    };
    this.ac.post('/api/candidates/' + candidate._id + '/donations', donation).then(res => {
      const returnedDonation = res.content;
      this.donations.push(returnedDonation);
      console.log(amount + ' donated to ' + candidate.firstName + ' ' + candidate.lastName + ': ' + method);

      this.total = this.total + parseInt(amount, 10);
      console.log('Total so far ' + this.total);
      this.ea.publish(new TotalUpdate(this.total));
    });
  }

  addCandidate(firstName, lastName, office) {
    const candidate = {
      firstName: firstName,
      lastName: lastName,
      office: office
    };
    this.ac.post('/api/candidates', candidate).then(res => {
      this.candidates.push(res.content);
    });
  }

  register(firstName, lastName, email, password) {
    const newUser = {
      firstName: firstName,
      lastName: lastName,
      email: email,
      password: password
    };
    this.ac.post('/api/users', newUser).then(res => {
      this.getUsers();
    });
  }

  login(email, password) {
    const status = {
      success: false,
      message: 'Login Attempt Failed'
    };
    for (let user of this.users) {
      if (user.email === email && user.password === password) {
        status.success = true;
        status.message = 'logged in';
      }
    }
    this.ea.publish(new LoginStatus(status));
  }

  logout() {
    const status = {
      success: false,
      message: ''
    };
    this.ea.publish(new LoginStatus(new LoginStatus(status)));
  }
}

Exercises

Archive of the lab so far:

Exercise 1: Retrieve prior donations

When you first log in, modify app such that existing donations are retrieved from donation-web. Currently we only show donations made my the current user.

Exercise 2: Show Candidate names in report

Notice that, when you make a donation - the actual candidate's name does not appear. Why is this? In chrome, debug into the app to explore why. See if you can fix this.

There are two approaches:

  • You already know the candidate name in the donation-client, so just insert it into the donation.
  • Change donation-web to populate the donation's candidate field when you create a donation.