Continue evolving the donation-client with login/logout feature + a mechanism for selecting revealing the various view/models.
This is a new method in the DonationService
class, which will add a new candidate:
addCandidate(firstName, lastName, office) {
const candidate = {
firstName: firstName,
lastName: lastName,
office: office
};
this.candidates.push(candidate);
}
.. and this is the implementation of the Candidates
viewmodel class to accompany the candidates.html
template:
import {inject} from 'aurelia-framework';
import DonationService from '../services/donation-service';
@inject(DonationService)
export class Candidate {
firstName = '';
lastName = '';
office = '';
constructor(ds) {
this.donationService = ds;
}
addCandidate() {
this.donationService.addCandidate(this.firstName, this.lastName, this.office);
}
}
Now we can include this viewmodel in app.html
:
<template>
<div class="ui container">
<section class="ui three column stackable grid basic segment">
<div class="ui row">
<aside class="column">
<compose view-model="./viewmodels/donate"></compose>
</aside>
<article class="column">
<compose view-model="./viewmodels/report"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/candidates"></compose
</article>
</div>
</section>
</div>
</template>
This should render as shown:
Our existing data binding strategies should ensure that entering a new candidate should nicely propagate to the donate viewmodel.
We would like to display the total amount donated so far. To prepare for this, we can extend donation-service to track and accumulate this as a total
attribute:
export default class DonationService {
...
...
total = 0;
...
...
donate(amount, method, candidate) {
...
...
this.total = this.total + parseInt(amount, 10);
console.log('Total so far ' + this.total);
}
...
...
}
Include the above now and make sure the total are appearing in the (browser) console.
Here is our first attempt at a component to display the total:
<template>
<section class="ui stacked statistic segment">
<div class="value">
${total}
</div>
<div class="label">
Donated
</div>
</section>
</template>
import {inject} from 'aurelia-framework';
import DonationService from '../services/donation-service';
@inject(DonationService)
export class Stats {
total = 0;
constructor(ds) {
this.donationService = ds;
this.total = ds.total;
}
}
We can include it in our main view:
<template>
<div class="ui container">
<section class="ui four column stackable grid basic segment">
<div class="ui row">
<aside class="column">
<compose view-model="./viewmodels/donate"></compose>
</aside>
<article class="column">
<compose view-model="./viewmodels/report"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/candidates"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/stats"></compose>
</article>
</div>
</section>
</div>
</template>
This should render like this:
Try making donations now - you will notice that our stat view is not updating! Can you explain why?
We have been able to hook up the candidate array directly to the donation-service candidate list - and these have stayed in sync. However, the total maintained in the stats viewmodel is not performing in a similiar manner. This is because this is a separate value object (a number) and not a reference to an array.
Fixing this gives us an opportunity to explore events in Aurelia, which are an elegant and simple mechanism for communicating in a clean way between components.
First, in the services folder, introduce a new messages
file:
export class TotalUpdate {
constructor(total) {
this.total = total;
}
}
This defines a message of interest - namely an update to the total donations received.
In DonationService
import this class + the event aggregator we need to dispatch the events:
...
import {TotalUpdate} from './messages';
import {EventAggregator} from 'aurelia-event-aggregator';
...
The constructor in DonationService
needs to initialise store the aggregator:
@inject(Fixtures, EventAggregator)
export default class DonationService {
...
...
constructor(data, ea) {
...
...
this.ea = ea;
}
and finally (for DonationService), we need to generate the event when a total is updated:
donate(amount, method, candidate) {
...
...
this.total = this.total + parseInt(amount, 10);
console.log('Total so far ' + this.total);
this.ea.publish(new TotalUpdate(this.total));
}
This is the revised stats view model:
import {inject} from 'aurelia-framework';
import {TotalUpdate} from '../services/messages';
import {EventAggregator} from 'aurelia-event-aggregator';
@inject(EventAggregator)
export class Stats {
total = 0;
constructor(ea) {
ea.subscribe(TotalUpdate, msg => {
this.total = msg.total;
});
}
}
It has been simplified (removed reference to donation-service), and hooked up to the event aggregator. It listens for TotalUpdate
events, and stores them in total
attribute. This is automatically rendered in the view.
Try this now and verify it works as expected.
We might like to have a user log in before we allow donations etc.. Bring in this template:
<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>
Note that we are not using a viewmodel class with this template. We can bring in this 'viewmodelless' view into app.html
:
<template>
<div class="ui container">
<section class="ui four column stackable grid basic segment">
<div class="ui row">
<section class="ui five wide column">
<compose view="./viewmodels/login.html"></compose>
</section>
</div>
<div class="ui row">
<aside class="column">
<compose view-model="./viewmodels/donate"></compose>
</aside>
<article class="column">
<compose view-model="./viewmodels/report"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/candidates"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/stats"></compose>
</article>
</div>
</section>
</div>
</template>
Look at how we have composed the login view:
<section class="ui five wide column">
<compose view="./viewmodels/login.html"></compose>
</section>
..we have used view=
instead of view-model=
.
Now we can bind this view's login fields directly to app.js
:
export class App {
email = 'marge@simpson.com';
password = 'secret';
loggedIn = false;
login() {
console.log(`Logging in ${this.email}`);
this.loggedIn = true;
}
}
Refreshing the browser:
We should only reveal the donation / report / stats view is the user is logged in. Also, when the user is logged in we should hide the login view.
We already have the boolean value defined in App
:
loggedIn = false;
Using this, we can introduce a clause in the view to reveal the appropriate components:
<template>
<div class="ui container">
<section class="ui four column stackable grid basic segment">
<div show.bind="!loggedIn" class="ui row">
<section class="ui five wide column">
<compose view="./viewmodels/login.html"></compose>
</section>
</div>
<div show.bind="loggedIn" class="ui row">
<aside class="column">
<compose view-model="./viewmodels/donate"></compose>
</aside>
<article class="column">
<compose view-model="./viewmodels/report"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/candidates"></compose>
</article>
<article class="column">
<compose view-model="./viewmodels/stats"></compose>
</article>
</div>
</section>
</div>
</template>
The binding behaviour to achieve this is in here:
<div show.bind="!loggedIn" class="ui row">
...
</div>
<div show.bind="loggedIn" class="ui row">
...
</div>
This effectively toggles the two rows, depending on the value of loggedIn
in app. Once logged in, though, we dont seem to have a way of logging out.
To support login/logout in a more structured manner, modify app.html
to include a menubar:
<template>
<div class="ui container">
<nav class="ui inverted menu">
<header class="header item"><a href="/"> Donation </a></header>
<div class="right menu">
<div show.bind="loggedIn">
<a id="logout" class= "item" click.trigger="logout()"> Logout</a>
</div>
</div>
</nav>
...
...
This menubar displays a logout button - but only if the state of the viewmodel is loggedIn
. To support the logout button, implement the event handler in the app
class:
logout() {
console.log('Logging out`');
this.loggedIn = false;
}
Although we are not using any validation, the UX should now support a facsimile of login/logout behaviour.
Archive of the lab so far:
Implement in DonationService
a login feature, which checks the username/password against the fixtures we have already loaded in this class. Only allow the user to progress to logged in status if there is a valid entry
Implement a signup viewmodel, which will add a user to the DonationService
users array. This view should be visible when the user presses a 'signup' option on the menu. This will take some work, as we want either the login view OR the signup view to appear. Subsequently, once logged in, we wish neither view to be visible.