├── .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 | ![](https://travis-ci.org/ritterim/angular2-bank.svg) 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 |
2 |
3 |
4 | Angular2 Bank 5 |
6 |
7 |
8 |
9 |
10 |
Account Balances
11 | 12 |
13 |
14 |
Account Operations
15 | 16 |
17 |
18 |
19 | 25 |
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 | 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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
IdBalance
{{ account.id }}{{ account.balance }}
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 | --------------------------------------------------------------------------------