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.
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?
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.
Build a new view/view model - to appear on the menu as 'dashboard' - which combines all existing views/viewmodels
<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>
export class Dashboard {
}
...
{ route: 'dashboard', name: 'dashboard', moduleId: 'viewmodels/dashboard/dashboard', nav: true, title: 'Dashboard' },
...
We need some preparatory steps before trying to contacting the donation-web api from 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:
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 {
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:
export default class Fixtures {
baseUrl = 'http://localhost:4000';
methods = ['Cash', 'PayPal'];
}
In the above, remove all the donations, users, candidates - as we not longer 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 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.
We can now have a go at creating a donation:
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.
Because we have centralised all access to the donation-web in the donation-service class, we include candidate and user creation in here now:
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)));
}
}
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: