Objectives

Redevelop a version of donation-client in Typescript. This version should match the features in the Aurelia Lab 3 application.

Starter Project

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'

Services

We start by creating a new services folder in src. Create a new models.ts module:

src/services/models.ts

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:

src/services/fixtures.ts

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:

src/services/messages.ts

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:

src/services/donation-service.ts

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));
  }
}

Initial Components

Create a new folder: src/components, and introduce these two viewmodel/view pairs:

src/components/login.html

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

src/components/login.ts

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);
  }
}

src/components/signup.html

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

src/components/signup.ts

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:

src/components/nav-bar.html

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

Index & App

Include the semantic-ui libraries in index.html

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.

src/app.ts

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;
  }
}

src/app.html

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

Donate & Report Components

Introduce these additional components:

src/components/donate.ts

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,
    );
  }
}

src/components/donate.html

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

src/components/report.ts

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;
  }
}

src/components/report.html

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

src/components/logout.ts

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();
  }
}

src/components/logout.html

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

Home View Model

We now need a new Viewmodel for the logged in users:

src/home.ts

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;
  }
}

src/home.html

<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:

src/app.ts

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.

Solution

Exercise 1

Implement the Stats component (see Aurelia Lab 3)

Exercise 2

Implement the Candidates component (see Aurelia Lab 3)

Exercise 3

Implement the Dashboard component (see Aurelia Lab 3)