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.

Exercise Solutions

Archive of the lab so far:

First the solutions, a fix to Donation.ts:

src/components/donate.ts

  amount = '0';

  ...
  ...
  makeDonation() {
    this.donationService.donate(
      parseInt(this.amount),
      this.selectedMethod,
      this.selectedCandidate,
    )
  ...

This addresses an issue in binding (it may be a Javascript anomaly), discussed here:

A more robust solution would be to use a Value Converter:

Exercise 1

Implement the Stats component (see Aurelia Lab 3)

src/components/stats.ts

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 {
  donationService: DonationService;
  total = 0;

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

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

src/components/stats.html

<template>

  <section class="ui stacked statistic segment">
    <div class="value">
      ${total}
    </div>
    <div class="label">
      Donated
    </div>
  </section>

</template>

Exercise 2

Implement the Candidates component (see Aurelia Lab 3)

src/components/candidates.ts

import { inject } from 'aurelia-framework';
import { DonationService } from '../services/donation-service';

@inject(DonationService)
export class Candidate {
  donationService: DonationService;
  firstName = '';
  lastName = '';
  office = '';

  constructor(ds: DonationService) {
    this.donationService = ds;
  }

  addCandidate() {
    this.donationService.addCandidate(
      this.firstName,
      this.lastName,
      this.office,
    );
  }
}

src/components/candidates.html

<template>

  <form submit.trigger="addCandidate()" class="ui form stacked segment">
    <h3 class="ui dividing header"> Add a Candidate </h3>
    <div class="field">
      <label>First Name </label> <input value.bind="firstName">
    </div>
    <div class="field">
      <label>Last Name </label> <input value.bind="lastName">
    </div>
    <div class="field">
      <label>Office </label> <input value.bind="office">
    </div>
    <button class="ui blue submit button">Add</button>
  </form>

</template>

Exercise 3

Implement the Dashboard component (see Aurelia Lab 3)

src/components/dashboard.ts

export class Dashboard {
}

src/components/dashboard.html

<template>

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

</template>

Configuration - donation-client and donation-web

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

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

This is the most recent donation-web project:

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 (remember, we are now working in donation-web, not donation-client)

npm install hapi-cors-headers -save

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

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

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

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

import { Candidate, Donation, User } from './models';

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 class DonationService {
  ea: EventAggregator;
  ac: AsyncHttpClient;
  donations: Array<Donation> = [];
  methods: Array<string> = [];
  candidates: Array<Candidate> = [];
  users: Map<string, User> = new Map();
  total = 0;

  constructor(data: Fixtures, ea: EventAggregator, ac: AsyncHttpClient) {
    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 => {
        const users = res.content as Array<User>;
        users.forEach(user => {
          this.users.set(user.email, user);
        });
      });
  }

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: number, method: string, candidate: Candidate) {
    const donation = {
      amount: amount,
      method: method,
    };
    this.ac
      .post('/api/candidates/' + candidate._id + '/donations', donation)
      .then(res => {
        let returnedDonation = res.content as Donation;
        returnedDonation.candidate = candidate;
        this.donations.push(returnedDonation);
        console.log(
          `${amount} donated to ${candidate.firstName} ${
            candidate.lastName
          } : ${method}`,
        );

        this.total = this.total + amount;
        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 centralized 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: string, lastName: string, office: string) {
    const candidate = {
      firstName: firstName,
      lastName: lastName,
      office: office,
    };
    this.ac.post('/api/candidates', candidate).then(res => {
      this.candidates.push(res.content);
    });
  }

  register(
    firstName: string,
    lastName: string,
    email: string,
    password: string,
  ) {
    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 AsyncHttpClient from './async-http-client';
import Fixtures from './fixtures';
import { TotalUpdate, LoginStatus } from './messages';
import { EventAggregator } from 'aurelia-event-aggregator';
import { Candidate, Donation, User } from './models';

@inject(Fixtures, EventAggregator, AsyncHttpClient)
export class DonationService {
  ea: EventAggregator;
  ac: AsyncHttpClient;
  donations: Array<Donation> = [];
  methods: Array<string> = [];
  candidates: Array<Candidate> = [];
  users: Map<string, User> = new Map();
  total = 0;

  constructor(data: Fixtures, ea: EventAggregator, ac: AsyncHttpClient) {
    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 => {
      const users = res.content as Array<User>;
      users.forEach(user => {
        this.users.set(user.email, user);
      });
    });
  }

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

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

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

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

  login(email: string, password: string) {
    const loginStatus = new LoginStatus(false);

    const user = this.users.get(email);
    if (user) {
      if (user.password === password) {
        loginStatus.status = true;
        loginStatus.message = 'logged in';
      } else {
        loginStatus.message = 'Incorrect password';
      }
    } else {
      loginStatus.message = 'Unknown user';
    }
    this.ea.publish(loginStatus);
  }

  logout() {
    this.ea.publish(new LoginStatus(false));
  }
}

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.