├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── karma.conf.js
├── package.json
├── protractor.conf.js
├── spec-bundle.js
├── src
├── UserNotifyingExceptionHandler.ts
├── account.spec.ts
├── account.ts
├── app
│ ├── app.css
│ ├── app.html
│ ├── app.spec.ts
│ └── app.ts
├── bank.spec.ts
├── bank.ts
├── bootstrap.ts
├── components
│ ├── AccountOperationsComponent.html
│ ├── AccountOperationsComponent.spec.ts
│ ├── AccountOperationsComponent.ts
│ ├── ShowBalancesComponent.html
│ ├── ShowBalancesComponent.spec.ts
│ └── ShowBalancesComponent.ts
├── polyfills.ts
├── public
│ ├── angular-shield.png
│ ├── css
│ │ └── alerts.css
│ ├── favicon.ico
│ ├── humans.txt
│ ├── icon
│ │ ├── android-icon-144x144.png
│ │ ├── android-icon-192x192.png
│ │ ├── android-icon-36x36.png
│ │ ├── android-icon-48x48.png
│ │ ├── android-icon-72x72.png
│ │ ├── android-icon-96x96.png
│ │ ├── apple-icon-114x114.png
│ │ ├── apple-icon-120x120.png
│ │ ├── apple-icon-144x144.png
│ │ ├── apple-icon-152x152.png
│ │ ├── apple-icon-180x180.png
│ │ ├── apple-icon-57x57.png
│ │ ├── apple-icon-60x60.png
│ │ ├── apple-icon-72x72.png
│ │ ├── apple-icon-76x76.png
│ │ ├── apple-icon-precomposed.png
│ │ ├── apple-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── favicon.ico
│ │ ├── ms-icon-144x144.png
│ │ ├── ms-icon-150x150.png
│ │ ├── ms-icon-310x310.png
│ │ └── ms-icon-70x70.png
│ ├── img
│ │ ├── angular-logo.png
│ │ └── angularclass-logo.png
│ ├── index.html
│ ├── manifest.json
│ ├── robots.txt
│ └── service-worker.js
└── vendor.ts
├── test
├── injector.spec.ts
└── sanity-test.spec.ts
├── tsconfig.json
├── tslint.json
├── typedoc.json
├── typings.json
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # @AngularClass
2 | # http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | insert_final_newline = false
16 | trim_trailing_whitespace = false
17 |
18 | [*.json]
19 | insert_final_newline = false
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # @AngularClass
2 |
3 | # Logs
4 | logs
5 | *.log
6 |
7 | # Runtime data
8 | pids
9 | *.pid
10 | *.seed
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
19 | .grunt
20 |
21 | # Compiled binary addons (http://nodejs.org/api/addons.html)
22 | build/Release
23 |
24 | # Users Environment Variables
25 | .lock-wscript
26 |
27 | # OS generated files #
28 | .DS_Store
29 | ehthumbs.db
30 | Icon?
31 | Thumbs.db
32 |
33 | # Node Files #
34 | /node_modules
35 | /bower_components
36 |
37 | # Coverage #
38 | /coverage/
39 |
40 | # Typing #
41 | /src/typings/tsd/
42 | /typings/
43 | /tsd_typings/
44 |
45 | # Dist #
46 | /dist
47 | /public/__build__/
48 | /src/*/__build__/
49 | __build__/**
50 | .webpack.json
51 |
52 | # Doc
53 | /doc/
54 |
55 | # IDE #
56 | .idea/
57 | *.swp
58 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "5"
4 | - "4"
5 | env:
6 | - CXX=g++-4.8
7 | addons:
8 | apt:
9 | sources:
10 | - ubuntu-toolchain-r-test
11 | packages:
12 | - g++-4.8
13 | before_script:
14 | - npm run lint
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Ritter Insurance Marketing
4 |
5 | Project starter: Copyright (c) 2015 AngularClass LLC
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular2 Bank
2 |
3 | 
4 |
5 | An example bank implementation to experiment with Angular2, TypeScript, etc.
6 |
7 | # Core feature goals
8 |
9 | - Create and close accounts
10 | - View account balances
11 | - Ability to deposit, withdraw, and transfer account funds
12 |
13 | # Setup
14 |
15 | - Running the app: https://github.com/AngularClass/angular2-webpack-starter/tree/f8584c1e5c3d64301774e6787603e070c41b1fd7#running-the-app
16 |
17 | # Miscellaneous
18 |
19 | - Project starter: [https://github.com/AngularClass/angular2-webpack-starter](https://github.com/AngularClass/angular2-webpack-starter).
20 | - Useful reference for writing tests for Angular2 components: [https://github.com/juliemr/ng2-test-seed/blob/e2c833b3c8a2d58239534941ddea9fb1a76d7d26/src/test](https://github.com/juliemr/ng2-test-seed/blob/e2c833b3c8a2d58239534941ddea9fb1a76d7d26/src/test)
21 |
22 | # License
23 | [MIT](/LICENSE)
24 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // @AngularClass
2 | var path = require('path');
3 |
4 | module.exports = function(config) {
5 | var _config = {
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | // we are building the test environment in ./spec-bundle.js
19 | { pattern: 'spec-bundle.js', watched: false }
20 | ],
21 |
22 |
23 | // list of files to exclude
24 | exclude: [
25 | ],
26 |
27 |
28 | // preprocess matching files before serving them to the browser
29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
30 | preprocessors: {
31 | 'spec-bundle.js': ['webpack', 'sourcemap']
32 | // 'test/**/*.spec.ts': ['webpack', 'sourcemap']
33 | },
34 |
35 | webpack: {
36 |
37 | resolve: {
38 | cache: false,
39 | root: __dirname,
40 | extensions: ['','.ts','.js','.json', '.css', '.html'],
41 | alias: {
42 | 'app': 'src/app',
43 | 'common': 'src/common'
44 | }
45 | },
46 | devtool: 'inline-source-map',
47 | module: {
48 | loaders: [
49 | {
50 | test: /\.ts$/,
51 | loader: 'ts-loader',
52 | query: {
53 | 'ignoreDiagnostics': [
54 | 2403, // 2403 -> Subsequent variable declarations
55 | 2300, // 2300 Duplicate identifier
56 | 2374, // 2374 -> Duplicate number index signature
57 | 2375 // 2375 -> Duplicate string index signature
58 | ]
59 | },
60 | exclude: [ /\.e2e\.ts$/, /node_modules/ ]
61 | },
62 | { test: /\.json$/, loader: 'json-loader' },
63 | { test: /\.html$/, loader: 'raw-loader' },
64 | { test: /\.css$/, loader: 'raw-loader' }
65 | ],
66 | postLoaders: [
67 | // instrument only testing sources with Istanbul
68 | {
69 | test: /\.(js|ts)$/,
70 | include: path.resolve('src'),
71 | loader: 'istanbul-instrumenter-loader',
72 | exclude: [ /\.e2e\.ts$/, /node_modules/ ]
73 | }
74 | ]
75 | },
76 | stats: { colors: true, reasons: true },
77 | debug: false,
78 | noParse: [
79 | /zone\.js\/dist\/zone-microtask\.js/,
80 | /zone\.js\/dist\/long-stack-trace-zone\.js/,
81 | /zone\.js\/dist\/jasmine-patch\.js/
82 | ]
83 | },
84 |
85 | coverageReporter: {
86 | dir : 'coverage/',
87 | reporters: [
88 | { type: 'text-summary' },
89 | { type: 'html' }
90 | ],
91 | },
92 |
93 | webpackServer: {
94 | noInfo: true //please don't spam the console when running in karma!
95 | },
96 |
97 |
98 | // test results reporter to use
99 | // possible values: 'dots', 'progress'
100 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
101 | reporters: [ 'progress', 'coverage' ],
102 |
103 |
104 | // web server port
105 | port: 9876,
106 |
107 |
108 | // enable / disable colors in the output (reporters and logs)
109 | colors: true,
110 |
111 |
112 | // level of logging
113 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
114 | logLevel: config.LOG_INFO,
115 |
116 |
117 | // enable / disable watching file and executing tests whenever any file changes
118 | autoWatch: false,
119 |
120 |
121 | // start these browsers
122 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
123 | browsers: ['PhantomJS'],
124 |
125 |
126 | // Continuous Integration mode
127 | // if true, Karma captures browsers, runs the tests and exits
128 | singleRun: true
129 | };
130 |
131 | config.set(_config);
132 |
133 | };
134 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Angular2Bank",
3 | "version": "0.0.0",
4 | "description": "An example bank implementation written using Angular2.",
5 | "main": "",
6 | "engines": {
7 | "node": ">= 4.2.1 <= 5",
8 | "npm": ">= 3"
9 | },
10 | "scripts": {
11 | "clean": "rimraf node_modules doc typings && npm cache clean",
12 | "clean-install": "npm run clean && npm install",
13 | "clean-start": "npm run clean && npm start",
14 | "watch": "webpack --watch --progress --profile --colors --display-error-details --display-cached",
15 | "build": "webpack --progress --profile --colors --display-error-details --display-cached",
16 | "build:prod": "webpack --progress --profile --colors --display-error-details --display-cached --optimize-occurence-order --optimize-minimize --optimize-dedupe",
17 | "server": "webpack-dev-server --inline --progress --profile --colors --display-error-details --display-cached --port 3000",
18 | "webdriver-update": "webdriver-manager update",
19 | "webdriver-start": "webdriver-manager start",
20 | "lint": "tsconfig-lint",
21 | "e2e": "protractor",
22 | "test": "karma start",
23 | "ci": "npm run e2e && npm run test",
24 | "docs": "typedoc --options typedoc.json src/**/*.ts",
25 | "start": "npm run server",
26 | "postinstall": "typings install"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/ritterim/angular2-bank.git"
31 | },
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/ritterim/angular2-bank/issues"
35 | },
36 | "homepage": "https://github.com/ritterim/angular2-bank",
37 | "dependencies": {
38 | "alerts": "^0.1.3",
39 | "angular2": "2.0.0-beta.0",
40 | "es6-promise": "^3.0.2",
41 | "es6-shim": "^0.33.3",
42 | "es7-reflect-metadata": "^1.2.0",
43 | "rxjs": "5.0.0-beta.0",
44 | "zone.js": "0.5.10"
45 | },
46 | "devDependencies": {
47 | "css-loader": "^0.23.0",
48 | "exports-loader": "0.6.2",
49 | "expose-loader": "^0.7.1",
50 | "file-loader": "^0.8.4",
51 | "imports-loader": "^0.6.4",
52 | "istanbul-instrumenter-loader": "^0.1.3",
53 | "json-loader": "^0.5.3",
54 | "karma": "^0.13.11",
55 | "karma-chrome-launcher": "^0.2.1",
56 | "karma-coverage": "^0.5.2",
57 | "karma-jasmine": "^0.3.6",
58 | "karma-phantomjs-launcher": "^0.2.1",
59 | "karma-sourcemap-loader": "^0.3.6",
60 | "karma-webpack": "1.7.0",
61 | "phantomjs": "^1.9.18",
62 | "phantomjs-polyfill": "0.0.1",
63 | "protractor": "^3.0.0",
64 | "raw-loader": "0.5.1",
65 | "reflect-metadata": "0.1.2",
66 | "rimraf": "^2.4.4",
67 | "style-loader": "^0.13.0",
68 | "ts-loader": "^0.7.2",
69 | "tsconfig-lint": "^0.2.0",
70 | "tslint": "^3.2.0",
71 | "tslint-loader": "^2.1.0",
72 | "typedoc": "^0.3.12",
73 | "typescript": "^1.7.3",
74 | "typings": "^0.3.1",
75 | "url-loader": "^0.5.6",
76 | "webpack": "^1.12.9",
77 | "webpack-dev-server": "^1.12.1"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @AngularClass
2 |
3 | exports.config = {
4 | baseUrl: 'http://localhost:3000/',
5 |
6 | specs: [
7 | 'test/**/*.e2e.js'
8 | ],
9 | exclude: [],
10 |
11 | framework: 'jasmine',
12 |
13 | allScriptsTimeout: 110000,
14 |
15 | jasmineNodeOpts: {
16 | showTiming: true,
17 | showColors: true,
18 | isVerbose: false,
19 | includeStackTrace: false,
20 | defaultTimeoutInterval: 400000
21 | },
22 | directConnect: true,
23 |
24 | capabilities: {
25 | 'browserName': 'chrome',
26 | 'chromeOptions': {
27 | 'args': ['show-fps-counter=true']
28 | }
29 | },
30 |
31 | onPrepare: function() {
32 | browser.ignoreSynchronization = true;
33 | },
34 |
35 |
36 | /**
37 | * Angular 2 configuration
38 | *
39 | * useAllAngular2AppRoots: tells Protractor to wait for any angular2 apps on the page instead of just the one matching
40 | * `rootEl`
41 | *
42 | */
43 | useAllAngular2AppRoots: true
44 | };
45 |
--------------------------------------------------------------------------------
/spec-bundle.js:
--------------------------------------------------------------------------------
1 | // @AngularClass
2 | /*
3 | * When testing with webpack and ES6, we have to do some extra
4 | * things get testing to work right. Because we are gonna write test
5 | * in ES6 to, we have to compile those as well. That's handled in
6 | * karma.conf.js with the karma-webpack plugin. This is the entry
7 | * file for webpack test. Just like webpack will create a bundle.js
8 | * file for our client, when we run test, it well compile and bundle them
9 | * all here! Crazy huh. So we need to do some setup
10 | */
11 | Error.stackTraceLimit = Infinity;
12 | require('phantomjs-polyfill');
13 | require('es6-promise');
14 | require('es6-shim');
15 | require('es7-reflect-metadata/dist/browser');
16 | require('zone.js/lib/browser/zone-microtask.js');
17 | require('zone.js/lib/browser/long-stack-trace-zone.js');
18 | require('zone.js/lib/browser/jasmine-patch.js');
19 | // these are global EmitHelpers used by compiled typescript
20 | globalPolyfills()
21 |
22 | require('angular2/testing');
23 |
24 | /*
25 | Ok, this is kinda crazy. We can use the the context method on
26 | require that webpack created in order to tell webpack
27 | what files we actually want to require or import.
28 | Below, context will be an function/object with file names as keys.
29 | using that regex we are saying look in client/app and find
30 | any file that ends with spec.js and get its path. By passing in true
31 | we say do this recursively
32 | */
33 | var testContext = require.context('./test', true, /\.spec\.ts/);
34 | var appContext = require.context('./src', true, /\.spec\.ts/);
35 |
36 | // get all the files, for each file, call the context function
37 | // that will require the file and load it up here. Context will
38 | // loop and require those spec files here
39 | appContext.keys().forEach(appContext);
40 | testContext.keys().forEach(testContext);
41 |
42 | // Select BrowserDomAdapter.
43 | // see https://github.com/AngularClass/angular2-webpack-starter/issues/124
44 | var domAdapter = require('angular2/src/platform/browser/browser_adapter');
45 | domAdapter.BrowserDomAdapter.makeCurrent();
46 |
47 |
48 |
49 |
50 | // these are helpers that typescript uses
51 | // I manually added them by opting out of EmitHelpers by noEmitHelpers: false
52 | function globalPolyfills(){
53 | global.__extends = (this && this.__extends) || function (d, b) {
54 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
55 | var __ = function() { this.constructor = d; };
56 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
57 | };
58 |
59 | global.__decorate = global.Reflect.decorate;
60 | global.__metadata = global.Reflect.metadata;
61 |
62 | global.__param = (this && this.__param) || function (paramIndex, decorator) {
63 | return function (target, key) { decorator(target, key, paramIndex); };
64 | };
65 |
66 | global.__awaiter = (this && this.__awaiter) ||
67 | function (thisArg, _arguments, Promise, generator) {
68 | return new Promise(function (resolve, reject) {
69 | generator = generator.call(thisArg, _arguments);
70 | function cast(value) {
71 | return value instanceof Promise && value.constructor === Promise ?
72 | value : new Promise(function (resolve) { resolve(value); }); }
73 | function onfulfill(value) { try { step('next', value); } catch (e) { reject(e); } }
74 | function onreject(value) { try { step('throw', value); } catch (e) { reject(e); } }
75 | function step(verb, value) {
76 | var result = generator[verb](value);
77 | result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject);
78 | }
79 | step('next', void 0);
80 | });
81 | };
82 | }
--------------------------------------------------------------------------------
/src/UserNotifyingExceptionHandler.ts:
--------------------------------------------------------------------------------
1 | import { ExceptionHandler } from 'angular2/core';
2 | import { print } from 'angular2/src/facade/lang';
3 | let alert = require('alerts');
4 |
5 | export class UserNotifyingExceptionHandler extends ExceptionHandler {
6 | private alertTimeout = 5000;
7 | private transitionTime = 200; // Matches the css transition time
8 |
9 | constructor() {
10 | super(new PrintLogger(), /* _rethrowException: */ true);
11 | }
12 |
13 | call(error, stackTrace = null, reason = null) {
14 | let splitErrorMessage = error.message.split('\n');
15 | let errorMessageToDisplay = `${splitErrorMessage[0]}
${splitErrorMessage[1]}`;
16 |
17 | alert(errorMessageToDisplay, {
18 | timeout: this.alertTimeout,
19 | transitionTime: this.transitionTime
20 | });
21 |
22 | // Call the parent behavior (remove in production?)
23 | super.call(error, stackTrace, reason);
24 | }
25 | }
26 |
27 | /* tslint:disable:max-line-length no-empty */
28 | // https://github.com/angular/angular/blob/7ae23adaff2990cf6022af9792c449730d451d1d/modules/angular2/src/platform/worker_app_common.ts#L28-L33
29 | class PrintLogger {
30 | log = print;
31 | logError = print;
32 | logGroup = print;
33 | logGroupEnd() {}
34 | }
35 | /* tslint:enable */
36 |
--------------------------------------------------------------------------------
/src/account.spec.ts:
--------------------------------------------------------------------------------
1 | // Import necessary wrappers for Jasmine
2 | import {
3 | describe,
4 | expect,
5 | it
6 | } from 'angular2/testing';
7 |
8 | import {Account} from './account';
9 |
10 | describe('constructor', () => {
11 | it('should throw for missing id', () => {
12 | expect(() => new Account(undefined))
13 | .toThrowError('id must be provided.');
14 | });
15 |
16 | it('should allow an id of \'0\'', () => {
17 | let accountId = '0';
18 |
19 | let account = new Account(accountId);
20 |
21 | expect(account.id).toEqual(accountId);
22 | });
23 |
24 | it('should throw for negative initialBalance', () => {
25 | expect(() => new Account('account-1', -1))
26 | .toThrowError('initialBalance must not be negative.');
27 | });
28 |
29 | it('should throw for decimal initialBalance', () => {
30 | let initialBalance = 123.45;
31 |
32 | expect(() => new Account('account-1', initialBalance))
33 | .toThrowError(
34 | `The amount specified '${initialBalance}' must be an integer ` +
35 | '(decimals are not supported)');
36 | });
37 |
38 | it('should default balance to zero if initialBalance is not specified', () => {
39 | let account = new Account('account-1');
40 |
41 | expect(account.balance).toEqual(0);
42 | });
43 |
44 | it('should set balance if initialBalance is specified', () => {
45 | let initialBalance = 123;
46 |
47 | let account = new Account('account-1', initialBalance);
48 |
49 | expect(account.balance).toEqual(initialBalance);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/account.ts:
--------------------------------------------------------------------------------
1 | export class Account {
2 | public id: string;
3 | public balance: number;
4 |
5 | constructor(id: string, initialBalance = 0) {
6 | if (!id) {
7 | throw new Error('id must be provided.');
8 | }
9 |
10 | if (initialBalance < 0) {
11 | throw new Error('initialBalance must not be negative.');
12 | }
13 |
14 | if (!this.isInteger(initialBalance)) {
15 | throw new Error(
16 | `The amount specified '${initialBalance}' must be an integer ` +
17 | '(decimals are not supported)');
18 | }
19 |
20 | this.id = id;
21 | this.balance = initialBalance;
22 | }
23 |
24 | /* tslint:disable:quotemark max-line-length */
25 | private isInteger(value: number) {
26 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
27 | return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
28 | }
29 | /* tslint: enable */
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/app.css:
--------------------------------------------------------------------------------
1 | .bank-no-control-header {
2 | padding-left: 15px;
3 | }
4 | .bank-narrow-footer {
5 | padding-top: 0;
6 | padding-bottom: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/app.html:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/src/app/app.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | describe,
3 | it,
4 | inject,
5 | injectAsync,
6 | beforeEachProviders,
7 | TestComponentBuilder
8 | } from 'angular2/testing';
9 |
10 | import {Component, provide} from 'angular2/core';
11 | import {BaseRequestOptions, Http} from 'angular2/http';
12 | import {MockBackend} from 'angular2/http/testing';
13 |
14 | import {Bank} from '../bank';
15 |
16 | // Load the implementations that should be tested
17 | import {App} from './app';
18 |
19 | describe('App', () => {
20 | // provide our implementations or mocks to the dependency injector
21 | beforeEachProviders(() => [
22 | App,
23 | Bank,
24 | BaseRequestOptions,
25 | MockBackend,
26 | provide(Http, {
27 | useFactory: function(backend, defaultOptions) {
28 | return new Http(backend, defaultOptions);
29 | },
30 | deps: [MockBackend, BaseRequestOptions]})
31 | ]);
32 |
33 | it('should log ngOnInit', inject([ App ], (app) => {
34 | spyOn(console, 'log');
35 | expect(console.log).not.toHaveBeenCalled();
36 |
37 | app.ngOnInit();
38 | expect(console.log).toHaveBeenCalled();
39 | }));
40 |
41 | it('should create initial accounts', inject([ App ], (app) => {
42 | spyOn(app.bank, 'openAccount');
43 | expect(app.bank.openAccount).not.toHaveBeenCalled();
44 |
45 | app.ngAfterViewInit();
46 |
47 | expect(app.bank.openAccount).toHaveBeenCalledWith('account-1', 123);
48 | expect(app.bank.openAccount).toHaveBeenCalledWith('account-2', 234);
49 | expect(app.bank.openAccount.calls.count()).toEqual(2);
50 | }));
51 | });
52 |
--------------------------------------------------------------------------------
/src/app/app.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, OnInit} from 'angular2/core';
2 | import {CORE_DIRECTIVES, FORM_DIRECTIVES } from 'angular2/common';
3 |
4 | import {Bank} from '../bank';
5 | import {AccountOperationsComponent} from '../components/AccountOperationsComponent';
6 | import {ShowBalancesComponent} from '../components/ShowBalancesComponent';
7 |
8 | @Component({
9 | directives: [
10 | CORE_DIRECTIVES,
11 | FORM_DIRECTIVES,
12 | AccountOperationsComponent,
13 | ShowBalancesComponent
14 | ],
15 | pipes: [],
16 | providers: [ Bank ],
17 | selector: 'app',
18 | styles: [ require('./app.css') ],
19 | template: require('./app.html')
20 | })
21 | export class App implements OnInit, AfterViewInit {
22 | constructor(private bank: Bank) {
23 | }
24 |
25 | ngOnInit() {
26 | console.log('hello App');
27 | }
28 |
29 | ngAfterViewInit() {
30 | this.bank.openAccount('account-1', 123);
31 | this.bank.openAccount('account-2', 234);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/bank.spec.ts:
--------------------------------------------------------------------------------
1 | // Import necessary wrappers for Jasmine
2 | import {
3 | beforeEachProviders,
4 | describe,
5 | expect,
6 | it
7 | } from 'angular2/testing';
8 |
9 | import {Bank} from './bank';
10 |
11 | let bank: Bank;
12 | let accountId = 'account-1';
13 |
14 | describe('clear', () => {
15 | beforeEachProviders(() => {
16 | Bank.clear();
17 | bank = new Bank().openAccount(accountId);
18 | });
19 |
20 | it('should clear all accounts', () => {
21 | Bank.clear();
22 |
23 | expect(bank.getAllAccounts().length).toEqual(0);
24 | });
25 | });
26 |
27 | describe('openAccount', () => {
28 | beforeEachProviders(() => {
29 | Bank.clear();
30 | bank = new Bank().openAccount(accountId);
31 | });
32 |
33 | it('should error if account already exists', () => {
34 | expect(() => bank.openAccount(accountId, 0))
35 | .toThrowError(`An account with id of \'${accountId}\' already exists.`);
36 | });
37 |
38 | it('should error if initialBalance is negative', () => {
39 | expect(() => bank.openAccount('new-account', -1))
40 | .toThrowError('The initialBalance of \'-1\' must not be negative.');
41 | });
42 |
43 | it('should error if initialBalance is a decimal number', () => {
44 | let initialBalance = 123.45;
45 |
46 | expect(() => bank.openAccount('new-account', initialBalance))
47 | .toThrowError(
48 | `The amount specified '${initialBalance}' must be an integer ` +
49 | '(decimals are not supported).');
50 | });
51 |
52 | it('should open account with correct balance', () => {
53 | let initialBalance = 123;
54 | bank.openAccount('account-2', initialBalance);
55 |
56 | let account2 = bank.getAllAccounts()[1];
57 |
58 | expect(account2.id).toEqual('account-2');
59 | expect(account2.balance).toEqual(initialBalance);
60 | });
61 |
62 | it('should emit to accountUpdates', () => {
63 | spyOn(Bank.accountUpdates, 'emit');
64 |
65 | bank.openAccount('account-2');
66 |
67 | expect(Bank.accountUpdates.emit).toHaveBeenCalledWith({
68 | operation: 'openAccount',
69 | accountId: 'account-2',
70 | initialBalance: 0
71 | });
72 | });
73 | });
74 |
75 | describe('closeAccount', () => {
76 | beforeEachProviders(() => {
77 | Bank.clear();
78 | bank = new Bank().openAccount(accountId);
79 | });
80 |
81 | it('should error if account does not exist', () => {
82 | let accountIdDoesNotExist = 'does-not-exist';
83 |
84 | expect(() => bank.closeAccount(accountIdDoesNotExist))
85 | .toThrowError(`There was no account with id of '${accountIdDoesNotExist}'.`);
86 | });
87 |
88 | it('should error if account balance is not zero', () => {
89 | let deposit = 123;
90 | bank.deposit(accountId, deposit);
91 |
92 | expect(() => bank.closeAccount(accountId))
93 | .toThrowError(`The account balance must be zero (it is currently \'${deposit}\').`);
94 | });
95 |
96 | it('should remove the account from the list of accounts', () => {
97 | bank.closeAccount(accountId);
98 |
99 | expect(bank.getAllAccounts().length).toEqual(0);
100 | });
101 |
102 | it('should emit to accountUpdates', () => {
103 | spyOn(Bank.accountUpdates, 'emit');
104 |
105 | bank.closeAccount(accountId);
106 |
107 | expect(Bank.accountUpdates.emit).toHaveBeenCalledWith({
108 | operation: 'closeAccount',
109 | accountId: accountId
110 | });
111 | });
112 | });
113 |
114 | describe('deposit', () => {
115 | beforeEachProviders(() => {
116 | Bank.clear();
117 | bank = new Bank().openAccount(accountId);
118 | });
119 |
120 | it('should error if account does not exist', () => {
121 | let accountIdDoesNotExist = 'does-not-exist';
122 |
123 | expect(() => bank.deposit(accountIdDoesNotExist, 0))
124 | .toThrowError(`There was no account with id of '${accountIdDoesNotExist}'.`);
125 | });
126 |
127 | it('should error for missing amount', () => {
128 | expect(() => bank.deposit(accountId, undefined))
129 | .toThrowError('amount must be specified.');
130 | });
131 |
132 | it('should error for negative amount', () => {
133 | let amount = -1;
134 |
135 | expect(() => bank.deposit(accountId, amount))
136 | .toThrowError(`The amount specified '${amount}' must not be negative.`);
137 | });
138 |
139 | it('should error for decimal amount', () => {
140 | let amount = 123.45;
141 |
142 | expect(() => bank.deposit(accountId, amount))
143 | .toThrowError(
144 | `The amount specified '${amount}' must be an integer ` +
145 | '(decimals are not supported)');
146 | });
147 |
148 | it('should keep same balance for zero amount', () => {
149 | let startingBalance = bank.getBalance(accountId);
150 |
151 | bank.deposit(accountId, 0);
152 |
153 | expect(bank.getBalance(accountId)).toEqual(startingBalance);
154 | });
155 |
156 | it('should add amount to account balance', () => {
157 | let amount = 123;
158 | let startingBalance = bank.getBalance(accountId);
159 |
160 | bank.deposit(accountId, amount);
161 |
162 | expect(bank.getBalance(accountId)).toEqual(startingBalance + amount);
163 | });
164 |
165 | it('should emit to accountUpdates', () => {
166 | let amount = 123;
167 | spyOn(Bank.accountUpdates, 'emit');
168 |
169 | bank.deposit(accountId, amount);
170 |
171 | expect(Bank.accountUpdates.emit).toHaveBeenCalledWith({
172 | operation: 'deposit',
173 | accountId: accountId,
174 | amount: amount
175 | });
176 | });
177 | });
178 |
179 | describe('withdraw', () => {
180 | beforeEachProviders(() => {
181 | Bank.clear();
182 | bank = new Bank().openAccount(accountId);
183 | });
184 |
185 | it('should error if account does not exist', () => {
186 | let accountIdDoesNotExist = 'does-not-exist';
187 |
188 | expect(() => bank.withdraw(accountIdDoesNotExist, 0))
189 | .toThrowError(`There was no account with id of '${accountIdDoesNotExist}'.`);
190 | });
191 |
192 | it('should error for missing amount', () => {
193 | expect(() => bank.withdraw(accountId, undefined))
194 | .toThrowError('amount must be specified.');
195 | });
196 |
197 | it('should error for negative amount', () => {
198 | let amount = -1;
199 |
200 | expect(() => bank.withdraw(accountId, amount))
201 | .toThrowError(`The amount specified '${amount}' must not be negative.`);
202 | });
203 |
204 | it('should error for decimal amount', () => {
205 | let amount = 123.45;
206 |
207 | expect(() => bank.withdraw(accountId, amount))
208 | .toThrowError(
209 | `The amount specified '${amount}' must be an integer ` +
210 | '(decimals are not supported)');
211 | });
212 |
213 | it('should error if insufficient funds', () => {
214 | let amount = 1;
215 | let startingBalance = bank.getBalance(accountId);
216 |
217 | expect(() => bank.withdraw(accountId, startingBalance + amount))
218 | .toThrowError(
219 | `The requested withdraw of '${amount}' cannot be completed, ` +
220 | `there is only '${startingBalance}' available in this account.`);
221 | });
222 |
223 | it('should keep same balance for zero amount', () => {
224 | let startingBalance = bank.getBalance(accountId);
225 |
226 | bank.withdraw(accountId, 0);
227 |
228 | expect(bank.getBalance(accountId)).toEqual(startingBalance);
229 | });
230 |
231 | it('should deduct amount from account balance', () => {
232 | let amount = 123;
233 | let startingBalance = bank.getBalance(accountId);
234 | bank.deposit(accountId, amount);
235 |
236 | bank.withdraw(accountId, amount);
237 |
238 | expect(bank.getBalance(accountId)).toEqual(startingBalance);
239 | });
240 |
241 | it('should emit to accountUpdates', () => {
242 | let amount = 123;
243 | bank.deposit(accountId, amount);
244 | spyOn(Bank.accountUpdates, 'emit');
245 |
246 | bank.withdraw(accountId, amount);
247 |
248 | expect(Bank.accountUpdates.emit).toHaveBeenCalledWith({
249 | operation: 'withdraw',
250 | accountId: accountId,
251 | amount: amount
252 | });
253 | });
254 | });
255 |
256 | describe('transfer', () => {
257 | let account2Id = 'account-2';
258 |
259 | beforeEachProviders(() => {
260 | Bank.clear();
261 | bank = new Bank().openAccount(accountId);
262 |
263 | bank.openAccount(account2Id, 0);
264 | });
265 |
266 | it('should error if \'from\' account does not exist', () => {
267 | let accountIdDoesNotExist = 'does-not-exist';
268 |
269 | expect(() => bank.transfer(accountIdDoesNotExist, account2Id, 0))
270 | .toThrowError(`There was no account with id of '${accountIdDoesNotExist}'.`);
271 | });
272 |
273 | it('should error if \'to\' account does not exist', () => {
274 | let accountIdDoesNotExist = 'does-not-exist';
275 |
276 | expect(() => bank.transfer(accountId, accountIdDoesNotExist, 0))
277 | .toThrowError(`There was no account with id of '${accountIdDoesNotExist}'.`);
278 | });
279 |
280 | it('should error if \'from\' account and \'to\' account are the same account', () => {
281 | expect(() => bank.transfer(accountId, accountId, 123))
282 | .toThrowError('fromAccountId and toAccountId must not be the same account.');
283 | });
284 |
285 | it('should error for missing amount', () => {
286 | expect(() => bank.transfer(accountId, account2Id, undefined))
287 | .toThrowError('amount must be specified.');
288 | });
289 |
290 | it('should error for negative amount', () => {
291 | let amount = -1;
292 |
293 | expect(() => bank.transfer(accountId, account2Id, amount))
294 | .toThrowError(`The amount specified '${amount}' must not be negative.`);
295 | });
296 |
297 | it('should error for decimal amount', () => {
298 | let amount = 123.45;
299 |
300 | expect(() => bank.transfer(accountId, account2Id, amount))
301 | .toThrowError(
302 | `The amount specified '${amount}' must be an integer ` +
303 | '(decimals are not supported)');
304 | });
305 |
306 | it('should error if insufficient funds', () => {
307 | let amount = 123;
308 |
309 | let fromBalance = bank.getBalance(accountId);
310 |
311 | expect(() => bank.transfer(accountId, account2Id, amount))
312 | .toThrowError(
313 | `The requested withdraw of '${amount}' cannot be completed, ` +
314 | `there is only '${fromBalance}' available in account '${accountId}'.`);
315 | });
316 |
317 | it('should keep same \'from\' balance for zero amount', () => {
318 | let fromStartingBalance = bank.getBalance(accountId);
319 |
320 | bank.transfer(accountId, account2Id, 0);
321 |
322 | expect(bank.getBalance(accountId)).toEqual(fromStartingBalance);
323 | });
324 |
325 | it('should keep same \'to\' balance for zero amount', () => {
326 | let toStartingBalance = bank.getBalance(account2Id);
327 |
328 | bank.transfer(accountId, account2Id, 0);
329 |
330 | expect(bank.getBalance(account2Id)).toEqual(toStartingBalance);
331 | });
332 |
333 | it('should deduct amount from \'from\' account', () => {
334 | let amount = 123;
335 | bank.deposit(accountId, amount);
336 |
337 | bank.transfer(accountId, account2Id, amount);
338 |
339 | expect(bank.getBalance(accountId)).toEqual(0);
340 | });
341 |
342 | it('should add amount to \'to\' account', () => {
343 | let amount = 123;
344 | bank.deposit(accountId, amount);
345 |
346 | bank.transfer(accountId, account2Id, amount);
347 |
348 | expect(bank.getBalance(account2Id)).toEqual(amount);
349 | });
350 |
351 | it('should emit to accountUpdates', () => {
352 | let amount = 123;
353 | bank.deposit(accountId, amount);
354 | spyOn(Bank.accountUpdates, 'emit');
355 |
356 | bank.transfer(accountId, account2Id, amount);
357 |
358 | expect(Bank.accountUpdates.emit).toHaveBeenCalledWith({
359 | operation: 'transfer',
360 | fromAccountId: accountId,
361 | toAccountId: account2Id,
362 | amount: amount
363 | });
364 | });
365 | });
366 |
367 | describe('getBalance', () => {
368 | beforeEachProviders(() => {
369 | Bank.clear();
370 | bank = new Bank().openAccount(accountId);
371 | });
372 |
373 | it('should error if account does not exist', () => {
374 | let accountIdDoesNotExist = 'does-not-exist';
375 |
376 | expect(() => bank.getBalance(accountIdDoesNotExist))
377 | .toThrowError(`There was no account with id of '${accountIdDoesNotExist}'.`);
378 | });
379 |
380 | it('should return expected balance', () => {
381 | let amount = 123;
382 | bank.deposit(accountId, amount);
383 |
384 | expect(bank.getBalance(accountId)).toEqual(amount);
385 | });
386 | });
387 |
388 | describe('getAllAccounts', () => {
389 | beforeEachProviders(() => {
390 | Bank.clear();
391 | bank = new Bank().openAccount(accountId);
392 | });
393 |
394 | it('should return the complete collection of accounts', () => {
395 | bank.openAccount('account-2', 0);
396 |
397 | expect(bank.getAllAccounts().length).toEqual(2);
398 | });
399 | });
400 |
401 | describe('getTotalBankCurrency', () => {
402 | beforeEachProviders(() => {
403 | Bank.clear();
404 | bank = new Bank().openAccount(accountId);
405 | });
406 |
407 | it('should return a total of all bank held currency', () => {
408 | let amount1 = 123;
409 | let amount2 = 234;
410 |
411 | bank.deposit(accountId, amount1);
412 | bank.openAccount('account-2', amount2);
413 |
414 | expect(bank.getTotalBankCurrency()).toEqual(amount1 + amount2);
415 | });
416 | });
417 |
--------------------------------------------------------------------------------
/src/bank.ts:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'angular2/core';
2 | import {Account} from './account';
3 |
4 | export class Bank {
5 | public static accountUpdates: EventEmitter = new EventEmitter();
6 |
7 | private static accounts: Account[] = new Array();
8 |
9 | public static clear() : void {
10 | Bank.accounts.length = 0;
11 | }
12 |
13 | public openAccount(accountId: string, initialBalance = 0) : Bank {
14 | let account = this.getAccount(accountId, /* errorIfDoesNotExist */ false);
15 |
16 | if (account) {
17 | throw new Error(`An account with id of '${accountId}' already exists.`);
18 | }
19 |
20 | if (initialBalance < 0) {
21 | throw new Error(`The initialBalance of '${initialBalance}' must not be negative.`);
22 | }
23 |
24 | if (!this.isInteger(initialBalance)) {
25 | throw new Error(
26 | `The amount specified '${initialBalance}' must be an integer ` +
27 | '(decimals are not supported).');
28 | }
29 |
30 | Bank.accounts.push(new Account(accountId, initialBalance));
31 |
32 | Bank.accountUpdates.emit({
33 | operation: 'openAccount',
34 | accountId: accountId,
35 | initialBalance: initialBalance
36 | });
37 |
38 | return this;
39 | }
40 |
41 | public closeAccount(accountId: string) : Bank {
42 | let account = this.getAccount(accountId);
43 |
44 | if (account.balance !== 0) {
45 | throw new Error(`The account balance must be zero (it is currently '${account.balance}').`);
46 | }
47 |
48 | Bank.accounts = Bank.accounts.filter(x => x.id !== accountId);
49 |
50 | Bank.accountUpdates.emit({
51 | operation: 'closeAccount',
52 | accountId: accountId
53 | });
54 |
55 | return this;
56 | }
57 |
58 | public deposit(accountId: string, amount: number) : Bank {
59 | let account = this.getAccount(accountId);
60 |
61 | if (!amount && amount !== 0) {
62 | throw new Error('amount must be specified.');
63 | }
64 |
65 | if (amount < 0) {
66 | throw new Error(`The amount specified '${amount}' must not be negative.`);
67 | }
68 |
69 | if (!this.isInteger(amount)) {
70 | throw new Error(
71 | `The amount specified '${amount}' must be an integer ` +
72 | '(decimals are not supported)');
73 | }
74 |
75 | account.balance += amount;
76 |
77 | Bank.accountUpdates.emit({
78 | operation: 'deposit',
79 | accountId: accountId,
80 | amount: amount
81 | });
82 |
83 | return this;
84 | }
85 |
86 | public withdraw(accountId: string, amount: number) : Bank {
87 | let account = this.getAccount(accountId);
88 |
89 | if (!amount && amount !== 0) {
90 | throw new Error('amount must be specified.');
91 | }
92 |
93 | if (amount < 0) {
94 | throw new Error(`The amount specified '${amount}' must not be negative.`);
95 | }
96 |
97 | if (!this.isInteger(amount)) {
98 | throw new Error(
99 | `The amount specified '${amount}' must be an integer ` +
100 | '(decimals are not supported)');
101 | }
102 |
103 | if (account.balance < amount) {
104 | throw new Error(
105 | `The requested withdraw of '${amount}' cannot be completed, ` +
106 | `there is only '${account.balance}' available in this account.`);
107 | }
108 |
109 | account.balance -= amount;
110 |
111 | Bank.accountUpdates.emit({
112 | operation: 'withdraw',
113 | accountId: accountId,
114 | amount: amount
115 | });
116 |
117 | return this;
118 | }
119 |
120 | public transfer(fromAccountId: string, toAccountId: string, amount: number) : Bank {
121 | let fromAccount = this.getAccount(fromAccountId);
122 | let toAccount = this.getAccount(toAccountId);
123 |
124 | if (fromAccountId === toAccountId) {
125 | throw new Error('fromAccountId and toAccountId must not be the same account.');
126 | }
127 |
128 | if (!amount && amount !== 0) {
129 | throw new Error('amount must be specified.');
130 | }
131 |
132 | if (amount < 0) {
133 | throw new Error(`The amount specified '${amount}' must not be negative.`);
134 | }
135 |
136 | if (!this.isInteger(amount)) {
137 | throw new Error(
138 | `The amount specified '${amount}' must be an integer ` +
139 | '(decimals are not supported)');
140 | }
141 |
142 | if (fromAccount.balance < amount) {
143 | throw new Error(
144 | `The requested withdraw of '${amount}' cannot be completed, ` +
145 | `there is only '${fromAccount.balance}' available in account '${fromAccountId}'.`);
146 | }
147 |
148 | fromAccount.balance -= amount;
149 | toAccount.balance += amount;
150 |
151 | Bank.accountUpdates.emit({
152 | operation: 'transfer',
153 | fromAccountId: fromAccountId,
154 | toAccountId: toAccountId,
155 | amount: amount
156 | });
157 |
158 | return this;
159 | }
160 |
161 | public getBalance(accountId: string) : number {
162 | let account = this.getAccount(accountId);
163 |
164 | return account.balance;
165 | }
166 |
167 | public getAllAccounts() : Account[] {
168 | return Bank.accounts;
169 | }
170 |
171 | public getTotalBankCurrency() : number {
172 | return Bank.accounts.map(x => x.balance).reduce((x, y) => x + y);
173 | }
174 |
175 | private getAccount(accountId: string, errorIfDoesNotExist = true) : Account {
176 | let matchedAccounts = Bank.accounts.filter(x => x.id === accountId);
177 |
178 | if (matchedAccounts.length === 0) {
179 | if (errorIfDoesNotExist) {
180 | throw new Error(`There was no account with id of '${accountId}'.`);
181 | }
182 |
183 | return null;
184 | }
185 |
186 | if (matchedAccounts.length > 1) {
187 | throw new Error(`There is more than one account with an id of '${accountId}'.`);
188 | }
189 |
190 | return matchedAccounts[0];
191 | }
192 |
193 | /* tslint:disable:quotemark max-line-length */
194 | private isInteger(value: number) {
195 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
196 | return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
197 | }
198 | /* tslint:enable */
199 | }
200 |
--------------------------------------------------------------------------------
/src/bootstrap.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Providers provided by Angular
3 | */
4 | import {bootstrap} from 'angular2/platform/browser';
5 | import {ROUTER_PROVIDERS} from 'angular2/router';
6 | import {HTTP_PROVIDERS} from 'angular2/http';
7 | // include for development builds
8 | import {ELEMENT_PROBE_PROVIDERS} from 'angular2/platform/common_dom';
9 | // include for production builds
10 | // import {enableProdMode} from 'angular2/core';
11 |
12 | import {ExceptionHandler, provide} from 'angular2/core';
13 | import {UserNotifyingExceptionHandler} from './UserNotifyingExceptionHandler';
14 |
15 | /*
16 | * App Component
17 | * our top level component that holds all of our components
18 | */
19 | import {App} from './app/app';
20 |
21 | /*
22 | * Bootstrap our Angular app with a top level component `App` and inject
23 | * our Services and Providers into Angular's dependency injection
24 | */
25 | // enableProdMode() // include for production builds
26 | function main() {
27 | return bootstrap(App, [
28 | // This paves over the default ExceptionHandler.
29 | provide(ExceptionHandler, {useClass: UserNotifyingExceptionHandler}),
30 |
31 | // These are dependencies of our App
32 | HTTP_PROVIDERS,
33 | ROUTER_PROVIDERS,
34 | ELEMENT_PROBE_PROVIDERS // remove in production
35 | ])
36 | .catch(err => console.error(err));
37 | }
38 |
39 | document.addEventListener('DOMContentLoaded', main);
40 |
--------------------------------------------------------------------------------
/src/components/AccountOperationsComponent.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
17 |
18 | Amount must be a whole number.
19 |
20 |
21 |
22 |
23 |
29 |
30 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
72 |
--------------------------------------------------------------------------------
/src/components/AccountOperationsComponent.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | beforeEachProviders,
3 | describe,
4 | expect,
5 | injectAsync,
6 | it,
7 | TestComponentBuilder
8 | } from 'angular2/testing';
9 |
10 | import {AccountOperationsComponent} from './AccountOperationsComponent';
11 | import {Bank} from '../bank';
12 |
13 | let accountId = 'account-1';
14 | let amount = 123;
15 |
16 | let bank: Bank;
17 | let component: AccountOperationsComponent;
18 |
19 | beforeEachProviders(() => {
20 | Bank.clear();
21 | bank = new Bank();
22 | component = new AccountOperationsComponent(bank);
23 | });
24 |
25 | function getButton(compiled: any, innerText: string) : HTMLButtonElement {
26 | // http://stackoverflow.com/a/222847
27 | var buttons = [].slice.call(compiled.querySelectorAll('button'))
28 | .filter(x => x.innerText === innerText);
29 |
30 | if (buttons.length === 0) {
31 | throw new Error(`No buttons were found with innerText: '${innerText}'.`);
32 | }
33 |
34 | if (buttons.length > 1) {
35 | throw new Error(
36 | `More than one button (${buttons.length}) was found with innerText: '${innerText}'.`);
37 | }
38 |
39 | return buttons[0];
40 | };
41 |
42 | describe('Open Account button', () => {
43 | it('should be disabled when accountId is not provided', injectAsync([TestComponentBuilder], (tcb) => {
44 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
45 | fixture.detectChanges();
46 |
47 | let compiled = fixture.debugElement.nativeElement;
48 |
49 | let openAccountButton = getButton(compiled, 'Open Account');
50 |
51 | expect(openAccountButton.hasAttribute('disabled')).toEqual(true);
52 | });
53 | }));
54 |
55 | it('should be disabled if accountId already exists', injectAsync([TestComponentBuilder], (tcb) => {
56 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
57 | fixture.detectChanges();
58 |
59 | let component = fixture.debugElement.componentInstance;
60 | component._bank.openAccount(accountId);
61 | component.accountId = accountId;
62 |
63 | fixture.detectChanges();
64 |
65 | let compiled = fixture.debugElement.nativeElement;
66 | let openAccountButton = getButton(compiled, 'Open Account');
67 |
68 | expect(openAccountButton.hasAttribute('disabled')).toEqual(true);
69 | });
70 | }));
71 |
72 | it('should be enabled when conditions satisfied', injectAsync([TestComponentBuilder], (tcb) => {
73 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
74 | fixture.detectChanges();
75 |
76 | let component = fixture.debugElement.componentInstance;
77 | component._bank.openAccount(accountId);
78 | component.accountId = 'account-2';
79 |
80 | fixture.detectChanges();
81 |
82 | let compiled = fixture.debugElement.nativeElement;
83 | let openAccountButton = getButton(compiled, 'Open Account');
84 |
85 | expect(openAccountButton.hasAttribute('disabled')).toEqual(false);
86 | });
87 | }));
88 | });
89 |
90 | describe('Close Account button', () => {
91 | it('should be disabled when accountId is not provided', injectAsync([TestComponentBuilder], (tcb) => {
92 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
93 | fixture.detectChanges();
94 |
95 | let compiled = fixture.debugElement.nativeElement;
96 |
97 | let closeAccountButton = getButton(compiled, 'Close Account');
98 |
99 | expect(closeAccountButton.hasAttribute('disabled')).toEqual(true);
100 | });
101 | }));
102 |
103 | it('should be disabled if accountId does not exist', injectAsync([TestComponentBuilder], (tcb) => {
104 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
105 | fixture.detectChanges();
106 |
107 | let component = fixture.debugElement.componentInstance;
108 | component.accountId = accountId;
109 |
110 | fixture.detectChanges();
111 |
112 | let compiled = fixture.debugElement.nativeElement;
113 | let closeAccountButton = getButton(compiled, 'Close Account');
114 |
115 | expect(closeAccountButton.hasAttribute('disabled')).toEqual(true);
116 | });
117 | }));
118 |
119 | it('should be disabled if accountId does not have a zero balance', injectAsync([TestComponentBuilder], (tcb) => {
120 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
121 | fixture.detectChanges();
122 |
123 | let component = fixture.debugElement.componentInstance;
124 | component._bank.openAccount(accountId, 123);
125 | component.accountId = accountId;
126 |
127 | fixture.detectChanges();
128 |
129 | let compiled = fixture.debugElement.nativeElement;
130 | let closeAccountButton = getButton(compiled, 'Close Account');
131 |
132 | expect(closeAccountButton.hasAttribute('disabled')).toEqual(true);
133 | });
134 | }));
135 |
136 | it('should be enabled when conditions satisfied', injectAsync([TestComponentBuilder], (tcb) => {
137 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
138 | fixture.detectChanges();
139 |
140 | let component = fixture.debugElement.componentInstance;
141 | component._bank.openAccount(accountId);
142 | component.accountId = accountId;
143 |
144 | fixture.detectChanges();
145 |
146 | let compiled = fixture.debugElement.nativeElement;
147 | let closeAccountButton = getButton(compiled, 'Close Account');
148 |
149 | expect(closeAccountButton.hasAttribute('disabled')).toEqual(false);
150 | });
151 | }));
152 | });
153 |
154 | describe('Deposit button', () => {
155 | it('should be disabled when accountId is not provided', injectAsync([TestComponentBuilder], (tcb) => {
156 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
157 | fixture.detectChanges();
158 |
159 | let compiled = fixture.debugElement.nativeElement;
160 |
161 | let depositButton = getButton(compiled, 'Deposit');
162 |
163 | expect(depositButton.hasAttribute('disabled')).toEqual(true);
164 | });
165 | }));
166 |
167 | it('should be disabled if amount is not provided', injectAsync([TestComponentBuilder], (tcb) => {
168 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
169 | fixture.detectChanges();
170 |
171 | let component = fixture.debugElement.componentInstance;
172 | component._bank.openAccount(accountId);
173 | component.accountId = accountId;
174 |
175 | fixture.detectChanges();
176 |
177 | let compiled = fixture.debugElement.nativeElement;
178 | let depositButton = getButton(compiled, 'Deposit');
179 |
180 | expect(depositButton.hasAttribute('disabled')).toEqual(true);
181 | });
182 | }));
183 |
184 | it('should be disabled if amount is negative', injectAsync([TestComponentBuilder], (tcb) => {
185 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
186 | fixture.detectChanges();
187 |
188 | let component = fixture.debugElement.componentInstance;
189 | component._bank.openAccount(accountId);
190 | component.accountId = accountId;
191 | component.amount = -1;
192 |
193 | fixture.detectChanges();
194 |
195 | let compiled = fixture.debugElement.nativeElement;
196 | let depositButton = getButton(compiled, 'Deposit');
197 |
198 | expect(depositButton.hasAttribute('disabled')).toEqual(true);
199 | });
200 | }));
201 |
202 | it('should be disabled if accountId does not exist', injectAsync([TestComponentBuilder], (tcb) => {
203 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
204 | fixture.detectChanges();
205 |
206 | let component = fixture.debugElement.componentInstance;
207 | component.accountId = accountId;
208 | component.amount = 1;
209 |
210 | fixture.detectChanges();
211 |
212 | let compiled = fixture.debugElement.nativeElement;
213 | let depositButton = getButton(compiled, 'Deposit');
214 |
215 | expect(depositButton.hasAttribute('disabled')).toEqual(true);
216 | });
217 | }));
218 |
219 | it('should be enabled when conditions satisfied', injectAsync([TestComponentBuilder], (tcb) => {
220 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
221 | fixture.detectChanges();
222 |
223 | let component = fixture.debugElement.componentInstance;
224 | component._bank.openAccount(accountId);
225 | component.accountId = accountId;
226 | component.amount = 1;
227 |
228 | fixture.detectChanges();
229 |
230 | let compiled = fixture.debugElement.nativeElement;
231 | let depositButton = getButton(compiled, 'Deposit');
232 |
233 | expect(depositButton.hasAttribute('disabled')).toEqual(false);
234 | });
235 | }));
236 | });
237 |
238 | describe('Withdraw button', () => {
239 | it('should be disabled when accountId is not provided', injectAsync([TestComponentBuilder], (tcb) => {
240 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
241 | fixture.detectChanges();
242 |
243 | let compiled = fixture.debugElement.nativeElement;
244 |
245 | let withdrawButton = getButton(compiled, 'Withdraw');
246 |
247 | expect(withdrawButton.hasAttribute('disabled')).toEqual(true);
248 | });
249 | }));
250 |
251 | it('should be disabled if accountId does not exist', injectAsync([TestComponentBuilder], (tcb) => {
252 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
253 | fixture.detectChanges();
254 |
255 | let component = fixture.debugElement.componentInstance;
256 | component.accountId = accountId;
257 | component.amount = 1;
258 |
259 | fixture.detectChanges();
260 |
261 | let compiled = fixture.debugElement.nativeElement;
262 | let withdrawButton = getButton(compiled, 'Withdraw');
263 |
264 | expect(withdrawButton.hasAttribute('disabled')).toEqual(true);
265 | });
266 | }));
267 |
268 | it('should be disabled if amount is not provided', injectAsync([TestComponentBuilder], (tcb) => {
269 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
270 | fixture.detectChanges();
271 |
272 | let component = fixture.debugElement.componentInstance;
273 | component._bank.openAccount(accountId, 1);
274 | component.accountId = accountId;
275 |
276 | fixture.detectChanges();
277 |
278 | let compiled = fixture.debugElement.nativeElement;
279 | let withdrawButton = getButton(compiled, 'Withdraw');
280 |
281 | expect(withdrawButton.hasAttribute('disabled')).toEqual(true);
282 | });
283 | }));
284 |
285 | it('should be disabled if amount is negative', injectAsync([TestComponentBuilder], (tcb) => {
286 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
287 | fixture.detectChanges();
288 |
289 | let component = fixture.debugElement.componentInstance;
290 | component._bank.openAccount(accountId, 1);
291 | component.accountId = accountId;
292 | component.amount = -1;
293 |
294 | fixture.detectChanges();
295 |
296 | let compiled = fixture.debugElement.nativeElement;
297 | let withdrawButton = getButton(compiled, 'Withdraw');
298 |
299 | expect(withdrawButton.hasAttribute('disabled')).toEqual(true);
300 | });
301 | }));
302 |
303 | it('should be disabled if account does not have enough funds to complete withdraw', injectAsync([TestComponentBuilder], (tcb) => {
304 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
305 | fixture.detectChanges();
306 |
307 | let component = fixture.debugElement.componentInstance;
308 | component._bank.openAccount(accountId);
309 | component.accountId = accountId;
310 | component.amount = 1;
311 |
312 | fixture.detectChanges();
313 |
314 | let compiled = fixture.debugElement.nativeElement;
315 | let withdrawButton = getButton(compiled, 'Withdraw');
316 |
317 | expect(withdrawButton.hasAttribute('disabled')).toEqual(true);
318 | });
319 | }));
320 |
321 | it('should be enabled when conditions satisfied', injectAsync([TestComponentBuilder], (tcb) => {
322 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
323 | fixture.detectChanges();
324 |
325 | let component = fixture.debugElement.componentInstance;
326 | component._bank.openAccount(accountId, 1);
327 | component.accountId = accountId;
328 | component.amount = 1;
329 |
330 | fixture.detectChanges();
331 |
332 | let compiled = fixture.debugElement.nativeElement;
333 | let withdrawButton = getButton(compiled, 'Withdraw');
334 |
335 | expect(withdrawButton.hasAttribute('disabled')).toEqual(false);
336 | });
337 | }));
338 | });
339 |
340 | describe('Transfer button', () => {
341 | let account2Id = 'account-2';
342 |
343 | it('should be disabled when accountId is not provided', injectAsync([TestComponentBuilder], (tcb) => {
344 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
345 | fixture.detectChanges();
346 |
347 | let component = fixture.debugElement.componentInstance;
348 | component.amount = 1;
349 | component.transferToAccountId = account2Id;
350 |
351 | fixture.detectChanges();
352 |
353 | let compiled = fixture.debugElement.nativeElement;
354 | let transferButton = getButton(compiled, 'Transfer');
355 |
356 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
357 | });
358 | }));
359 |
360 | it('should be disabled when transferToAccountId is not provided', injectAsync([TestComponentBuilder], (tcb) => {
361 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
362 | fixture.detectChanges();
363 |
364 | let component = fixture.debugElement.componentInstance;
365 | component.accountId = accountId;
366 | component.amount = 1;
367 |
368 | fixture.detectChanges();
369 |
370 | let compiled = fixture.debugElement.nativeElement;
371 | let transferButton = getButton(compiled, 'Transfer');
372 |
373 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
374 | });
375 | }));
376 |
377 | it('should be disabled if accountId does not exist', injectAsync([TestComponentBuilder], (tcb) => {
378 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
379 | fixture.detectChanges();
380 |
381 | let component = fixture.debugElement.componentInstance;
382 | component._bank.openAccount(account2Id);
383 | component.accountId = accountId;
384 | component.amount = 1;
385 | component.transferToAccountId = account2Id;
386 |
387 | fixture.detectChanges();
388 |
389 | let compiled = fixture.debugElement.nativeElement;
390 | let transferButton = getButton(compiled, 'Transfer');
391 |
392 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
393 | });
394 | }));
395 |
396 | it('should be disabled if transferToAccountId does not exist', injectAsync([TestComponentBuilder], (tcb) => {
397 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
398 | fixture.detectChanges();
399 |
400 | let component = fixture.debugElement.componentInstance;
401 | component._bank.openAccount(accountId, 1);
402 | component.accountId = accountId;
403 | component.amount = 1;
404 | component.transferToAccountId = account2Id;
405 |
406 | fixture.detectChanges();
407 |
408 | let compiled = fixture.debugElement.nativeElement;
409 | let transferButton = getButton(compiled, 'Transfer');
410 |
411 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
412 | });
413 | }));
414 |
415 | it('should be disabled if amount is not provided', injectAsync([TestComponentBuilder], (tcb) => {
416 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
417 | fixture.detectChanges();
418 |
419 | let component = fixture.debugElement.componentInstance;
420 | component._bank.openAccount(accountId);
421 | component._bank.openAccount(account2Id);
422 | component.accountId = accountId;
423 | component.transferToAccountId = account2Id;
424 |
425 | fixture.detectChanges();
426 |
427 | let compiled = fixture.debugElement.nativeElement;
428 | let transferButton = getButton(compiled, 'Transfer');
429 |
430 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
431 | });
432 | }));
433 |
434 | it('should be disabled if amount is negative', injectAsync([TestComponentBuilder], (tcb) => {
435 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
436 | fixture.detectChanges();
437 |
438 | let component = fixture.debugElement.componentInstance;
439 | component._bank.openAccount(accountId);
440 | component._bank.openAccount(account2Id);
441 | component.accountId = accountId;
442 | component.amount = -1;
443 | component.transferToAccountId = account2Id;
444 |
445 | fixture.detectChanges();
446 |
447 | let compiled = fixture.debugElement.nativeElement;
448 | let transferButton = getButton(compiled, 'Transfer');
449 |
450 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
451 | });
452 | }));
453 |
454 | it('should be disabled if account does not have enough funds to complete transfer', injectAsync([TestComponentBuilder], (tcb) => {
455 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
456 | fixture.detectChanges();
457 |
458 | let component = fixture.debugElement.componentInstance;
459 | component._bank.openAccount(accountId);
460 | component._bank.openAccount(account2Id);
461 | component.accountId = accountId;
462 | component.amount = 1;
463 | component.transferToAccountId = account2Id;
464 |
465 | fixture.detectChanges();
466 |
467 | let compiled = fixture.debugElement.nativeElement;
468 | let transferButton = getButton(compiled, 'Transfer');
469 |
470 | expect(transferButton.hasAttribute('disabled')).toEqual(true);
471 | });
472 | }));
473 |
474 | it('should be enabled when conditions satisfied', injectAsync([TestComponentBuilder], (tcb) => {
475 | return tcb.createAsync(AccountOperationsComponent).then((fixture) => {
476 | fixture.detectChanges();
477 |
478 | let component = fixture.debugElement.componentInstance;
479 | component._bank.openAccount(accountId, 1);
480 | component._bank.openAccount(account2Id);
481 | component.accountId = accountId;
482 | component.amount = 1;
483 | component.transferToAccountId = account2Id;
484 |
485 | fixture.detectChanges();
486 |
487 | let compiled = fixture.debugElement.nativeElement;
488 | let transferButton = getButton(compiled, 'Transfer');
489 |
490 | expect(transferButton.hasAttribute('disabled')).toEqual(false);
491 | });
492 | }));
493 | });
494 |
495 | describe('openAccount', () => {
496 | it('should throw for missing accountId', () => {
497 | component.amount = amount;
498 |
499 | expect(() => component.openAccount())
500 | .toThrowError('accountId must be provided.');
501 | });
502 |
503 | it('should throw for missing amount', () => {
504 | component.accountId = accountId;
505 |
506 | expect(() => component.openAccount())
507 | .toThrowError('amount must be provided.');
508 | });
509 |
510 | it('should open account', () => {
511 | component.accountId = accountId;
512 | component.amount = amount;
513 | spyOn(bank, 'openAccount');
514 |
515 | component.openAccount();
516 |
517 | expect(bank.openAccount).toHaveBeenCalledWith(accountId, amount);
518 | });
519 |
520 | it('should reset amount', () => {
521 | component.accountId = accountId;
522 | component.amount = amount;
523 |
524 | component.openAccount();
525 |
526 | expect(component.amount).toBeUndefined();
527 | });
528 | });
529 |
530 | describe('closeAccount', () => {
531 | beforeEachProviders(() => {
532 | bank.openAccount(accountId);
533 | });
534 |
535 | it('should throw for missing accountId', () => {
536 | expect(() => component.closeAccount())
537 | .toThrowError('accountId must be provided.');
538 | });
539 |
540 | it('should close account', () => {
541 | component.accountId = accountId;
542 | spyOn(bank, 'closeAccount');
543 |
544 | component.closeAccount();
545 |
546 | expect(bank.closeAccount).toHaveBeenCalledWith(accountId);
547 | });
548 |
549 | it('should reset amount', () => {
550 | component.accountId = accountId;
551 | component.amount = amount;
552 |
553 | component.closeAccount();
554 |
555 | expect(component.amount).toBeUndefined();
556 | });
557 | });
558 |
559 | describe('deposit', () => {
560 | beforeEachProviders(() => {
561 | bank.openAccount(accountId);
562 | });
563 |
564 | it('should throw for missing accountId', () => {
565 | component.amount = amount;
566 |
567 | expect(() => component.deposit())
568 | .toThrowError('accountId must be provided.');
569 | });
570 |
571 | it('should throw for missing amount', () => {
572 | component.accountId = accountId;
573 |
574 | expect(() => component.deposit())
575 | .toThrowError('amount must be provided.');
576 | });
577 |
578 | it('should perform deposit', () => {
579 | component.accountId = accountId;
580 | component.amount = amount;
581 | spyOn(bank, 'deposit');
582 |
583 | component.deposit();
584 |
585 | expect(bank.deposit).toHaveBeenCalledWith(accountId, amount);
586 | });
587 |
588 | it('should reset amount', () => {
589 | component.accountId = accountId;
590 | component.amount = amount;
591 |
592 | component.deposit();
593 |
594 | expect(component.amount).toBeUndefined();
595 | });
596 | });
597 |
598 | describe('withdraw', () => {
599 | beforeEachProviders(() => {
600 | bank.openAccount(accountId, amount);
601 | });
602 |
603 | it('should throw for missing accountId', () => {
604 | component.amount = amount;
605 |
606 | expect(() => component.withdraw())
607 | .toThrowError('accountId must be provided.');
608 | });
609 |
610 | it('should throw for missing amount', () => {
611 | component.accountId = accountId;
612 |
613 | expect(() => component.withdraw())
614 | .toThrowError('amount must be provided.');
615 | });
616 |
617 | it('should perform withdraw', () => {
618 | component.accountId = accountId;
619 | component.amount = amount;
620 | spyOn(bank, 'withdraw');
621 |
622 | component.withdraw();
623 |
624 | expect(bank.withdraw).toHaveBeenCalledWith(accountId, amount);
625 | });
626 |
627 | it('should reset amount', () => {
628 | component.accountId = accountId;
629 | component.amount = amount;
630 |
631 | component.withdraw();
632 |
633 | expect(component.amount).toBeUndefined();
634 | });
635 | });
636 |
637 | describe('transfer', () => {
638 | let transferToAccountId = 'account-2';
639 |
640 | beforeEachProviders(() => {
641 | bank.openAccount(accountId, amount);
642 | bank.openAccount(transferToAccountId);
643 | });
644 |
645 | it('should throw for missing accountId', () => {
646 | component.amount = amount;
647 | component.transferToAccountId = transferToAccountId;
648 |
649 | expect(() => component.transfer())
650 | .toThrowError('accountId must be provided.');
651 | });
652 |
653 | it('should throw for missing amount', () => {
654 | component.accountId = accountId;
655 | component.transferToAccountId = transferToAccountId;
656 |
657 | expect(() => component.transfer())
658 | .toThrowError('amount must be provided.');
659 | });
660 |
661 | it('should throw for missing transferToAccountId', () => {
662 | component.accountId = accountId;
663 | component.amount = amount;
664 |
665 | expect(() => component.transfer())
666 | .toThrowError('transferToAccountId must be provided.');
667 | });
668 |
669 | it('should perform transfer', () => {
670 | component.accountId = accountId;
671 | component.amount = amount;
672 | component.transferToAccountId = transferToAccountId;
673 | spyOn(bank, 'transfer');
674 |
675 | component.transfer();
676 |
677 | expect(bank.transfer).toHaveBeenCalledWith(accountId, transferToAccountId, amount);
678 | });
679 |
680 | it('should reset amount', () => {
681 | component.accountId = accountId;
682 | component.amount = amount;
683 | component.transferToAccountId = transferToAccountId;
684 |
685 | component.transfer();
686 |
687 | expect(component.amount).toBeUndefined();
688 | });
689 | });
690 |
--------------------------------------------------------------------------------
/src/components/AccountOperationsComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'angular2/core';
2 | import { FORM_DIRECTIVES } from 'angular2/common';
3 |
4 | import {Account} from '../account';
5 | import {Bank} from '../bank';
6 |
7 | @Component({
8 | directives: [ FORM_DIRECTIVES ],
9 | providers: [ Bank ],
10 | selector: 'account-operations',
11 | styles: [`
12 | .bank-textfield--account-id-label {
13 | width: 200px;
14 | }
15 | .bank-textfield--amount-label {
16 | width: 125px;
17 | }
18 | .bank-button {
19 | width: 150px;
20 | }
21 | `],
22 | template: require('./AccountOperationsComponent.html')
23 | })
24 | export class AccountOperationsComponent {
25 | public accountId: string;
26 | public amount: number;
27 | public transferToAccountId: string;
28 |
29 | public get openAccountProhibited() {
30 | if (!this.accountId) {
31 | return true;
32 | }
33 |
34 | let anyExistingAccount = this._bank
35 | .getAllAccounts()
36 | .some(x => x.id === this.accountId);
37 |
38 | return anyExistingAccount;
39 | };
40 |
41 | public get closeAccountProhibited() {
42 | if (!this.accountId) {
43 | return true;
44 | }
45 |
46 | let anyExistingZeroBalanceAccount = this._bank
47 | .getAllAccounts()
48 | .some(x => x.id === this.accountId && x.balance === 0);
49 |
50 | return !anyExistingZeroBalanceAccount;
51 | };
52 |
53 | public get depositProhibited() {
54 | if (!this.accountId) {
55 | return true;
56 | }
57 |
58 | if (!this.amount || this.amount < 0) {
59 | return true;
60 | }
61 |
62 | let anyExistingAccount = this._bank
63 | .getAllAccounts()
64 | .some(x => x.id === this.accountId);
65 |
66 | return !anyExistingAccount;
67 | };
68 |
69 | public get withdrawProhibited() {
70 | if (!this.accountId) {
71 | return true;
72 | }
73 |
74 | if (!this.amount || this.amount < 0) {
75 | return true;
76 | }
77 |
78 | let existingAccounts = this._bank
79 | .getAllAccounts()
80 | .filter(x => x.id === this.accountId);
81 |
82 | if (existingAccounts.length === 0) {
83 | return true;
84 | }
85 |
86 | let existingAccount = existingAccounts[0];
87 |
88 | return existingAccount.balance < this.amount;
89 | };
90 |
91 | public get transferProhibited() {
92 | if (!this.accountId) {
93 | return true;
94 | }
95 |
96 | if (!this.amount || this.amount < 0) {
97 | return true;
98 | }
99 |
100 | if (!this.transferToAccountId) {
101 | return true;
102 | }
103 |
104 | if (this.accountId === this.transferToAccountId) {
105 | return true;
106 | }
107 |
108 | let existingAccounts = this._bank
109 | .getAllAccounts()
110 | .filter(x => x.id === this.accountId);
111 |
112 | if (existingAccounts.length === 0) {
113 | return true;
114 | }
115 |
116 | let existingAccount = existingAccounts[0];
117 |
118 | if (existingAccount.balance < this.amount) {
119 | return true;
120 | }
121 |
122 | let anyExistingTransferToAccount = this._bank
123 | .getAllAccounts()
124 | .some(x => x.id === this.transferToAccountId);
125 |
126 | return !anyExistingTransferToAccount;
127 | };
128 |
129 | private _bank: Bank;
130 |
131 | constructor(bank: Bank) {
132 | this._bank = bank;
133 | }
134 |
135 | public openAccount() : void {
136 | if (!this.accountId) {
137 | throw new Error('accountId must be provided.');
138 | }
139 | if (!this.amount) {
140 | throw new Error('amount must be provided.');
141 | }
142 |
143 | this._bank.openAccount(this.accountId, this.amount);
144 |
145 | this.resetAmount();
146 | }
147 |
148 | public closeAccount() : void {
149 | if (!this.accountId) {
150 | throw new Error('accountId must be provided.');
151 | }
152 |
153 | this._bank.closeAccount(this.accountId);
154 |
155 | this.resetAmount();
156 | }
157 |
158 | public deposit() : void {
159 | if (!this.accountId) {
160 | throw new Error('accountId must be provided.');
161 | }
162 |
163 | if (!this.amount) {
164 | throw new Error('amount must be provided.');
165 | }
166 |
167 | this._bank.deposit(this.accountId, this.amount);
168 |
169 | this.resetAmount();
170 | }
171 |
172 | public withdraw() : void {
173 | if (!this.accountId) {
174 | throw new Error('accountId must be provided.');
175 | }
176 |
177 | if (!this.amount) {
178 | throw new Error('amount must be provided.');
179 | }
180 |
181 | this._bank.withdraw(this.accountId, this.amount);
182 |
183 | this.resetAmount();
184 | }
185 |
186 | public transfer() : void {
187 | if (!this.accountId) {
188 | throw new Error('accountId must be provided.');
189 | }
190 |
191 | if (!this.amount) {
192 | throw new Error('amount must be provided.');
193 | }
194 |
195 | if (!this.transferToAccountId) {
196 | throw new Error('transferToAccountId must be provided.');
197 | }
198 |
199 | this._bank.transfer(this.accountId, this.transferToAccountId, this.amount);
200 |
201 | this.resetAmount();
202 | this.transferToAccountId = null;
203 | }
204 |
205 | private resetAmount() : void {
206 | this.amount = undefined;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/components/ShowBalancesComponent.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Id |
5 | Balance |
6 |
7 |
8 |
9 |
10 | {{ account.id }} |
11 | {{ account.balance }} |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/ShowBalancesComponent.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | beforeEachProviders,
3 | describe,
4 | expect,
5 | injectAsync,
6 | it,
7 | TestComponentBuilder
8 | } from 'angular2/testing';
9 |
10 | import {Account} from '../account';
11 | import {Bank} from '../bank';
12 | import {ShowBalancesComponent} from './ShowBalancesComponent';
13 |
14 | beforeEachProviders(() => {
15 | Bank.clear();
16 | });
17 |
18 | describe('ShowBalancesComponent', () => {
19 | it('should be empty to start', injectAsync([TestComponentBuilder], (tcb) => {
20 | return tcb.createAsync(ShowBalancesComponent).then((fixture) => {
21 | fixture.detectChanges();
22 |
23 | let compiled = fixture.debugElement.nativeElement;
24 | let tbody = compiled.getElementsByTagName('tbody')[0];
25 | let rows = tbody.getElementsByTagName('tr');
26 |
27 | expect(rows.length).toEqual(0);
28 | });
29 | }));
30 |
31 | it('should show Id column', injectAsync([TestComponentBuilder], (tcb) => {
32 | return tcb.createAsync(ShowBalancesComponent).then((fixture) => {
33 | fixture.detectChanges();
34 |
35 | let compiled = fixture.debugElement.nativeElement;
36 | let thead = compiled.getElementsByTagName('thead')[0];
37 | let th = thead.getElementsByTagName('th')[0];
38 |
39 | expect(th.innerHTML).toEqual('Id');
40 | });
41 | }));
42 |
43 | it('should show Balance column', injectAsync([TestComponentBuilder], (tcb) => {
44 | return tcb.createAsync(ShowBalancesComponent).then((fixture) => {
45 | fixture.detectChanges();
46 |
47 | let compiled = fixture.debugElement.nativeElement;
48 | let thead = compiled.getElementsByTagName('thead')[0];
49 | let th = thead.getElementsByTagName('th')[1];
50 |
51 | expect(th.innerHTML).toEqual('Balance');
52 | });
53 | }));
54 |
55 | it('should show expected number of accounts', injectAsync([TestComponentBuilder], (tcb) => {
56 | return tcb.createAsync(ShowBalancesComponent).then((fixture) => {
57 | fixture.detectChanges();
58 |
59 | fixture.debugElement.componentInstance._bank.openAccount('account-1', 0);
60 | fixture.debugElement.componentInstance._bank.openAccount('account-2', 0);
61 |
62 | fixture.debugElement.componentInstance.refreshAccounts();
63 |
64 | fixture.detectChanges();
65 |
66 | let compiled = fixture.debugElement.nativeElement;
67 | let tbody = compiled.getElementsByTagName('tbody')[0];
68 | let trTags = tbody.getElementsByTagName('tr');
69 |
70 | expect(trTags.length).toEqual(2);
71 | });
72 | }));
73 |
74 | it('should show accountId', injectAsync([TestComponentBuilder], (tcb) => {
75 | return tcb.createAsync(ShowBalancesComponent).then((fixture) => {
76 | fixture.detectChanges();
77 |
78 | fixture.debugElement.componentInstance._bank.openAccount('account-1', 0);
79 |
80 | fixture.debugElement.componentInstance.refreshAccounts();
81 |
82 | fixture.detectChanges();
83 |
84 | let compiled = fixture.debugElement.nativeElement;
85 | let tbody = compiled.getElementsByTagName('tbody')[0];
86 | let trTags = tbody.getElementsByTagName('tr');
87 | let tdTags = trTags[0].getElementsByTagName('td');
88 |
89 | expect(tdTags[0].innerHTML).toEqual('account-1');
90 | });
91 | }));
92 |
93 | it('should show balance', injectAsync([TestComponentBuilder], (tcb) => {
94 | return tcb.createAsync(ShowBalancesComponent).then((fixture) => {
95 | fixture.detectChanges();
96 |
97 | fixture.debugElement.componentInstance._bank.openAccount('account-1', 123);
98 |
99 | fixture.debugElement.componentInstance.refreshAccounts();
100 |
101 | fixture.detectChanges();
102 |
103 | let compiled = fixture.debugElement.nativeElement;
104 | let tbody = compiled.getElementsByTagName('tbody')[0];
105 | let trTags = tbody.getElementsByTagName('tr');
106 | let tdTags = trTags[0].getElementsByTagName('td');
107 |
108 | expect(tdTags[1].innerHTML).toEqual('123');
109 | });
110 | }));
111 |
112 | describe('isZeroBalance', () => {
113 | it('should return true for zero value', () => {
114 | let bank = new Bank();
115 | let component = new ShowBalancesComponent(bank);
116 |
117 | let result = component.isZeroBalance(new Account('account-1', 0))
118 |
119 | expect(result).toEqual(true);
120 | });
121 |
122 | it('should return false for positive value', () => {
123 | let bank = new Bank();
124 | let component = new ShowBalancesComponent(bank);
125 |
126 | let result = component.isZeroBalance(new Account('account-1', 123))
127 |
128 | expect(result).toEqual(false);
129 | });
130 | });
131 |
132 | describe('refreshAccounts', () => {
133 | it('should refresh accounts', () => {
134 | let bank = new Bank();
135 | let component = new ShowBalancesComponent(bank);
136 |
137 | bank.openAccount('account-1');
138 |
139 | component.refreshAccounts();
140 |
141 | expect(component.accounts.length).toEqual(1);
142 | });
143 | });
144 | });
145 |
--------------------------------------------------------------------------------
/src/components/ShowBalancesComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from 'angular2/core';
2 | import { CORE_DIRECTIVES } from 'angular2/common';
3 |
4 | import {Account} from '../account';
5 | import {Bank} from '../bank';
6 |
7 | @Component({
8 | directives: [ CORE_DIRECTIVES ],
9 | providers: [ Bank ],
10 | selector: 'show-balances',
11 | styles: [`
12 | .zero-balance {
13 | color: red;
14 | font-weight: bold;
15 | }
16 | `],
17 | template: require('./ShowBalancesComponent.html')
18 | })
19 | export class ShowBalancesComponent implements OnInit, OnDestroy {
20 | public accounts: Account[];
21 |
22 | private _accountUpdatesSubscription: any;
23 | private _bank: Bank;
24 |
25 | constructor(bank: Bank) {
26 | this._bank = bank;
27 | }
28 |
29 | public ngOnInit() {
30 | this._accountUpdatesSubscription = Bank.accountUpdates
31 | .subscribe(() => this.refreshAccounts());
32 | }
33 |
34 | public ngOnDestroy() {
35 | this._accountUpdatesSubscription.unsubscribe();
36 | }
37 |
38 | public isZeroBalance(account: Account) : boolean {
39 | return account.balance === 0;
40 | }
41 |
42 | public refreshAccounts() : void {
43 | this.accounts = this._bank.getAllAccounts();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'es6-shim';
2 | import 'es6-promise';
3 | // (these modules are what is in 'angular2/bundles/angular2-polyfills' so don't use that here)
4 | import 'es7-reflect-metadata/dist/browser';
5 | import 'zone.js/lib/browser/zone-microtask';
6 |
7 | // in Production you may want to remove this
8 | import 'zone.js/lib/browser/long-stack-trace-zone';
9 |
10 |
11 | (global).__extends = (this && this.__extends) || function (d?: any, b?: any) {
12 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
13 | var __: any = function() { this.constructor = d; };
14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15 | };
16 |
17 | (global).__decorate = (global).Reflect.decorate;
18 | (global).__metadata = (global).Reflect.metadata;
19 |
20 | (global).__param = (this && this.__param) || function (paramIndex?: any, decorator?: any) {
21 | return function (target?: any, key?: any) { decorator(target, key, paramIndex); };
22 | };
23 |
24 | (global).__awaiter = (this && this.__awaiter) ||
25 | function (thisArg?: any, _arguments?: any, Promise?: any, generator?: any) {
26 | return new Promise(function (resolve?: any, reject?: any) {
27 | generator = generator.call(thisArg, _arguments);
28 | function cast(value?: any) {
29 | return value instanceof Promise && value.constructor === Promise ?
30 | value : new Promise(function (resolve?: any) { resolve(value); }); }
31 | function onfulfill(value?: any) { try { step('next', value); } catch (e) { reject(e); } }
32 | function onreject(value?: any) { try { step('throw', value); } catch (e) { reject(e); } }
33 | function step(verb?: any, value?: any) {
34 | var result = generator[verb](value);
35 | result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject);
36 | }
37 | step('next', void 0);
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/src/public/angular-shield.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/angular-shield.png
--------------------------------------------------------------------------------
/src/public/css/alerts.css:
--------------------------------------------------------------------------------
1 | /* Adapted from CSS posted at https://www.npmjs.com/package/alerts */
2 | .alerts {
3 | position: fixed;
4 | z-index: 10000;
5 | width: 30em;
6 | top: 1em;
7 | right: 1em;
8 | }
9 |
10 | .alerts > div {
11 | padding: .8em;
12 | margin-bottom: .4em;
13 | background-color: lightpink;
14 | cursor: default;
15 | transition: opacity .2s;
16 | }
17 |
18 | .alerts > .alert,
19 | .alerts > .alert-dismiss {
20 | opacity: 0;
21 | }
22 |
23 | .alerts > .alert-show {
24 | opacity: 1;
25 | }
26 |
--------------------------------------------------------------------------------
/src/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/favicon.ico
--------------------------------------------------------------------------------
/src/public/humans.txt:
--------------------------------------------------------------------------------
1 | # humanstxt.org/
2 | # The humans responsible & technology colophon
3 |
4 | # TEAM
5 |
6 | -- --
7 |
8 | # THANKS
9 |
10 |
11 | PatrickJS -- @gdi2290
12 | AngularClass -- @AngularClass
13 |
14 | # TECHNOLOGY COLOPHON
15 |
16 | HTML5, CSS3
17 | Angular2, TypeScript, Webpack
18 |
--------------------------------------------------------------------------------
/src/public/icon/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/android-icon-144x144.png
--------------------------------------------------------------------------------
/src/public/icon/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/android-icon-192x192.png
--------------------------------------------------------------------------------
/src/public/icon/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/android-icon-36x36.png
--------------------------------------------------------------------------------
/src/public/icon/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/android-icon-48x48.png
--------------------------------------------------------------------------------
/src/public/icon/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/android-icon-72x72.png
--------------------------------------------------------------------------------
/src/public/icon/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/android-icon-96x96.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-114x114.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-120x120.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-144x144.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-152x152.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-180x180.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-57x57.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-60x60.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-72x72.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-76x76.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/src/public/icon/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/apple-icon.png
--------------------------------------------------------------------------------
/src/public/icon/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/src/public/icon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/favicon-16x16.png
--------------------------------------------------------------------------------
/src/public/icon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/favicon-32x32.png
--------------------------------------------------------------------------------
/src/public/icon/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/favicon-96x96.png
--------------------------------------------------------------------------------
/src/public/icon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/favicon.ico
--------------------------------------------------------------------------------
/src/public/icon/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/ms-icon-144x144.png
--------------------------------------------------------------------------------
/src/public/icon/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/ms-icon-150x150.png
--------------------------------------------------------------------------------
/src/public/icon/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/ms-icon-310x310.png
--------------------------------------------------------------------------------
/src/public/icon/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/icon/ms-icon-70x70.png
--------------------------------------------------------------------------------
/src/public/img/angular-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/img/angular-logo.png
--------------------------------------------------------------------------------
/src/public/img/angularclass-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ritterim/angular2-bank/08bc9e25c1af2fb9fde368a03b88908cd506ff77/src/public/img/angularclass-logo.png
--------------------------------------------------------------------------------
/src/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Angular2 Bank
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | Loading...
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "/icon/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "/icon/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "/icon/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "/icon/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "/icon/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "/icon/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/public/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/src/public/service-worker.js:
--------------------------------------------------------------------------------
1 | // This file is intentionally without code.
2 |
--------------------------------------------------------------------------------
/src/vendor.ts:
--------------------------------------------------------------------------------
1 | // Polyfills
2 | import './polyfills';
3 |
4 | // Angular 2
5 | import 'angular2/platform/browser';
6 | import 'angular2/platform/common_dom';
7 | import 'angular2/core';
8 | import 'angular2/router';
9 | import 'angular2/http';
10 |
11 | // RxJS
12 | import 'rxjs';
13 |
14 | // Other vendors for example jQuery or Lodash
15 |
--------------------------------------------------------------------------------
/test/injector.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | it,
3 | inject,
4 | injectAsync,
5 | beforeEachProviders,
6 | TestComponentBuilder
7 | } from 'angular2/testing';
8 | import {APP_ID} from 'angular2/core';
9 |
10 |
11 | describe('default test injector', () => {
12 | it('should provide default id', inject([APP_ID], (id) => {
13 | expect(id).toBe('a');
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/test/sanity-test.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | it,
3 | inject,
4 | injectAsync,
5 | beforeEachProviders,
6 | TestComponentBuilder
7 | } from 'angular2/testing';
8 |
9 | describe('sanity checks', () => {
10 | it('should also be able to test', () => {
11 | expect(true).toBe(true);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "commonjs",
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "noEmitHelpers": false,
9 | "sourceMap": true
10 | },
11 | "files": [
12 | "./src/account.spec.ts",
13 | "./src/account.ts",
14 | "./src/app/app.spec.ts",
15 | "./src/app/app.ts",
16 | "./src/bank.spec.ts",
17 | "./src/bank.ts",
18 | "./src/bootstrap.ts",
19 | "./src/components/AccountOperationsComponent.spec.ts",
20 | "./src/components/AccountOperationsComponent.ts",
21 | "./src/components/ShowBalancesComponent.spec.ts",
22 | "./src/components/ShowBalancesComponent.ts",
23 | "./src/vendor.ts",
24 | "./src/polyfills.ts",
25 | "./test/injector.spec.ts",
26 | "./test/sanity-test.spec.ts"
27 | ],
28 | "filesGlob": [
29 | "./src/**/*.ts",
30 | "./test/**/*.ts",
31 | "!./node_modules/**/*.ts"
32 | ],
33 | "compileOnSave": false,
34 | "buildOnSave": false,
35 | "atom": {
36 | "rewriteTsconfig": true
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "class-name": true,
4 | "curly": false,
5 | "eofline": true,
6 | "indent": "spaces",
7 | "max-line-length": [
8 | true,
9 | 100
10 | ],
11 | "member-ordering": [
12 | true,
13 | "public-before-private",
14 | "static-before-instance",
15 | "variables-before-functions"
16 | ],
17 | "no-arg": true,
18 | "no-construct": true,
19 | "no-duplicate-key": true,
20 | "no-duplicate-variable": true,
21 | "no-empty": true,
22 | "no-eval": true,
23 | "no-trailing-comma": true,
24 | "no-trailing-whitespace": true,
25 | "no-unused-expression": true,
26 | "no-unused-variable": false,
27 | "no-unreachable": true,
28 | "no-use-before-declare": true,
29 | "one-line": [
30 | true,
31 | "check-open-brace",
32 | "check-catch",
33 | "check-else",
34 | "check-whitespace"
35 | ],
36 | "quotemark": [
37 | true,
38 | "single"
39 | ],
40 | "semicolon": true,
41 | "triple-equals": [
42 | true,
43 | "allow-null-check"
44 | ],
45 | "variable-name": false,
46 | "whitespace": [
47 | true,
48 | "check-branch",
49 | "check-decl",
50 | "check-operator",
51 | "check-separator",
52 | "check-type"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "mode": "modules",
3 | "out": "doc",
4 | "theme": "default",
5 | "ignoreCompilerErrors": "true",
6 | "experimentalDecorators": "true",
7 | "emitDecoratorMetadata": "true",
8 | "target": "ES5",
9 | "moduleResolution": "node",
10 | "preserveConstEnums": "true",
11 | "stripInternal": "true",
12 | "suppressExcessPropertyErrors": "true",
13 | "suppressImplicitAnyIndexErrors": "true",
14 | "module": "commonjs"
15 | }
16 |
--------------------------------------------------------------------------------
/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "es6-promise": "github:typings/typed-es6-promise#9243c53f70fb4909ed7cce3094bec221b9fb6d5f"
4 | },
5 | "devDependencies": {},
6 | "ambientDependencies": {
7 | "angular-protractor": "github:angular/DefinitelyTyped/angular-protractor/angular-protractor.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9",
8 | "es6-shim": "github:angular/DefinitelyTyped/es6-shim/es6-shim.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9",
9 | "hammerjs": "github:angular/DefinitelyTyped/hammerjs/hammerjs.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9",
10 | "jasmine": "github:angular/DefinitelyTyped/jasmine/jasmine.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9",
11 | "node": "github:angular/DefinitelyTyped/node/node.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9",
12 | "selenium-webdriver": "github:angular/DefinitelyTyped/selenium-webdriver/selenium-webdriver.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9",
13 | "zone": "github:angular/DefinitelyTyped/zone/zone.d.ts#31e7317c9a0793857109236ef7c7f223305a8aa9"
14 | }
15 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // @AngularClass
2 |
3 | /*
4 | * Helper: root(), and rootDir() are defined at the bottom
5 | */
6 | var path = require('path');
7 | var webpack = require('webpack');
8 | // Webpack Plugins
9 | var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
10 |
11 | /*
12 | * Config
13 | */
14 | module.exports = {
15 | // for faster builds use 'eval'
16 | devtool: 'source-map',
17 | debug: true, // remove in production
18 |
19 | entry: {
20 | 'vendor': './src/vendor.ts',
21 | 'app': './src/bootstrap.ts' // our angular app
22 | },
23 |
24 | // Config for our build files
25 | output: {
26 | path: root('__build__'),
27 | filename: '[name].js',
28 | sourceMapFilename: '[name].map',
29 | chunkFilename: '[id].chunk.js'
30 | },
31 |
32 | resolve: {
33 | // ensure loader extensions match
34 | extensions: ['','.ts','.js','.json', '.css', '.html']
35 | },
36 |
37 | module: {
38 | preLoaders: [ { test: /\.ts$/, loader: 'tslint-loader' } ],
39 | loaders: [
40 | // Support for .ts files.
41 | {
42 | test: /\.ts$/,
43 | loader: 'ts-loader',
44 | query: {
45 | 'ignoreDiagnostics': [
46 | 2403, // 2403 -> Subsequent variable declarations
47 | 2300, // 2300 -> Duplicate identifier
48 | 2374, // 2374 -> Duplicate number index signature
49 | 2375 // 2375 -> Duplicate string index signature
50 | ]
51 | },
52 | exclude: [ /\.(spec|e2e)\.ts$/, /node_modules\/(?!(ng2-.+))/ ]
53 | },
54 |
55 | // Support for *.json files.
56 | { test: /\.json$/, loader: 'json-loader' },
57 |
58 | // Support for CSS as raw text
59 | { test: /\.css$/, loader: 'raw-loader' },
60 |
61 | // support for .html as raw text
62 | { test: /\.html$/, loader: 'raw-loader' },
63 | ],
64 | noParse: [ /.+zone\.js\/dist\/.+/, /.+angular2\/bundles\/.+/ ]
65 | },
66 |
67 | plugins: [
68 | new CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js', minChunks: Infinity }),
69 | new CommonsChunkPlugin({ name: 'common', filename: 'common.js', minChunks: 2, chunks: ['app', 'vendor'] })
70 | // include uglify in production
71 | ],
72 |
73 | // Other module loader config
74 | tslint: {
75 | emitErrors: false,
76 | failOnHint: false
77 | },
78 | // our Webpack Development Server config
79 | devServer: {
80 | historyApiFallback: true,
81 | contentBase: 'src/public',
82 | publicPath: '/__build__'
83 | }
84 | };
85 |
86 | // Helper functions
87 |
88 | function root(args) {
89 | args = Array.prototype.slice.call(arguments, 0);
90 | return path.join.apply(path, [__dirname].concat(args));
91 | }
92 |
93 | function rootNode(args) {
94 | args = Array.prototype.slice.call(arguments, 0);
95 | return root.apply(path, ['node_modules'].concat(args));
96 | }
97 |
--------------------------------------------------------------------------------