can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
1,739 lines (1,310 loc) • 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() {
i