can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
5,268 lines • 157 kB
HTML
<!DOCTYPE html>
<!--####################################################################
THIS IS A GENERATED FILE -- ANY CHANGES MADE WILL BE OVERWRITTEN
INSTEAD CHANGE:
source: [object Object]
@page guides/atm
######################################################################## -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>CanJS - ATM Guide</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" type="text/css" href="../static/bundles/bit-docs-site/static.css">
<link rel="shortcut icon" sizes="16x16 24x24 32x32 48x48 64x64" href="/docs/images/canjs_favicon.ico">
<link rel="apple-touch-icon" sizes="57x57" href="../../docs/images/canjs_favicon_57x57.png">
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="../../docs/images/canjs_favicon_57x57.png">
<link rel="apple-touch-icon" sizes="72x72" href="../../docs/images/canjs_favicon_72x72.png">
<link rel="apple-touch-icon" sizes="114x114" href="../../docs/images/canjs_favicon_114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="../../docs/images/canjs_favicon_128x128.png">
<link rel="apple-touch-icon" sizes="144x144" href="../../docs/images/canjs_favicon_144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="../../docs/images/canjs_favicon_152x152.png">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta name="apple-mobile-web-app-status-bar-style" content="white-translucent">
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-2302003-11', 'auto');
ga('send', 'pageview');
</script>
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger"/>
<label for="nav-trigger">Menu</label>
<div id="everything">
<div id="left" class="column">
<div class="top-left">
<div class="brand">
<div class="logo">
<a href="../../index.html" alt="CanJS"></a>
<div class="dropdown project-dropdown">
<a href="https://donejs.com/">DoneJS</a>
<a href="http://stealjs.com/">StealJS</a>
<a href="http://jquerypp.com/">jQuery ++</a>
<a href="https://funcunit.com/">FuncUnit</a>
<a href="http://documentjs.com/">DocumentJS</a>
</div>
</div>
<div class="version">
<div class="version-number">
3.0.0
</div>
<div class="dropdown version-dropdown">
<a href="https://v2.canjs.com">2.3.27</a>
</div>
</div>
</div>
<div class="search-bar">
<p>
</p>
</div>
</div>
<div class="bottom-left">
<div class="social-side-container">
<ul class="social-side">
<li>
<a class="header-mobile github" href="https://github.com/canjs/canjs" target="_blank"><img class="social-icon-small" src="../../docs/images/github.png">Github</a>
</li>
<li>
<a class="header-mobile twitter" href="https://twitter.com/canjs" target="_blank"><img class="social-icon-small" src="../../docs/images/twitter.png">Twitter</a>
</li>
</ul>
<ul class="social-side">
<li>
<a class="header-mobile" href="https://gitter.im/canjs/canjs" target="_blank">Chat</a>
</li>
<li>
<a class="header-mobile" href="http://forums.donejs.com/c/canjs" target="_blank">Forum</a>
</li>
</ul>
</div>
<ul>
<li class="
parent
expanded">
<a class="page"
href="../guides.html"
title="Welcome to CanJS! These guides are here to help you develop and improve your relationship with CanJS. After all, picking a JavaScript framework is a commitment. We want CanJS to be the
framework you marry. This page helps you know how advance through the different stages of this
relationship:">
Guides
</a>
<ul>
<li>
<span>introduction</span>
<ul>
<li class="
">
<a class="page"
href="mission.html"
title="Learn about CanJS's mission, why it matters, and how we've worked (and will keep working) to accomplish it.">
Mission
</a>
</li>
<li class="
">
<a class="page"
href="technical.html"
title="">
Technical Highlights
</a>
</li>
<li class="
">
<a class="page"
href="who-uses-canjs.html"
title="">
Who uses CanJS?
</a>
</li>
</ul>
</li>
<li>
<span>experiment</span>
<ul>
<li class="
">
<a class="page"
href="chat.html"
title="This guide walks through building real time chat application with CanJS's Core libraries. It takes about 30 minutes to complete.">
Chat Guide
</a>
</li>
<li class="
">
<a class="page"
href="todomvc.html"
title="This guide walks through building a slightly modified version of TodoMVC with CanJS's Core libraries and can-fixture. It takes about 1 hour to complete.">
TodoMVC Guide
</a>
</li>
<li class="current
parent
expanded">
<a class="page"
href="atm.html"
title="This guide walks through building and testing an ATM application with CanJS's
Core libraries. It teaches how to do test driven development (TDD)
and manage complex state. It takes about 2 hours to complete.">
ATM Guide
</a>
</li>
<li class="
">
<a class="page"
href="setup.html"
title="CanJS is packaged in multiple ways so that it can fit into any development workflow. Learn how to setup CanJS in different environments.">
Setting up CanJS
</a>
</li>
</ul>
</li>
<li>
<span>commitment</span>
<ul>
<li class="
">
<a class="page"
href="api.html"
title="This page walks through how to use and understand CanJS's API documentation.">
API Guide
</a>
</li>
<li class="
">
<a class="page"
href="examples.html"
title="">
Examples
</a>
</li>
<li class="
">
<a class="page"
href="../roadmap.html"
title="Learn about CanJS's future plans and how we make them, and how you can influence them.">
Roadmap
</a>
</li>
<li class="
">
<a class="page"
href="../migrate-3.html"
title="">
Migrating to 3.0
</a>
</li>
</ul>
</li>
<li>
<span>contribute</span>
<ul>
<li class="
">
<a class="page"
href="contributing/bug-report.html"
title="Learn how to submit a bug report.">
Bug Report
</a>
</li>
<li class="
">
<a class="page"
href="contributing/code.html"
title="Learn how contribute a code change to CanJS.">
Code
</a>
</li>
<li class="
">
<a class="page"
href="contributing/documentation.html"
title="Learn how to improve CanJS's site and documentation.">
Documentation
</a>
</li>
<li class="
">
<a class="page"
href="contributing/evangelism.html"
title="Learn about resources that can help you spread the word about CanJS.">
Evangelism
</a>
</li>
<li class="
">
<a class="page"
href="contributing/feature-suggestion.html"
title="Learn how to suggest a feature.">
Feature Suggestion
</a>
</li>
<li class="
">
<a class="page"
href="contributing/releases.html"
title="Release and hosting information for CanJS maintainers.">
Releases
</a>
</li>
</ul>
</li>
</ul>
</li>
<li class="
">
<a class="page"
href="../can-core.html"
title="The best, most hardened and generally useful libraries in CanJS.">
Core
</a>
</li>
<li class="
">
<a class="page"
href="../can-ecosystem.html"
title="Useful libraries that extend or add important features to the core collection.">
Ecosystem
</a>
</li>
<li class="
">
<a class="page"
href="../can-infrastructure.html"
title="Utility libraries that power the core and ecosystem collection.">
Infrastructure
</a>
</li>
<li class="
">
<a class="page"
href="../can-legacy.html"
title="Former libraries that we still accept patches for, but are not under active development.">
Legacy
</a>
</li>
</ul>
</div>
</div>
<div id="right" class="column">
<div class="top-right">
<div class="top-right-top">
<ul class="top-right-bitovi">
<li class="dropdown">
<a href="http://bitovi.com" class="bitovi icon-bits">Bitovi</a>
<ul class="dropdown-menu">
<li><a href="http://bitovi.com">Bitovi.com</a></li>
<li><a href="http://bitovi.com/blog/">Blog</a></li>
<li><a href="http://bitovi.com/consulting/">Consulting</a></li>
<li><a href="http://bitovi.com/training/">Training</a></li>
<li><a href="http://bitovi.com/open-source/">Open Source</a></li>
</ul>
</li>
</ul>
<div class="brand">
<div class="logo">
<a href="../../index.html" alt="CanJS"></a>
</div>
</div>
<ul class="top-right-links">
<li>
<a href="https://gitter.im/canjs/canjs">Chat</a>
</li>
<li>
<a href="http://forums.donejs.com/c/canjs">Forum</a>
</li>
<li>
<a class="github-button nav-social" href="https://github.com/canjs/canjs" data-count-href="/canjs/canjs/stargazers" data-count-api="/repos/canjs/canjs#stargazers_count">Star</a>
</li>
<li>
<a href="https://twitter.com/canjs" class="twitter-follow-button nav-social" data-show-count="true" data-show-screen-name="false">Follow @canjs</a><script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
</li>
</ul>
</div>
<div class="breadcrumb">
<li><a href="../../index.html">CanJS</a></li> /
<li><a href="../guides.html">Guides</a></li> /
<li><a href="atm.html">ATM Guide</a></li>
<li class="breadcrumb-dropdown">/ <a> On this page</a>
<ul class="on-this-page"></ul>
</li>
<div class="nav-toggle" title="Back to top"></div>
</div>
</div>
<div class="bottom-right">
<article>
<section class="title">
<div class="page-type">
<h1>ATM Guide</h1>
<div>page</div>
</div>
<section class="description">
<p>This guide walks through building and <strong>testing</strong> an ATM application with CanJS's
<a href="../can-core.html" title="The best, most hardened and generally useful libraries in CanJS.">Core libraries</a>. It teaches how to do test driven development (TDD)
and manage complex state. It takes about 2 hours to complete.</p>
</section>
</section>
<section class="on-this-page-table">
</section>
<section class="title-footer">
<ul class="title-links">
<!-- <li><a href="#">docco</a></li> -->
<li><a href="//github.com/canjs/canjs/tree/v3.0.0/docs/can-guides/experiment/atm/atm.md">source</a></li>
<!-- <li><a href="#">download</a></li> -->
<!-- <li><a href="#">tests</a></li> -->
</ul>
</section>
<section class="body">
<h2>Overview</h2>
<p>Checkout the final app:</p>
<p><a class="jsbin-embed" href="http://jsbin.com/yayupo/10/embed?js,output">JS Bin on jsbin.com</a></p>
<p>Notice it has tests at the bottom of the <code>Output</code> tab.</p>
<p>Watch the following video to see it in action:</p>
<blockquote>
<p>TODO: VIDEO OF IT IN ACTION</p>
</blockquote>
<h2>Setup</h2>
<p>The easiest way to get started is to clone the following JSBin by clicking the <strong>JS Bin</strong> button on the top left:</p>
<p><a class="jsbin-embed" href="http://justinbmeyer.jsbin.com/meziyu/3/edit?html,js,output">JS Bin on jsbin.com</a></p>
<p>The JSBin is designed to run both the application and its tests in the <code>OUTPUT</code>
tab. To set this up, the <code>HTML</code> tab:</p>
<ul>
<li><p>Loads QUnit for its testing library. It also includes the <code><div id="qunit"></div></code>
element where QUnit's test results will be written to.</p></li>
<li><p>Loads <a href="https://unpkg.com/can/dist/global/can.all.js">can.all.js</a>, which
is a script that includes all of CanJS core under a single global <code>can</code> namespace.</p>
<p>Generally speaking, you should not use the global can script and instead
should import things directly with a module loader like <a href="http://stealjs.com">StealJS</a>,
WebPack or Browserify. Read <a href="setup.html" title="CanJS is packaged in multiple ways so that it can fit into any development workflow. Learn how to setup CanJS in different environments.">Setting up CanJS</a> on how to setup CanJS in a real app.</p></li>
<li><p>Includes the content for a <code>app-template</code> <a href="../can-stache.html" title="Live binding Mustache and Handlebars-compatible templates.">can-stache</a> template. This template
provides the title for the ATM app, and uses the <code><atm-machine></code> custom <a href="../can-component.html" title="Create a custom element that can be used to manage widgets or application logic.">can-component</a>
element that will eventually provide the ATM functionality.</p></li>
</ul>
<p>The <code>JS</code> tab is split into two sections:</p>
<ul>
<li><code>CODE</code> - The ATM's models, view-models and component code will go here.</li>
<li><code>TESTS</code> - The ATM's tests will go here.</li>
</ul>
<p>Normally, your application's code and test will be in separate files and loaded
by different html pages. But we combine them here to fit within JSBin's limitations.</p>
<p>The <code>CODE</code> section is rendering the <code>app-template</code> with:</p>
<pre><code class="language-js">document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
</code></pre>
<p>The <code>TESTS</code> section is labeling which module will be tested:</p>
<pre><code class="language-js">QUnit.module("ATM system", {});
</code></pre>
<h2>Mock out switching between pages</h2>
<p>In this section, we will mock out which pages will be shown as the <code>state</code>
of the <code>ATM</code> changes.</p>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Switch between different pages of the application as the <code>ATM</code> view-model's <code>state</code> property changes
with <a href="../can-stache.helpers.switch.html" title="">{{#switch expression}}</a>.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='12-60,only'></span>
Update the <code>JavaScript</code> tab to:</p>
<ul>
<li>Create the <code>ATM</code> view-model with a <code>state</code> property initialized to <code>readingCard</code> with <a href="../can-define/map/map.html" title="Create observable objects.">can-define/map/map</a>.</li>
<li>Create an <code><atm-machine></code> custom element with <a href="../can-component.html" title="Create a custom element that can be used to manage widgets or application logic.">can-component</a>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
var ATM = can.DefineMap.extend({
state: {type: "string", value: "readingCard"}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {});
</code></pre>
<p><span line-highlight='5-13,only'></span>
When complete, you should see the <strong>"Reading Card"</strong> title.</p>
<p>This step outlines the page transitions we're going to make the <code>state</code>
property transition between:</p>
<ul>
<li>readingCard</li>
<li>readingPin</li>
<li>choosingTransaction</li>
<li>pickingAccount</li>
<li>depositInfo</li>
<li>withdrawalInfo</li>
<li>successfulTransaction</li>
<li>printingReceipt</li>
</ul>
<p>Each of those states are present in the following state diagram.</p>
<p><img src="../../docs/can-guides/experiment/atm/1-pages-template/state-diagram.png"></p>
<p>We'll build out these pages once we build the <code>Card</code> and <code>Transaction</code> sub-models that will make building the ATM view model easier.</p>
<h2>Card tests</h2>
<p>In this section, we will:</p>
<ul>
<li>Design an API for an ATM <code>Card</code></li>
<li>Write out tests for the card.</li>
</ul>
<p>An ATM <code>Card</code> will take a card <code>number</code> and <code>pin</code>. It will start out as
having a <code>state</code> of <code>"unverified"</code>. It will have a <code>verify</code> method
that will change the <code>state</code> to <code>"verifying"</code> and if the response is successful,
<code>state</code> will change to <code>"verified"</code>.</p>
<p>Update the <code>JS</code> tab to:</p>
<ul>
<li>Make the fake data request delay <code>1ms</code> by setting <a href="../can-fixture.delay.html" title="can-fixture.delay">delay</a> to <code>1</code> before every test and
restoring it to <code>2s</code> after every test runs.</li>
<li>Write a test that creates a valid card, calls <code>.verify()</code>, and asserts the <code>state</code> is <code>"verified"</code>.</li>
<li>Write a test that creates a invalid card, calls <code>.verify()</code>, and asserts the <code>state</code> is <code>"invalid"</code>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
var ATM = can.DefineMap.extend({
state: {type: "string", value: "readingCard"}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
</code></pre>
<p><span line-highlight='24-70,only'></span>
When complete you should have a breaking test. Now lets make it pass.</p>
<h2>Card model</h2>
<p>In this section, we will:</p>
<ul>
<li>Implement the <code>Card</code> model so that all tests pass.</li>
</ul>
<p>Update the <code>JavaScript</code> tab to:</p>
<ul>
<li>Simulate the <code>/verifyCard</code> with <a href="../can-fixture.html" title="can-fixture intercepts an AJAX request and simulates the response with a file or function.">can-fixture</a>. It return a successful response if
the request body has a <code>number</code> and <code>pin</code> and a 400 if not.</li>
<li>Use <a href="../can-define/map/map.html" title="Create observable objects.">can-define/map/map</a> to define the <code>Card</code> model, including:
<ul>
<li>a <code>number</code> and <code>pin</code> property.</li>
<li>a <code>state</code> property initialized to <code>unverified</code> that is not part of the card's <a href="../can-define.types.serialize.html" title="Defines custom serialization behavior for a property.">serialize</a>d data.</li>
<li>a <code>verify</code> method that posts the card's data to <code>/verifyCard</code> and updates the <code>state</code>
accordingly.</li>
</ul></li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var ATM = can.DefineMap.extend({
state: {type: "string", value: "readingCard"}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
</code></pre>
<p><span line-highlight='5-42,only'></span>
When complete, all tests should pass.</p>
<p>In this step, you implemented a <code>Card</code> model that encapsulates the behavior of its own state.</p>
<h2>Deposit test</h2>
<p>In this section, we will:</p>
<ul>
<li>Design an API retrieving <code>Account</code>s.</li>
<li>Design an API for a <code>Deposit</code> type.</li>
<li>Write out tests for the <code>Deposit</code> type.</li>
</ul>
<p>An <code>Account</code> will have an <code>id</code>, <code>name</code>, and <code>balance</code>. We'll use <a href="../can-connect.html" title="can-connect provides persisted data middleware. Use it to assemble powerful model layers for any JavaScript project.">can-connect</a> to add a
<a href="../can-connect/can/map/map.getList.html" title="Gets a list of instances of the map type.">getList</a> method that retrieves an account given a <code>card</code>.</p>
<p>A <code>Deposit</code> will take a <code>card</code>, an <code>amount</code>, and an <code>account</code>. Deposits will start out having
a <code>state</code> of <code>"invalid"</code>. When the deposit has a <code>card</code>, <code>amount</code> and <code>account</code>, the <code>state</code>
will change to <code>"ready"</code>. Once the deposit is ready, the <code>.execute()</code> method will change the state
to <code>"executing"</code> and then to <code>"executed"</code> once the transaction completes.</p>
<p>Update the <code>JS</code> tab to:</p>
<ul>
<li>Create a <code>deposit</code> with an <code>amount</code> and a <code>card</code>.</li>
<li>Check that the <code>state</code> is <code>"invalid"</code> because there is no <code>account</code>.</li>
<li>Use <code>Account.getList</code> to get the accounts for the card and:
<ul>
<li>set the <code>deposit.accounts</code> to the first account.</li>
<li>remember the starting <code>balance</code>.</li>
</ul></li>
<li>Use <a href="../can-define/map/map.prototype.on.html" title="Add event handlers to a map.">on</a> to listen for <code>state</code> changes. When <code>state</code> is:
<ul>
<li><code>"ready"</code>, <code>.execute()</code> the transaction.</li>
<li><code>"executed"</code>, verify the new account balance.</li>
</ul></li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var ATM = can.DefineMap.extend({
state: {type: "string", value: "readingCard"}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
</code></pre>
<p><span line-highlight='111-151,only'></span>
When complete, the <strong>Deposit</strong> test should run, but error because <em>Deposit is not defined</em>.</p>
<blockquote>
<p><strong>Optional:</strong> Challenge yourself by writing the <strong>Withdrawal</strong> test on your own. How is it different than the <strong>Deposit</strong> test?</p>
</blockquote>
<h2>Transaction, Deposit, and Withdrawal models</h2>
<p>In this section, we will:</p>
<ul>
<li>Implement the <code>Account</code> model.</li>
<li>Implement a base <code>Transaction</code> model and extend it into a <code>Deposit</code> and
<code>Withdrawal</code> model.</li>
<li>Get the <strong>Deposit</strong> test to pass.</li>
</ul>
<p>Update the <code>JavaScript</code> tab to:</p>
<ul>
<li>Simulate <code>/accounts</code> to return <code>Account</code> data with <a href="../can-fixture.html" title="can-fixture intercepts an AJAX request and simulates the response with a file or function.">can-fixture</a>.</li>
<li>Simulate <code>/deposit</code> to always return a successful result.</li>
<li>Simulate <code>/withdrawal</code> to always return a successful result.</li>
<li>Define the <code>Account</code> model to:
<ul>
<li>have an <code>id</code> property</li>
<li>have a <code>balance</code> property</li>
<li>have a <code>name</code> property</li>
</ul></li>
<li>Define an <code>Account.List</code> type with <a href="../can-define/list/list.html" title="Create observable lists.">can-define/list/list</a></li>
<li>Connect <code>Account</code> and <code>Account.List</code> types to the restful <code>"/accounts"</code> endpoint using <a href="../can-connect/can/base-map/base-map.html" title="Create connection with many of the best behaviors in can-connect and hook it up to
a map.">can-connect/can/base-map/base-map</a>.</li>
<li>Define the <code>Transaction</code> model to:
<ul>
<li>have an <code>account</code> and <code>card</code> property.</li>
<li>have an <code>executing</code> and <code>executed</code> property that track if the transaction is executing or has executed.</li>
<li>have a <code>rejected</code> property that stores the error given for a failed transaction.</li>
<li>have an <strong>abstract</strong> <code>ready</code> property that <code>Deposit</code> and <code>Withdrawal</code> will implement to return <code>true</code>
when the transaction is in a state able to be executed.</li>
<li>have a <code>state</code> property that reads other stateful properties and returns a string representation
of the state.</li>
<li>have an <strong>abstract</strong> <code>executeStart</code> method that <code>Deposit</code> and <code>Withdrawal</code> will implement to
execute the transaction and return a <code>Promise</code> the resolves when the transaction completes.</li>
<li>have an <strong>abstract</strong> <code>executeEnd</code> method that <code>Deposit</code> and <code>Withdrawal</code> will implement to
update the transactions values (typically the <code>account</code> balance) if the transaction completed
successfully.</li>
<li>have an <code>execute</code> method that calls <code>.executeStart()</code> and <code>executeEnd()</code> and keeps the stateful
properties updated correctly.</li>
</ul></li>
<li>Define the <code>Deposit</code> model to:
<ul>
<li>have an <code>amount</code> property.</li>
<li>implement <code>ready</code> to return <code>true</code> when the amount is greater than <code>0</code> and there's an <code>account</code>
and <code>card</code>.</li>
<li>implement <code>executeStart</code> to <code>POST</code> the deposit information to <code>/deposit</code></li>
<li>implement <code>executeEnd</code> to update the account balance.</li>
</ul></li>
<li>Define the <code>Withdrawal</code> model to behave in the same way as <code>Deposit</code> except that
it <code>POST</code>s the withdrawal information to <code>/withdrawal</code>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
state: {type: "string", value: "readingCard"}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
</code></pre>
<p><span line-highlight='13-31,63-187,only'></span>
When complete, the <strong>Deposit</strong> tests will pass.</p>
<h2>Reading Card page and test</h2>
<p>In this section, we will:</p>
<ul>
<li>Allow the user to enter a card number and go to the <strong>Reading Pin</strong> page.</li>
<li>Add tests to <strong>ATM Basics</strong> test.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Allow a user to call <code>cardNumber</code> with the <code><input></code>'s <code>value</code>.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='20-26,only'></span>
Update the <code>JavaScript</code> tab to:</p>
<ul>
<li>Declare a <code>card</code> property.</li>
<li>Derive a <code>state</code> property that changes to <code>"readingPin"</code> when <code>card</code> is defined.</li>
<li>Add a <code>cardNumber</code> that creates a <code>card</code> with the <code>number</code> provided.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
// stateful properties
card: Card,
// derived properties
get state(){
if(this.card) {
return "readingPin";
}
return "readingCard";
},
// methods
cardNumber: function(number) {
this.card = new Card({
number: number
});
}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
QUnit.asyncTest("ATM basics", function() {
var atm = new ATM();
equal(atm.state, "readingCard", "starts at reading card state");
atm.cardNumber("01233456789");
equal(atm.state, "readingPin", "moves to reading card state");
QUnit.start();
});
</code></pre>
<p><span line-highlight='190-205,313-325,only'></span>
When complete, you should be able to enter a card number and see the <strong>Reading Pin</strong>
page.</p>
<h2>Reading Pin page and test</h2>
<p>In this section, we will:</p>
<ul>
<li>Allow the user to enter a pin number and go to the <strong>Choosing Transaction</strong> page.</li>
<li>Add tests to <strong>ATM Basics</strong> test.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Call <code>pinNumber</code> with the <code><input></code>'s <code>value</code>.</li>
<li>Disable the <code><input></code> while the pin is being verified.</li>
<li>Show a loading icon while the pin is being verified.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='31-47,only'></span>
Update the <code>ATM</code> view model in the <code>CODE</code> section of the <code>JavaScript</code> tab to:</p>
<ul>
<li>Define an <code>accountsPromise</code> property that will contain a list of accounts for the <code>card</code>.</li>
<li>Define a <code>transactions</code> property that will contain a list of transactions for this session.</li>
<li>Update <code>state</code> to be in the <code>"choosingTransaction"</code> state when the <code>card</code> is verified.</li>
<li>Define a <code>pinNumber</code> method that updates the <code>card</code>'s <code>pin</code>, calls <code>.verify()</code>,
and then initializes the <code>accountsPromise</code> and <code>transactions</code> property.</li>
</ul>
<p>Update the <code>TESTS</code> section of the <code>JavaScript</code> tab to:</p>
<ul>
<li>Test calling <code>pinNumber</code> moves the <code>state</code> to <code>"choosingTransaction"</code>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
// stateful properties
card: Card,
accountsPromise: "any",
transactions: can.DefineList,
// derived properties
get state(){
if(this.card) {
if (this.card.state === "verified") {
return "choosingTransaction";
}
return "readingPin";
}
return "readingCard";
},
// methods
cardNumber: function(number) {
this.card = new Card({
number: number
});
},
pinNumber: function(pin) {
var card = this.card;
card.pin = pin;
this.transactions = new can.DefineList();
this.accountsPromise = card.verify().then(function(card) {
return Account.getList(card.serialize());
});
},
exit: function(){
this.set({
card: null,
accountsPromise: null,
transactions: null
});
}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
QUnit.asyncTest("ATM basics", function() {
var atm = new ATM();
equal(atm.state, "readingCard", "starts at reading card state");
atm.cardNumber("01233456789");
equal(atm.state, "readingPin", "moves to reading card state");
atm.pinNumber("1234");
ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
atm.on("state", function(ev, newVal) {
if (newVal === "choosingTransaction") {
QUnit.ok(true, "in choosingTransaction");
QUnit.start();
}
});
});
</code></pre>
<p><span line-highlight='192-193,198-200,212-228,346-356,only'></span>
When complete, you should be able to enter a card and pin number and see the <strong>Choosing Transaction</strong>
page.</p>
<h2>Choosing Transaction page and test</h2>
<p>In this section, we will:</p>
<ul>
<li>Allow the user to pick a transaction type and go to the <strong>Picking Account</strong> page.</li>
<li>Add tests to <strong>ATM Basics</strong> test.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Have buttons for choosing a deposit, withdrawal, or print a receipt and exit.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="chooseDeposit()">Deposit</li>
<li ($click)="chooseWithdraw()">Withdraw</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='52-59,only'></span>
Update the <code>ATM</code> view model in the <code>CODE</code> section of the <code>JavaScript</code> tab to:</p>
<ul>
<li>Define a <code>currentTransaction</code> property that when set, adds the previous <code>currentTransaction</code>
to the list of <code>transactions</code>.</li>
<li>Update the <code>state</code> property to <code>"pickingAccount"</code> when there is a <code>currentTransaction</code>.</li>
<li>Update the <code>exit</code> method to clear the <code>currentTransaction</code> property.</li>
<li>Define <code>chooseDeposit</code> that creates a <code>Deposit</code> and sets it as the <code>currentTransaction</code>.</li>
<li>Define <code>chooseWithdraw</code> that creates a <code>Withdraw</code> and sets it as the <code>currentTransaction</code>.</li>
</ul>
<p>Update the <code>TESTS</code> section of the <code>JavaScript</code> tab to:</p>
<ul>
<li>Call <code>.chooseDeposit()</code> and verify the state moves to <code>"pickingAccount"</code>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
// stateful properties
card: Card,
accountsPromise: "any",
transactions: can.DefineList,
currentTransaction: {
set: function(newTransaction) {
var currentTransaction = this.currentTransaction;
if (this.transactions && currentTransaction &&
currentTransaction.state === "executed") {
this.transactions.push(currentTransaction);
}
return newTransaction;
}
},
// derived properties
get state(){
if (this.currentTransaction) {
return "pickingAccount";
}
if(this.card) {
if (this.card.state === "verified") {
return "choosingTransaction";
}
return "readingPin";
}
return "readingCard";
},
// methods
cardNumber: function(number) {
this.card = new Card({
number: number
});
},
pinNumber: function(pin) {
var card = this.card;
card.pin = pin;
this.transactions = new can.DefineList();
this.accountsPromise = card.verify().then(function(card) {
return Account.getList(card.serialize());
});
},
exit: function(){
this.set({
card: null,
accountsPromise: null,
transactions: null,
currentTransaction: null
});
},
chooseDeposit: function() {
this.currentTransaction = new Deposit({
card: this.card
});
},
chooseWithdraw: function() {
this.currentTransaction = new Withdrawal({
card: this.card
});
}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
QUnit.asyncTest("ATM basics", function() {
var atm = new ATM();
equal(atm.state, "readingCard", "starts at reading card state");
atm.cardNumber("01233456789");
equal(atm.state, "readingPin", "moves to reading card state");
atm.pinNumber("1234");
ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
atm.on("state", function(ev, newVal) {
if (newVal === "choosingTransaction") {
QUnit.ok(true, "in choosingTransaction");
atm.chooseDeposit();
} else if (newVal === "pickingAccount") {
QUnit.ok(true, "in picking account state");
QUnit.start();
}
});
});
</code></pre>
<p><span line-highlight='194-204,209-211,243,246-255,363,382-389,only'></span></p>
<blockquote>
<p>We will define <code>printReceiptAndExit</code> later!</p>
</blockquote>
<h2>Picking Account page and test</h2>
<p>In this section, we will:</p>
<ul>
<li>Allow the user to pick an account and go to either the <strong>Deposit Info</strong> or
<strong>Withdrawal Info</strong> page.</li>
<li>Add tests to <strong>ATM Basics</strong> test.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Write out a <em>"Loading Accounts ..."</em> message while the accounts are loading.</li>
<li>Write out the accounts when loaded.</li>
<li>Call <code>chooseAccount()</code> when an account is clicked.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="chooseDeposit()">Deposit</li>
<li ($click)="chooseWithdraw()">Withdraw</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
<p>Please pick your account:</p>
{{#if accountsPromise.isPending}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
Loading Accounts ...
</p>
</div>
{{else}}
<ul>
{{#each accountsPromise.value}}
<li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
{{/each}}
</ul>
{{/if}}
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='64-78,only'></span>
Update the <code>ATM</code> view model in the <code>CODE</code> section of the <code>JavaScript</code> tab to:</p>
<ul>
<li>Change <code>state</code> to check if the <code>currentTransaction</code> has an <code>account</code> and update the
value to <code>"depositInfo"</code> or <code>"withdrawalInfo"</code> depending on the type of the <code>currentTransaction</code>.</li>
<li>Add a <code>chooseAccount</code> method that sets the <code>currentTransaction</code>'s <code>account</code>.</li>
</ul>
<p>Update the <code>TESTS</code> section of the <code>JavaScript</code> tab to:</p>
<ul>
<li>Call <code>.chooseAccount()</code> with the first account loaded.</li>
<li>Verify the state changes to <code>"depositInfo"</code>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
// stateful properties
card: Card,
accountsPromise: "any",
transactions: can.DefineList,
currentTransaction: {
set: function(newTransaction) {
var currentTransaction = this.currentTransaction;
if (this.transactions && currentTransaction &&
currentTransaction.state === "executed") {
this.transactions.push(currentTransaction);
}
return newTransaction;
}
},
// derived properties
get state(){
if (this.currentTransaction) {
if (this.currentTransaction.account) {
if (this.currentTransaction instanceof Deposit) {
return "depositInfo";
} else {
return "withdrawalInfo";
}
}
return "pickingAccount";
}
if(this.card) {
if (this.card.state === "verified") {
return "choosingTransaction";
}
return "readingPin";
}
return "readingCard";
},
// methods
cardNumber: function(number) {
this.card = new Card({
number: number
});
},
pinNumber: function(pin) {
var card = this.card;
card.pin = pin;
this.transactions = new can.DefineList();
this.accountsPromise = card.verify().then(function(card) {
return Account.getList(card.serialize());
});
},
exit: function(){
this.set({
card: null,
accountsPromise: null,
transactions: null,
currentTransaction: null
});
},
chooseDeposit: function() {
this.currentTransaction = new Deposit({
card: this.card
});
},
chooseWithdraw: function() {
this.currentTransaction = new Withdrawal({
card: this.card
});
},
chooseAccount: function(account) {
this.currentTransaction.account = account;
}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
QUnit.asyncTest("ATM basics", function() {
var atm = new ATM();
equal(atm.state, "readingCard", "starts at reading card state");
atm.cardNumber("01233456789");
equal(atm.state, "readingPin", "moves to reading card state");
atm.pinNumber("1234");
ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
atm.on("state", function(ev, newVal) {
if (newVal === "choosingTransaction") {
QUnit.ok(true, "in choosingTransaction");
atm.chooseDeposit();
} else if (newVal === "pickingAccount") {
QUnit.ok(true, "in picking account state");
atm.accountsPromise.then(function(accounts){
atm.chooseAccount(accounts[0]);
});
} else if (newVal === "depositInfo") {
QUnit.ok(true, "in depositInfo state");
QUnit.start();
}
});
});
</code></pre>
<p><span line-highlight='210-216,264-266,398-407,only'></span></p>
<h2>Deposit Info page and test</h2>
<p>In this section, we will:</p>
<ul>
<li>Allow the user to enter the amount of a deposit and go to the <strong>Successful Transaction</strong> page.</li>
<li>Add tests to <strong>ATM Basics</strong> test.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Ask the user how much they would like to deposit into the account.</li>
<li>Update <code>currentTransaction.amount</code> with an <code><input></code>'s <code>value</code>.</li>
<li>If the transaction is executing, show a spinner.</li>
<li>If the transaction is not executed:
<ul>
<li>show a <strong>Deposit</strong> button that will be
active only once the transaction has a value.</li>
<li>show a <strong>cancel</strong> button that will clear this transaction.</li>
</ul></li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="chooseDeposit()">Deposit</li>
<li ($click)="chooseWithdraw()">Withdraw</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
<p>Please pick your account:</p>
{{#if accountsPromise.isPending}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
Loading Accounts ...
</p>
</div>
{{else}}
<ul>
{{#each accountsPromise.value}}
<li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
{{/each}}
</ul>
{{/if}}
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
<p>
How much would you like to deposit
into {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="deposit" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Deposit
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='83-104,only'></span>
Update the <code>ATM</code> view model in the <code>JavaScript</code> tab to:</p>
<ul>
<li>Change <code>state</code> to <code>"successfulTransaction"</code> if the <code>currentTransaction</code> was executed.</li>
<li>Add a <code>removeTransaction</code> method that removes the <code>currentTransaction</code>, which will revert state
to <code>"choosingTransaction"</code>.</li>
</ul>
<p>Update the <code>ATM basics</code> test in the <code>JavaScript</code> tab to:</p>
<ul>
<li>Add an <code>amount</code> to the <code>currentTransaction</code>.</li>
<li>Make sure the <code>currentTransaction</code> is <code>ready</code> to be executed.</li>
<li>Execute the <code>currentTransaction</code> and make sure that the <code>state</code> stays as <code>"depositInfo"</code> until
the transaction is successful.</li>
<li>Verify the state changed to <code>"successfulTransaction"</code>.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
// stateful properties
card: Card,
accountsPromise: "any",
transactions: can.DefineList,
currentTransaction: {
set: function(newTransaction) {
var currentTransaction = this.currentTransaction;
if (this.transactions && currentTransaction &&
currentTransaction.state === "executed") {
this.transactions.push(currentTransaction);
}
return newTransaction;
}
},
// derived properties
get state(){
if (this.currentTransaction) {
if (this.currentTransaction.state === "executed") {
return "successfulTransaction";
}
if (this.currentTransaction.account) {
if (this.currentTransaction instanceof Deposit) {
return "depositInfo";
} else {
return "withdrawalInfo";
}
}
return "pickingAccount";
}
if(this.card) {
if (this.card.state === "verified") {
return "choosingTransaction";
}
return "readingPin";
}
return "readingCard";
},
// methods
cardNumber: function(number) {
this.card = new Card({
number: number
});
},
pinNumber: function(pin) {
var card = this.card;
card.pin = pin;
this.transactions = new can.DefineList();
this.accountsPromise = card.verify().then(function(card) {
return Account.getList(card.serialize());
});
},
exit: function(){
this.set({
card: null,
accountsPromise: null,
transactions: null,
currentTransaction: null
});
},
chooseDeposit: function() {
this.currentTransaction = new Deposit({
card: this.card
});
},
chooseWithdraw: function() {
this.currentTransaction = new Withdrawal({
card: this.card
});
},
chooseAccount: function(account) {
this.currentTransaction.account = account;
},
removeTransaction: function() {
this.currentTransaction = null;
}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
QUnit.asyncTest("ATM basics", function() {
var atm = new ATM();
equal(atm.state, "readingCard", "starts at reading card state");
atm.cardNumber("01233456789");
equal(atm.state, "readingPin", "moves to reading card state");
atm.pinNumber("1234");
ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
atm.on("state", function(ev, newVal) {
if (newVal === "choosingTransaction") {
QUnit.ok(true, "in choosingTransaction");
atm.chooseDeposit();
} else if (newVal === "pickingAccount") {
QUnit.ok(true, "in picking account state");
atm.accountsPromise.then(function(accounts){
atm.chooseAccount(accounts[0]);
});
} else if (newVal === "depositInfo") {
QUnit.ok(true, "in depositInfo state");
var currentTransaction = atm.currentTransaction;
currentTransaction.amount = 120;
QUnit.ok(currentTransaction.ready, "we are ready to execute");
currentTransaction.execute();
QUnit.equal(atm.state, "depositInfo", "in deposit state until successful");
} else if (newVal === "successfulTransaction") {
QUnit.ok(true, "in successfulTransaction state");
QUnit.start();
}
});
});
</code></pre>
<p><span line-highlight='189,210-212,271-273,381,412-423,only'></span>
When complete, you should be able to enter a deposit amount and see that
the transaction was successful.</p>
<h2>Withdrawal Info page</h2>
<p>In this section, we will:</p>
<ul>
<li>Allow the user to enter the amount of a withdrawal and go to the <strong>Successful Transaction</strong> page.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>Add a <strong>Withdraw</strong> page that works very similar to the <strong>Deposit</strong> page.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="chooseDeposit()">Deposit</li>
<li ($click)="chooseWithdraw()">Withdraw</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
<p>Please pick your account:</p>
{{#if accountsPromise.isPending}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
Loading Accounts ...
</p>
</div>
{{else}}
<ul>
{{#each accountsPromise.value}}
<li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
{{/each}}
</ul>
{{/if}}
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
<p>
How much would you like to deposit
into {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="deposit" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Deposit
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
<p>
How much would you like to withdraw
from {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="withdrawl" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Withdraw
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='109-129,only'></span>
When complete, you should be able to enter a withdrawal amount and see that
the transaction was successful.</p>
<blockquote>
<p><strong>Optional:</strong> Challenge yourself by adding a test for the <code>withdrawalInfo</code> state of an atm instance. Consider the progression of states needed to make it to the <code>withdrawalInfo</code> state. How is it different from the <strong>ATM basics</strong> test we already have?</p>
</blockquote>
<h2>Transaction Successful page</h2>
<p>In this section, we will:</p>
<ul>
<li>Show the result of the transaction.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>List out the account balance.</li>
<li>Add buttons to:
<ul>
<li>start another transaction, or</li>
<li>print a receipt and exit the ATM. (<code>printReceiptAndExit</code> will be implemented in the next section)</li>
</ul></li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="chooseDeposit()">Deposit</li>
<li ($click)="chooseWithdraw()">Withdraw</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
<p>Please pick your account:</p>
{{#if accountsPromise.isPending}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
Loading Accounts ...
</p>
</div>
{{else}}
<ul>
{{#each accountsPromise.value}}
<li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
{{/each}}
</ul>
{{/if}}
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
<p>
How much would you like to deposit
into {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="deposit" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Deposit
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
<p>
How much would you like to withdraw
from {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="withdrawl" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Withdraw
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
<p>
{{currentTransaction.account.name}} has
${{currentTransaction.account.balance}}.
</p>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="removeTransaction()">Another transaction</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='134-144,only'></span>
When complete, you should be able to make a deposit or withdrawal, see the updated account balance
and then start another transaction.</p>
<h2>Printing Recipe page and test</h2>
<p>In this section, we will:</p>
<ul>
<li>Make it possible to see a receipt of all transactions and exit the ATM.</li>
</ul>
<p>Update the <code>HTML</code> tab to:</p>
<ul>
<li>List out all the transactions the user has completed.</li>
<li>List out the final value of all accounts.</li>
</ul>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='atm-template'>
<div class="screen">
<div class="screen-content">
<div class="screen-glass">
{{#switch state}}
{{#case "readingCard"}}
<h2>Reading Card</h2>
<p>Welcome to canATM where there are <strong>never</strong>
fees!</p>
</p>
<p>
Enter Card Number:
<input name="card" ($enter)="cardNumber(%element.value)"/>
</p>
{{/case}}
{{#case "readingPin"}}
<h2>Reading Pin</h2>
<p>
Enter Pin Number:
<input name="pin" type="password"
autofocus
{{#is card.state "verifying"}}DISABLED{{/is}}
($enter)="pinNumber(%element.value)"/>
{{#is card.state "verifying"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
verifying
</p>
</div>
{{/is}}
</p>
<a href="javascript://" ($click)="exit()">exit</a>
{{/case}}
{{#case "choosingTransaction"}}
<h2>Choose Transaction</h2>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="chooseDeposit()">Deposit</li>
<li ($click)="chooseWithdraw()">Withdraw</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "pickingAccount"}}
<h2>Pick Account</h2>
<p>Please pick your account:</p>
{{#if accountsPromise.isPending}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
Loading Accounts ...
</p>
</div>
{{else}}
<ul>
{{#each accountsPromise.value}}
<li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
{{/each}}
</ul>
{{/if}}
{{/case}}
{{#case "depositInfo"}}
<h2>Deposit</h2>
<p>
How much would you like to deposit
into {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="deposit" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Deposit
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "withdrawalInfo"}}
<h2>Withdraw</h2>
<p>
How much would you like to withdraw
from {{currentTransaction.account.name}}
(${{currentTransaction.account.balance}})?
<input name="withdrawl" {($value)}="currentTransaction.amount"/>
</p>
{{#eq currentTransaction.state "executing"}}
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
executing
</p>
</div>
{{else}}
<button ($click)="currentTransaction.execute()"
{{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
Withdraw
</button>
<a href="javascript://" ($click)="removeTransaction()">cancel</a>
{{/eq}}
{{/case}}
{{#case "successfulTransaction"}}
<h2>Transaction Successful!</h2>
<p>
{{currentTransaction.account.name}} has
${{currentTransaction.account.balance}}.
</p>
<p>What would you like to do?</p>
<nav>
<ul>
<li ($click)="removeTransaction()">Another transaction</li>
<li ($click)="printReceiptAndExit()">Exit</li>
</ul>
</nav>
{{/case}}
{{#case "printingReceipt"}}
<h2>Printing Receipt</h2>
<h3>Transactions</h3>
<ul>
{{#if transactions.length}}
{{#each transactions}}
<li>{{actionName(this)}} ${{amount}} {{actionPrep(this)}} {{account.name}}</li>
{{/each}}
{{else}}
<li>None</li>
{{/if}}
</ul>
<h3>Accounts</h3>
<ul>
{{#each accountsPromise.value}}
<li ($click)="chooseAccount(.)">{{name}} - ${{balance}}</li>
{{/each}}
</ul>
<div class='warn'>
<p>
<img src="http://v3.canjs.com/docs/images/loader.gif"/>
printing
</p>
</div>
{{/case}}
{{#default}}
<h2>Error</h2>
<p>Invalid state - {{state}}</p>
{{/default}}
{{/switch}}
</div>
</div>
</div>
</script>
<script type='text/stache' id='app-template'>
<div class="title">
<h1>canATM</h1>
</div>
<atm-machine/>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.all.js"></script>
<div id="qunit"></div>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="http://code.jquery.com/qunit/qunit-1.12.0.js"></script>
</body>
</html>
</code></pre>
<p><span line-highlight='149-170,only'></span>
Update the <code>ATM</code> view model in the <code>JavaScript</code> tab to:</p>
<ul>
<li>Add a <code>printingReceipt</code> and <code>receiptTime</code> property.</li>
<li>Change the <code>state</code> to <code>"printingReceipt"</code> when <code>printingReceipt</code> is true.</li>
<li>Make <code>.exit</code> set <code>printingReceipt</code> to <code>null</code>.</li>
<li>Add a <code>printReceiptAndExit</code> method that:
<ul>
<li>clears the current transaction, which will add the currentTransaction to the list of transactions.</li>
<li>sets <code>printingReceipt</code> to <code>true</code> for <code>printingReceipt</code> time.</li>
</ul></li>
</ul>
<p>Update the <code>ATM basics</code> test in the <code>JavaScript</code> tab to:</p>
<ul>
<li>Shorten the default <code>receiptTime</code> so the tests move quickly.</li>
<li>Call <code>printReceiptAndExit</code> and make sure that the <code>state</code> changes to <code>"printingReceipt"</code> and
then to <code>"readingCard"</code> and ensure that sensitive information is cleared from the atm.</li>
</ul>
<pre><code class="language-js">// ========================================
// CODE
// ========================================
can.fixture({
"/verifyCard": function(request, response) {
if (!request.data || !request.data.number || !request.data.pin) {
response(400, {});
} else {
return {};
}
},
"/accounts": function() {
return {
data: [{
balance: 100,
id: 1,
name: "checking"
}, {
balance: 10000,
id: 2,
name: "savings"
}]
};
},
"/deposit": function() {
return {};
},
"/withdrawal": function() {
return {};
}
});
can.fixture.delay = 1000;
var Card = can.DefineMap.extend({
number: "string",
pin: "string",
state: {
value: "unverified",
serialize: false
},
verify: function() {
this.state = "verifying";
var self = this;
return can.ajax({
type: "POST",
url: "/verifyCard",
data: this.serialize()
}).then(
function() {
self.state = "verified";
return self;
},
function() {
self.state = "invalid";
return self;
});
}
});
var Account = can.DefineMap.extend("Account", {
id: "number",
balance: "number",
name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
"*": Account
});
can.connect.baseMap({
url: "/accounts",
Map: Account,
List: Account.List,
name: "accounts"
});
var Transaction = can.DefineMap.extend({
account: Account,
card: Card,
executing: {
type: "boolean",
value: false
},
executed: {
type: "boolean",
value: false
},
rejected: "any",
get ready(){
throw new Error("Transaction::ready must be provided by extended type");
},
get state() {
if (this.rejected) {
return "rejected";
}
if (this.executed) {
return "executed";
}
if (this.executing) {
return "executing";
}
// make sure there's an amount, account, and card
if (this.ready) {
return "ready";
}
return "invalid";
},
executeStart: function(){
throw new Error("Transaction::executeStart must be provided by extended type");
},
executeEnd: function(){
throw new Error("Transaction::executeEnd must be provided by extended type");
},
execute: function() {
if (this.state === "ready") {
this.executing = true;
var def = this.executeStart(),
self = this;
def.then(function() {
can.batch.start();
self.set({
executing: false,
executed: true
});
self.executeEnd();
can.batch.stop();
}, function(reason){
self.set({
executing: false,
executed: true,
rejected: reason
});
});
}
}
});
var Deposit = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/deposit",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance + this.amount;
}
});
var Withdrawal = Transaction.extend({
amount: "number",
get ready() {
return this.amount > 0 &&
this.account &&
this.card;
},
executeStart: function() {
return can.ajax({
type: "POST",
url: "/withdrawal",
data: {
card: this.card.serialize(),
accountId: this.account.id,
amount: this.amount
}
});
},
executeEnd: function(data) {
this.account.balance = this.account.balance - this.amount;
}
});
var ATM = can.DefineMap.extend({
// stateful properties
card: Card,
accountsPromise: "any",
transactions: can.DefineList,
currentTransaction: {
set: function(newTransaction) {
var currentTransaction = this.currentTransaction;
if (this.transactions && currentTransaction &&
currentTransaction.state === "executed") {
this.transactions.push(currentTransaction);
}
return newTransaction;
}
},
printingReceipt: "boolean",
receiptTime: {
value: 5000,
type: "number"
},
// derived properties
get state(){
if (this.printingReceipt) {
return "printingReceipt";
}
if (this.currentTransaction) {
if (this.currentTransaction.state === "executed") {
return "successfulTransaction";
}
if (this.currentTransaction.account) {
if (this.currentTransaction instanceof Deposit) {
return "depositInfo";
} else {
return "withdrawalInfo";
}
}
return "pickingAccount";
}
if(this.card) {
if (this.card.state === "verified") {
return "choosingTransaction";
}
return "readingPin";
}
return "readingCard";
},
// methods
cardNumber: function(number) {
this.card = new Card({
number: number
});
},
pinNumber: function(pin) {
var card = this.card;
card.pin = pin;
this.transactions = new can.DefineList();
this.accountsPromise = card.verify().then(function(card) {
return Account.getList(card.serialize());
});
},
exit: function(){
this.set({
card: null,
accountsPromise: null,
transactions: null,
currentTransaction: null,
printingReceipt: null
});
},
printReceiptAndExit: function() {
this.currentTransaction = null;
this.printingReceipt = true;
var self = this;
setTimeout(function() {
self.exit();
}, this.receiptTime);
},
chooseDeposit: function() {
this.currentTransaction = new Deposit({
card: this.card
});
},
chooseWithdraw: function() {
this.currentTransaction = new Withdrawal({
card: this.card
});
},
chooseAccount: function(account) {
this.currentTransaction.account = account;
},
removeTransaction: function() {
this.currentTransaction = null;
}
});
can.Component.extend({
tag: "atm-machine",
view: can.stache.from("atm-template"),
ViewModel: ATM,
});
document.body.insertBefore(
can.stache.from("app-template")({}),
document.body.firstChild
);
// ========================================
// TESTS
// ========================================
QUnit.module("ATM system", {
setup: function() {
can.fixture.delay = 1;
},
teardown: function() {
can.fixture.delay = 2000;
}
});
QUnit.asyncTest("Valid Card", function() {
var c = new Card({
number: "01234567890",
pin: 1234
});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "verified", "card is verified");
QUnit.start();
});
});
QUnit.asyncTest("Invalid Card", function() {
var c = new Card({});
QUnit.equal(c.state, "unverified");
c.verify();
QUnit.equal(c.state, "verifying", "card is verifying");
c.on("state", function(ev, newVal) {
QUnit.equal(newVal, "invalid", "card is invalid");
QUnit.start();
});
});
QUnit.asyncTest("Deposit", 6, function() {
var card = new Card({
number: "0123456789",
pin: "1122"
});
var deposit = new Deposit({
amount: 100,
card: card
});
equal(deposit.state, "invalid");
var startingBalance;
Account.getList(card.serialize()).then(function(accounts) {
QUnit.ok(true, "got accounts");
deposit.account = accounts[0];
startingBalance = accounts[0].balance;
});
deposit.on("state", function(ev, newVal) {
if (newVal === "ready") {
QUnit.ok(true, "deposit is ready");
deposit.execute();
} else if (newVal === "executing") {
QUnit.ok(true, "executing a deposit");
} else if (newVal === "executed") {
QUnit.ok(true, "executed a deposit");
equal(deposit.account.balance, 100 + startingBalance);
start();
}
});
});
QUnit.asyncTest("ATM basics", function() {
var atm = new ATM();
equal(atm.state, "readingCard", "starts at reading card state");
atm.cardNumber("01233456789");
equal(atm.state, "readingPin", "moves to reading card state");
atm.pinNumber("1234");
ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");
atm.on("state", function(ev, newVal) {
if (newVal === "choosingTransaction") {
QUnit.ok(true, "in choosingTransaction");
atm.chooseDeposit();
} else if (newVal === "pickingAccount") {
QUnit.ok(true, "in picking account state");
atm.accountsPromise.then(function(accounts){
atm.chooseAccount(accounts[0]);
});
} else if (newVal === "depositInfo") {
QUnit.ok(true, "in depositInfo state");
var currentTransaction = atm.currentTransaction;
currentTransaction.amount = 120;
QUnit.ok(currentTransaction.ready, "we are ready to execute");
currentTransaction.execute();
QUnit.equal(atm.state, "depositInfo", "in deposit state until successful");
} else if (newVal === "successfulTransaction") {
QUnit.ok(true, "in successfulTransaction state");
atm.receiptTime = 100;
atm.printReceiptAndExit();
} else if (newVal === "printingReceipt") {
QUnit.ok(true, "in printingReceipt state");
} else if (newVal === "readingCard") {
QUnit.ok(true, "in readingCard state");
QUnit.ok(!atm.card, "card is removed");
QUnit.ok(!atm.transactions, "transactions removed");
QUnit.start();
}
});
});
</code></pre>
<p><span line-highlight='189,205-209,213-215,263,266-273,397,437-451,only'></span>
When complete, you have a working ATM! Cha-ching!</p>
<script src="//static.jsbin.com/js/embed.min.js?3.39.18"></script>
</section>
<script type="text/javascript">
var docObject = {"src":{"path":"docs/can-guides/experiment/atm/atm.md"},"description":"\nThis guide walks through building and __testing__ an ATM application with CanJS's\n[can-core Core libraries]. It teaches how to do test driven development (TDD)\nand manage complex state. It takes about 2 hours to complete.\n","name":"guides/atm","title":"ATM Guide","type":"page","parent":"guides/experiment","order":3,"comment":" ","pathToRoot":"../.."};
</script>
</article>
<footer><p>CanJS is part of <a href="http://donejs.com" target="_blank">DoneJS</a>. Created and maintained by the core <a href="https://donejs.com/About.html#section=section_Team" target="_blank">DoneJS team</a> and <a href="http://bitovi.com" target="_blank">Bitovi</a>. <strong>Currently 3.0.0.</strong></p></footer>
</div>
</div>
</div>
<script>
steal = {
instantiated: {
"bundles/bit-docs-site/static.css!$css" : null
}
};
</script>
<script type='text/javascript' data-main="bit-docs-site/static" src="../static/node_modules/steal/steal.production.js"></script>
<script async defer src="https://buttons.github.io/buttons.js"></script>
</body>
</html>