Redevelop a version of donation-client in Typescript. This version should match the features in the Aurelia Lab 3 application.
If you are not familiar with Typescript - it might be worth a quick review of the language here:
Make sure you have the latest typescript and aureli-cli installed on your workstation:
npm install typescript -g
npm install aurelia-cli -g
Create a new Aurelia project:
au new donstion-client-ts
Make sure to select [2] for typescript:
_ _ ____ _ ___
__ _ _ _ _ __ ___| (_) __ _ / ___| | |_ _|
/ _` | | | | '__/ _ \ | |/ _` | | | | | | |
| (_| | |_| | | | __/ | | (_| | | |___| |___ | |
\__,_|\__,_|_| \___|_|_|\__,_| \____|_____|___|
Would you like to use the default setup or customize your choices?
1. Default ESNext (Default)
A basic web-oriented setup with Babel and RequireJS for modern JavaScript development.
2. Default TypeScript
A basic web-oriented setup with TypeScript and RequireJS for modern JavaScript development.
3. Custom
Select loaders (requirejs/systemjs), bundlers (cli/webpack), transpilers, CSS pre-processors and more.
[Default ESNext]>2
Accept all other defaults.
You should be able to immediately open the project in Webstorm.
When invited to compile Typescript to Javascript - press 'OK'
We start by creating a new services
folder in src
. Create a new models.ts
module:
export interface Candidate {
firstName: string;
lastName: string;
office: string;
_id?: string;
}
export interface Donation {
amount: number;
method: string;
candidate: Candidate;
_id?: string;
}
export interface User {
firstName: string;
lastName: string;
email: string;
password: string;
_id?: string;
}
This mirrors the Donation models. The ?
indicates that the id fields may initially be empty.
Using these models, we can construct a class to hold some inital fixture data:
import { Candidate, Donation, User } from './models';
export default class Fixtures {
baseUrl = 'http://localhost:4000';
methods = ['Cash', 'PayPal'];
candidates: Array<Candidate> = [
{
firstName: 'Lisa',
lastName: 'Simpson',
office: 'President',
},
{
firstName: 'Bart',
lastName: 'Simpson',
office: 'President',
},
];
donations: Array<Donation> = [
{
amount: 23,
method: 'cash',
candidate: this.candidates[0],
},
{
amount: 212,
method: 'paypal',
candidate: this.candidates[1],
},
];
users: Map<string, User> = new Map()
.set('homer@simpson.com', {
firstName: 'Homer',
lastName: 'Simpson',
email: 'homer@simpson.com',
password: 'secret',
})
.set('marge@simpson.com', {
firstName: 'Marge',
lastName: 'Simpson',
email: 'marge@simpson.com',
password: 'secret',
});
}
Next, a definition of the messages to be used in the app:
export class TotalUpdate {
total: number;
constructor(total: number) {
this.total = total;
}
}
export class LoginStatus {
status: boolean;
message: string;
constructor(status: boolean, message:string = '') {
this.status = status;
this.message = message;
}
}
We can now define a DonationService
class:
import { inject } from 'aurelia-framework';
import Fixtures from './fixtures';
import { TotalUpdate, LoginStatus } from './messages';
import { EventAggregator } from 'aurelia-event-aggregator';
import { Candidate, Donation, User } from './models';
@inject(Fixtures, EventAggregator)
export class DonationService {
ea: EventAggregator;
donations: Array<Donation> = [];
methods: Array<string> = [];
candidates: Array<Candidate> = [];
users: Map<string, User>;
total = 0;
constructor(data: Fixtures, ea: EventAggregator) {
this.users = data.users;
this.donations = data.donations;
this.candidates = data.candidates;
this.methods = data.methods;
this.ea = ea;
}
donate(amount: number, method: string, candidate: Candidate) {
const donation = {
amount: amount,
method: method,
candidate: candidate,
};
this.donations.push(donation);
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.candidates.push(candidate);
}
register(firstName, lastName, email, password) {
const newUser = {
firstName: firstName,
lastName: lastName,
email: email,
password: password,
};
this.users.set(email, newUser);
}
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));
}
}
Create a new folder: src/components
, and introduce these two viewmodel/view pairs:
<template>
<form submit.delegate="login($event)" class="ui stacked segment form">
<h3 class="ui header">Log-in</h3>
<div class="field">
<label>Email</label> <input placeholder="Email" value.bind="email"/>
</div>
<div class="field">
<label>Password</label> <input type="password" value.bind="password"/>
</div>
<button class="ui blue submit button">Login</button>
<h3>${prompt}</h3>
</form>
</template>
import { inject } from 'aurelia-framework';
import { DonationService } from '../services/donation-service';
@inject(DonationService)
export class Login {
donationService: DonationService;
email = 'marge@simpson.com';
password = 'secret';
constructor(ds: DonationService) {
this.donationService = ds;
}
login(e) {
console.log(`Trying to log in ${this.email}`);
this.donationService.login(this.email, this.password);
}
}
<template>
<form submit.delegate="register($event)" class="ui stacked segment form">
<h3 class="ui header">Register</h3>
<div class="two fields">
<div class="field">
<label>First Name</label>
<input placeholder="First Name" type="text" value.bind="firstName">
</div>
<div class="field">
<label>Last Name</label>
<input placeholder="Last Name" type="text" value.bind="lastName">
</div>
</div>
<div class="field">
<label>Email</label>
<input placeholder="Email" type="text" value.bind="email">
</div>
<div class="field">
<label>Password</label>
<input type="password" value.bind="password">
</div>
<button class="ui blue submit button">Submit</button>
</form>
</template>
import { inject } from 'aurelia-framework';
import { DonationService } from '../services/donation-service';
@inject(DonationService)
export class Signup {
donationService: DonationService;
firstName = 'Marge';
lastName = 'Simpson';
email = 'marge@simpson.com';
password = 'secret';
constructor(ds) {
this.donationService = ds;
}
register(e) {
this.donationService.register(
this.firstName,
this.lastName,
this.email,
this.password,
);
this.donationService.login(this.email, this.password);
}
}
And then this view:
<template bindable="router">
<nav class="ui inverted menu">
<header class="header item"><a href="/"> Donation </a></header>
<div class="right menu">
<div repeat.for="row of router.navigation">
<a class="${row.isActive ? 'active' : ''} item" href.bind="row.href">${row.title}</a>
</div>
</div>
</nav>
</template>
Include the semantic-ui libraries in index.html
...
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.css" type="text/css">
...
We are now ready for to start the app
view-model and run the application for the first time.
import { inject, Aurelia } from 'aurelia-framework';
import { RouterConfiguration, Router } from 'aurelia-router';
import { EventAggregator } from 'aurelia-event-aggregator';
@inject(Aurelia, EventAggregator)
export class App {
router: Router;
configureRouter(config: RouterConfiguration, router: Router) {
config.map([
{
route: ['', 'login'],
name: 'login',
moduleId: 'components/login',
nav: true,
title: 'Login',
},
{
route: 'signup',
name: 'signup',
moduleId: 'components/signup',
nav: true,
title: 'Signup',
},
]);
this.router = router;
}
}
<template>
<require from="components/nav-bar.html"></require>
<div class="ui container page-host">
<nav-bar router.bind="router"></nav-bar>
<router-view></router-view>
</div>
</template>
Run the app now from the command line:
au run --watch
Verify that the login and signup screens are displayed - and the nav bar works as expected.
Also, keep an eye on the developer console, and make sure there are no error messages appearing there.
Introduce these additional components:
import { inject } from 'aurelia-framework';
import { DonationService } from '../services/donation-service';
import { Candidate } from '../services/models';
@inject(DonationService)
export class Donate {
donationService: DonationService;
amount = 0;
methods: Array<string> = [];
selectedMethod = '';
candidates: Array<Candidate>;
selectedCandidate: Candidate;
constructor(ds: DonationService) {
this.donationService = ds;
this.methods = ds.methods;
this.selectedMethod = this.methods[0];
this.candidates = ds.candidates;
this.selectedCandidate = this.candidates[0];
}
makeDonation() {
this.donationService.donate(
this.amount,
this.selectedMethod,
this.selectedCandidate,
);
}
}
<template>
<form submit.trigger="makeDonation()" class="ui form stacked segment">
<h3 class='ui dividing header'> Make a Donation </h3>
<div class="grouped inline fields">
<div class="field">
<label>Amount</label> <input type="number" value.bind="amount">
</div>
</div>
<h4 class="ui dividing header"> Select Method </h4>
<div class="grouped inline fields">
<div class="field" repeat.for="method of methods">
<div class="ui radio checkbox">
<input type="radio" model.bind="method" checked.bind="selectedMethod">
<label>${method}</label>
</div>
</div>
<label class="ui circular label"> ${selectedMethod} </label>
</div>
<h4 class="ui dividing header"> Select Candidate </h4>
<div class="grouped inline fields">
<div class="field" repeat.for="candidate of candidates">
<div class="ui radio checkbox">
<input type="radio" model.bind="candidate" checked.bind="selectedCandidate">
<label>${candidate.lastName}, ${candidate.firstName}</label>
</div>
</div>
<label class="ui circular label"> ${selectedCandidate.firstName} ${selectedCandidate.lastName}</label>
</div>
<button class="ui blue submit button">Donate</button>
</form>
</template>
import { inject } from 'aurelia-framework';
import { DonationService } from '../services/donation-service';
import { Donation } from '../services/models';
@inject(DonationService)
export class Report {
donationService: DonationService;
donations: Array<Donation>;
constructor(ds) {
this.donationService = ds;
this.donations = this.donationService.donations;
}
}
<template>
<article class="ui stacked segment">
<h3 class='ui dividing header'> Donations to Date </h3>
<table class="ui celled table segment">
<thead>
<tr>
<th>Amount</th>
<th>Method donated</th>
<th>Candidate</th>
</tr>
</thead>
<tbody>
<tr repeat.for="donation of donations">
<td> ${donation.amount}</td>
<td> ${donation.method}</td>
<td> ${donation.candidate.lastName}, ${donation.candidate.firstName}</td>
</tr>
</tbody>
</table>
</article>
</template>
import { DonationService } from '../services/donation-service';
import { inject } from 'aurelia-framework';
@inject(DonationService)
export class Logout {
donationService: DonationService;
constructor(donationService: DonationService) {
this.donationService = donationService;
}
logout() {
console.log('logging out');
this.donationService.logout();
}
}
<template>
<form submit.delegate="logout($event)" class="ui stacked segment form">
<h3 class="ui header">Are you sure you want to log out?</h3>
<button class="ui blue submit button">Logout</button>
</form>
</template>
We now need a new Viewmodel for the logged in users:
import { RouterConfiguration, Router } from 'aurelia-router';
export class Home {
router: Router;
configureRouter(config: RouterConfiguration, router: Router) {
config.map([
{
route: ['', 'home'],
name: 'donate',
moduleId: 'components/donate',
nav: true,
title: 'Donate',
},
{
route: 'report',
name: 'report',
moduleId: 'components/report',
nav: true,
title: 'Report',
},
{
route: 'logout',
name: 'logout',
moduleId: 'components/logout',
nav: true,
title: 'Logout',
},
]);
this.router = router;
}
}
<template>
<require from="components/nav-bar.html"></require>
<div class="ui container page-host">
<nav-bar router.bind="router"></nav-bar>
<router-view></router-view>
</div>
</template>
This is a revised app.ts
to support log in/out events:
import { inject, Aurelia } from 'aurelia-framework';
import { RouterConfiguration, Router } from 'aurelia-router';
import { EventAggregator } from 'aurelia-event-aggregator';
import { LoginStatus } from './services/messages';
@inject(Aurelia, EventAggregator)
export class App {
router: Router;
constructor(au: Aurelia, ea: EventAggregator) {
ea.subscribe(LoginStatus, msg => {
this.router.navigate('/', { replace: true, trigger: false });
this.router.reset();
if (msg.status === true) {
au.setRoot('home');
} else {
au.setRoot('app');
}
});
}
configureRouter(config: RouterConfiguration, router: Router) {
config.map([
{
route: ['', 'login'],
name: 'login',
moduleId: 'components/login',
nav: true,
title: 'Login',
},
{
route: 'signup',
name: 'signup',
moduleId: 'components/signup',
nav: true,
title: 'Signup',
},
]);
this.router = router;
}
}
Run the app now - and verify that login/logout + the basics donation and report features work as expected.
Implement the Stats component (see Aurelia Lab 3)
Implement the Candidates component (see Aurelia Lab 3)
Implement the Dashboard component (see Aurelia Lab 3)