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.
Archive of the lab so far:
First the solutions, a fix to Donation.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:
Implement the Stats component (see Aurelia Lab 3)
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;
}
}
<template>
<section class="ui stacked statistic segment">
<div class="value">
${total}
</div>
<div class="label">
Donated
</div>
</section>
</template>
Implement the Candidates component (see Aurelia Lab 3)
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,
);
}
}
<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>
Implement the Dashboard component (see Aurelia Lab 3)
export class Dashboard {
}
<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>
We need some preparatory steps before trying to contacting the donation-web api from donation-client-ts.
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:
npm install aurelia-http-client -save
This will install the module into our local node-modules
folder.
This must be manual inserted into the following configuration file:
"dependencies": [
...
"aurelia-history-browser",
"aurelia-http-client",
"aurelia-loader",
...
When this is complete, make sure to rebuild the app:
au run --watch
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:
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'));
...
This is a new class, which will encapsulate access to the aurelia-http-client we installed in the last step:
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:
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.
We can now try first contact from the donation-client to donation-web. Make sure donation-web is running.
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.
We can now have a go at creating a donation:
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.
Because we have centralized all access to the donation-web in the donation-service class, we include candidate and user creation in here now:
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));
}
}
Archive of the lab so far:
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.
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: