├── SPA ├── AngularFrontEnd │ ├── app │ │ ├── css │ │ │ ├── .gitkeep │ │ │ └── app.css │ │ ├── img │ │ │ └── .gitkeep │ │ ├── partials │ │ │ ├── .gitkeep │ │ │ ├── sector-component.html │ │ │ ├── transactions-component.html │ │ │ ├── about-page.html │ │ │ ├── home-page.html │ │ │ ├── investment-filter.html │ │ │ ├── nav-bar.html │ │ │ ├── investments-component.html │ │ │ └── investment-page.html │ │ ├── js │ │ │ ├── controllers.js │ │ │ ├── services.js │ │ │ ├── investment-filter.js │ │ │ ├── nav-bar.js │ │ │ ├── filters.js │ │ │ ├── sector-component.js │ │ │ ├── investments-component.js │ │ │ ├── app.js │ │ │ ├── investment-page.js │ │ │ ├── transactions-component.js │ │ │ └── directives.js │ │ ├── mock_server │ │ │ ├── mock-server-stub.js │ │ │ └── mock-server.js │ │ ├── index.html │ │ ├── index-async.html │ │ └── charts │ │ │ ├── svg-column-chart.js │ │ │ └── svg-bar-chart.js │ ├── .bowerrc │ ├── .jshintrc │ ├── .gitignore │ ├── test │ │ ├── unit │ │ │ ├── directivesSpec.js │ │ │ ├── servicesSpec.js │ │ │ ├── controllersSpec.js │ │ │ ├── filtersSpec.js │ │ │ ├── investmentPageSpec.js │ │ │ ├── sectorComponentSpec.js │ │ │ ├── transactionsComponentSpec.js │ │ │ └── investmentsComponentSpec.js │ │ ├── protractor-conf.js │ │ ├── karma.conf.js │ │ └── e2e │ │ │ └── scenarios.js │ ├── .travis.yml │ ├── bower.json │ ├── package.json │ └── gulpfile.js ├── .gitignore └── KnockoutFrontEnd │ ├── .bowerrc │ ├── test │ ├── .bowerrc │ ├── bower.json │ ├── SpecRunner.karma.js │ ├── app │ │ └── search-model.js │ ├── index.html │ ├── components │ │ ├── home-page.js │ │ ├── investment-filter.js │ │ ├── investment-page.js │ │ ├── sector-component.js │ │ ├── transactions-component.js │ │ └── investments-component.js │ ├── SpecRunner.browser.js │ ├── require.config.js │ └── extensions │ │ └── custom-format.js │ ├── .gitignore │ ├── src │ ├── components │ │ ├── home-page │ │ │ ├── home.js │ │ │ └── home.html │ │ ├── investment-filter │ │ │ ├── investment-filter.js │ │ │ └── investment-filter.html │ │ ├── sector-component │ │ │ ├── sector-component.html │ │ │ └── sector-component.js │ │ ├── transactions-component │ │ │ ├── transactions-component.html │ │ │ └── transactions-component.js │ │ ├── about-page │ │ │ └── about.html │ │ ├── nav-bar │ │ │ ├── nav-bar.js │ │ │ └── nav-bar.html │ │ ├── investments-component │ │ │ ├── investments-component.js │ │ │ └── investments-component.html │ │ └── investment-page │ │ │ ├── investment-page.js │ │ │ └── investment-page.html │ ├── app │ │ ├── search-model.js │ │ ├── startup.js │ │ ├── router.js │ │ └── require.config.js │ ├── index.html │ ├── mock_server │ │ ├── mock-server-stub.js │ │ └── mock-server.js │ ├── extensions │ │ ├── custom-format.js │ │ └── charts.js │ ├── css │ │ └── styles.css │ └── charts │ │ ├── svg-column-chart.js │ │ └── svg-bar-chart.js │ ├── bower.json │ ├── package.json │ ├── karma.conf.js │ └── gulpfile.js ├── .gitignore ├── Images ├── SampleApp.png ├── ServedFilesAN.png ├── ServedFilesKO.png └── PropogateFilter.png ├── Dist ├── Angular │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.svg │ ├── partials │ │ ├── about-page.html │ │ └── investment-page.html │ └── index.html └── Knockout │ ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff │ ├── index.html │ ├── about-stuff.js │ └── investment-page.js ├── LICENSE └── README.md /SPA/AngularFrontEnd/app/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/img/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SPA/.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | AngularFrontEnd/dist/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | SPA/*.bat 2 | SPA/AngularFrontEnd.sln 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "src/bower_modules" 3 | } 4 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_modules" 3 | } 4 | -------------------------------------------------------------------------------- /Images/SampleApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Images/SampleApp.png -------------------------------------------------------------------------------- /Images/ServedFilesAN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Images/ServedFilesAN.png -------------------------------------------------------------------------------- /Images/ServedFilesKO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Images/ServedFilesKO.png -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": true, 3 | "globals": { 4 | "angular": false 5 | } 6 | } -------------------------------------------------------------------------------- /Images/PropogateFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Images/PropogateFilter.png -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/.gitignore: -------------------------------------------------------------------------------- 1 | logs/* 2 | !.gitkeep 3 | node_modules/ 4 | bower_components/ 5 | tmp 6 | .DS_Store 7 | .idea -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | bower_modules/ 4 | 5 | # Don't track build output 6 | dist/ 7 | -------------------------------------------------------------------------------- /Dist/Angular/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Dist/Angular/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Dist/Angular/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Dist/Angular/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Dist/Angular/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Dist/Angular/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /Dist/Knockout/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Dist/Knockout/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /Dist/Knockout/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Dist/Knockout/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Dist/Knockout/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andy-lee-eng/Angular-vs-Knockout/HEAD/Dist/Knockout/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "jasmine": "~2.0.0", 7 | "requirejs": "~2.1.11" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/directivesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for directives go here */ 4 | 5 | describe('directives', function() { 6 | beforeEach(module('testSPA.directives')); 7 | 8 | describe('app-version', function() { 9 | 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/sector-component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sectors

4 |
5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /Dist/Angular/partials/about-page.html: -------------------------------------------------------------------------------- 1 |

About

This is a test Single Page Application built using:
  • AngularJS - framework (data binding and components, routing, dependency injection)
-------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/home-page/home.js: -------------------------------------------------------------------------------- 1 | define(["knockout", "text!./home.html", "app/search-model"], function (ko, homeTemplate, searchModel) { 2 | 3 | function HomeViewModel(route) { 4 | this.search = searchModel; 5 | } 6 | 7 | return { viewModel: HomeViewModel, template: homeTemplate }; 8 | }); 9 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/transactions-component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Transactions

4 |
5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/investment-filter/investment-filter.js: -------------------------------------------------------------------------------- 1 | define(['knockout', 'text!./investment-filter.html'], function(ko, templateMarkup) { 2 | 3 | function InvestmentFilter(params) { 4 | this.search = params.search; 5 | } 6 | 7 | return { viewModel: InvestmentFilter, template: templateMarkup }; 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Controllers */ 4 | 5 | angular.module('testSPA.controllers', []) 6 | .controller('HomeCtrl', ['$scope', 'searchModel', function ($scope, searchModel) { 7 | $scope.search = searchModel; 8 | }]) 9 | .controller('AboutCtrl', ['$scope', function($scope) { 10 | 11 | }]); 12 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/sector-component/sector-component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sectors

4 |
5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /Dist/Knockout/index.html: -------------------------------------------------------------------------------- 1 | Test SPA Knockout
-------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/transactions-component/transactions-component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Transactions

4 |
5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/protractor-conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | allScriptsTimeout: 11000, 3 | 4 | specs: [ 5 | 'e2e/*.js' 6 | ], 7 | 8 | capabilities: { 9 | 'browserName': 'chrome' 10 | }, 11 | 12 | baseUrl: 'http://127.0.0.1:8080/', 13 | 14 | framework: 'jasmine', 15 | 16 | jasmineNodeOpts: { 17 | defaultTimeoutInterval: 30000 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/SpecRunner.karma.js: -------------------------------------------------------------------------------- 1 | var tests = []; 2 | for (var file in window.__karma__.files) { 3 | if (window.__karma__.files.hasOwnProperty(file)) { 4 | if (/test\/components\/.*\.js$/.test(file)) { 5 | tests.push(file); 6 | } 7 | } 8 | } 9 | 10 | requirejs.config({ 11 | baseUrl: '/base/src', 12 | deps: tests, 13 | callback: window.__karma__.start 14 | }); 15 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/app/search-model.js: -------------------------------------------------------------------------------- 1 | define(['knockout'], function (ko) { 2 | function SearchModel() { 3 | this.name = ko.observable(''); 4 | 5 | this.json = ko.computed(function () { 6 | return { 7 | name: this.name() 8 | }; 9 | }, this).extend({ rateLimit: 500 }); 10 | }; 11 | 12 | return new SearchModel(); 13 | }); 14 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Services */ 4 | 5 | 6 | // Demonstrate how to register services 7 | // In this case it is a simple value service. 8 | angular.module('testSPA.services', []) 9 | .service('searchModel', function SearchModel() { 10 | this.name = ''; 11 | 12 | this.json = function () { 13 | return { 14 | name: this.name 15 | }; 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/app/search-model.js: -------------------------------------------------------------------------------- 1 | define(['app/search-model'], function (searchModel) { 2 | 3 | describe('search model', function () { 4 | beforeEach(function () { searchModel.name(''); }); 5 | 6 | it('json should return search json query', function () { 7 | searchModel.name('test-filter'); 8 | 9 | expect(searchModel.json()).toEqual({ name: 'test-filter' }); 10 | }); 11 | 12 | }); 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm start > /dev/null & 9 | - npm run update-webdriver 10 | - sleep 1 # give server time to start 11 | 12 | script: 13 | - node_modules/.bin/karma start test/karma.conf.js --no-auto-watch --single-run --reporters=dots --browsers=Firefox 14 | - node_modules/.bin/protractor test/protractor-conf.js --browser=firefox 15 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/about-page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

About

5 |
6 |
7 | This is a test Single Page Application built using: 8 |
    9 |
  • AngularJS - framework (data binding and components, routing, dependency injection)
  • 10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/servicesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for services go here */ 4 | 5 | describe('service', function() { 6 | beforeEach(module('testSPA.services')); 7 | 8 | describe('search model', function () { 9 | it('json should return filter json query', inject(function (searchModel) { 10 | searchModel.name = 'test-filter'; 11 | 12 | expect(searchModel.json()).toEqual({ name: 'test-filter' }); 13 | })); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-spa", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "components-bootstrap": "~3.1.1", 7 | "crossroads": "~0.12.0", 8 | "globalize": "~0.1.1", 9 | "hasher": "~1.2.0", 10 | "jquery": "~2.1.1", 11 | "jquery-mockjax": "~1.5.3", 12 | "requirejs": "~2.1.11", 13 | "requirejs-text": "~2.0.10", 14 | "knockout": "~3.2.0-alpha", 15 | "knockout-projections": "~1.1.0-pre", 16 | "d3": "~3.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/investment-filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.investmentFilter', []) 4 | .directive('investmentFilter', function () { 5 | return { 6 | restrict: 'E', 7 | scope: { 8 | search: '=' 9 | }, 10 | templateUrl: 'partials/investment-filter.html', 11 | controller: 'InvestmentFilterCtrl' 12 | }; 13 | }) 14 | .controller('InvestmentFilterCtrl', ['$scope', function ($scope) { 15 | 16 | }]); 17 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/home-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
-------------------------------------------------------------------------------- /SPA/AngularFrontEnd/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-seed", 3 | "description": "A starter project for AngularJS", 4 | "version": "0.0.0", 5 | "homepage": "https://github.com/angular/angular-seed", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "angular": "1.3.x", 10 | "angular-route": "1.3.x", 11 | "angular-loader": "1.3.x", 12 | "angular-mocks": "~1.3.x", 13 | "bootstrap": "~3.2", 14 | "globalize": "~0.1.1", 15 | "html5-boilerplate": "~4.3.0", 16 | "jquery": "~2.1.1", 17 | "d3": "~3.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/home-page/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
-------------------------------------------------------------------------------- /Dist/Knockout/about-stuff.js: -------------------------------------------------------------------------------- 1 | define("text!components/about-page/about.html",[],function(){return'
\r\n
\r\n
\r\n

About

\r\n
\r\n
\r\n This is a test Single Page Application built using:\r\n
    \r\n
  • KnockoutJS - data binding and components
  • \r\n
  • CrossroadsJS - routing
  • \r\n
  • RequireJS - module loader and dependency injection
  • \r\n
\r\n
\r\n
\r\n
'}); -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/about-page/about.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

About

5 |
6 |
7 | This is a test Single Page Application built using: 8 |
    9 |
  • KnockoutJS - data binding and components
  • 10 |
  • CrossroadsJS - routing
  • 11 |
  • RequireJS - module loader and dependency injection
  • 12 |
13 |
14 |
15 |
-------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/nav-bar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.navBar', []) 4 | .directive('navBar', function () { 5 | return { 6 | restrict: 'E', 7 | templateUrl: 'partials/nav-bar.html', 8 | controller: 'RouteCtrl' 9 | }; 10 | }) 11 | .controller('RouteCtrl', ['$scope', '$route', '$location', function ($scope, $route, $location) { 12 | $scope.$on("$routeChangeSuccess", function (event, current, previous) { 13 | $scope.controller = $route.current.controller; 14 | }); 15 | }]); 16 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/nav-bar/nav-bar.js: -------------------------------------------------------------------------------- 1 | define(['knockout', 'text!./nav-bar.html'], function(ko, template) { 2 | 3 | function NavBarViewModel(params) { 4 | 5 | // This viewmodel doesn't do anything except pass through the 'route' parameter to the view. 6 | // You could remove this viewmodel entirely, and define 'nav-bar' as a template-only component. 7 | // But in most apps, you'll want some viewmodel logic to determine what navigation options appear. 8 | 9 | this.route = params.route; 10 | } 11 | 12 | return { viewModel: NavBarViewModel, template: template }; 13 | }); 14 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spec Runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/components/home-page.js: -------------------------------------------------------------------------------- 1 | define(['components/home-page/home', 'app/search-model'], function (homePage, searchModel) { 2 | var HomePageViewModel = homePage.viewModel; 3 | 4 | describe('home page view model', function() { 5 | beforeEach(function () { searchModel.name(''); }); 6 | 7 | it('should store search model object', function() { 8 | var instance = new HomePageViewModel(); 9 | expect(instance.search.name()).toEqual(''); 10 | 11 | // Change the filter name 12 | searchModel.name('new-filter'); 13 | expect(instance.search.name()).toEqual('new-filter'); 14 | }); 15 | 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/investment-filter.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | 8 | 9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/investment-filter/investment-filter.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | 8 | 9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/sector-component/sector-component.js: -------------------------------------------------------------------------------- 1 | define(['knockout', 'text!./sector-component.html', 'extensions/charts'], function (ko, templateMarkup) { 2 | 3 | function SectorComponent(params) { 4 | this.sectors = ko.observableArray(); 5 | 6 | var getSectors = function () { 7 | $.getJSON('http://localhost:54361/getSectors', params.search.json(), this.sectors); 8 | }; 9 | 10 | getSectors.call(this); 11 | var subscription = params.search.json.subscribe(getSectors, this); 12 | this.dispose = function () { subscription.dispose(); }; 13 | } 14 | 15 | return { viewModel: SectorComponent, template: templateMarkup }; 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/SpecRunner.browser.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Reference your test modules here 3 | var testModules = [ 4 | 'app/search-model', 5 | 'components/home-page', 6 | 'components/investment-filter', 7 | 'components/investments-component', 8 | 'components/sector-component', 9 | 'components/transactions-component', 10 | 'components/investment-page', 11 | 'extensions/custom-format' 12 | ]; 13 | 14 | // After the 'jasmine-boot' module creates the Jasmine environment, load all test modules then run them 15 | require(['jasmine-boot'], function () { 16 | var modulesCorrectedPaths = testModules.map(function(m) { return '../test/' + m; }); 17 | require(modulesCorrectedPaths, window.onload); 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/components/investment-filter.js: -------------------------------------------------------------------------------- 1 | define(['components/investment-filter/investment-filter', 'app/search-model'], function (investmentFilter, searchModel) { 2 | var InvestmentFilterViewModel = investmentFilter.viewModel; 3 | 4 | describe('investment filter view model', function () { 5 | beforeEach(function () { searchModel.name(''); }); 6 | 7 | it('should store search model object', function () { 8 | var instance = new InvestmentFilterViewModel({ search: searchModel }); 9 | 10 | expect(instance.search.name()).toEqual(''); 11 | 12 | // Change the filter name 13 | searchModel.name('new-filter'); 14 | expect(instance.search.name()).toEqual('new-filter'); 15 | }); 16 | 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test SPA Knockout 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Filters */ 4 | Globalize.culture('en-GB'); 5 | 6 | angular.module('testSPA.filters', []) 7 | .filter('customDate', ['dateFilter', function (dateFilter) { 8 | return function (text) { 9 | return Globalize.format(new Date(Date.parse(text)), 'D'); 10 | }; 11 | }]) 12 | .filter('duration', function () { 13 | return function (text) { 14 | return Globalize.format(text, 'N2'); 15 | }; 16 | }) 17 | .filter('customCurrency', function () { 18 | return function (text) { 19 | return Globalize.format(text, 'C'); 20 | }; 21 | }) 22 | .filter('percent', function () { 23 | return function(text) { 24 | return Globalize.format(parseFloat(text) * 100, 'N0') + '%'; 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-spa", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "chalk": "~0.4.0", 6 | "combined-stream": "0.0.5", 7 | "deeply": "~0.1.0", 8 | "event-stream": "~3.1.0", 9 | "gulp": "^3.8.6", 10 | "gulp-clean": "~0.2.4", 11 | "gulp-concat": "~2.2.0", 12 | "gulp-html-replace": "~1.0.0", 13 | "gulp-minify-css": "^0.3.7", 14 | "gulp-minify-html": "^0.1.4", 15 | "gulp-replace": "~0.2.0", 16 | "gulp-requirejs-bundler": "^0.1.1", 17 | "gulp-uglify": "~0.2.1", 18 | "karma": "^0.12.15", 19 | "karma-chrome-launcher": "^0.1.3", 20 | "karma-jasmine": "^0.1.5", 21 | "karma-requireglobal-preprocessor": "0.0.0", 22 | "karma-requirejs": "^0.2.1", 23 | "requirejs": "^2.1.11" 24 | }, 25 | "scripts": { 26 | "postinstall": "bower install" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/sector-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.sectorComponent', []) 4 | .directive('sectorComponent', function () { 5 | return { 6 | restrict: 'E', 7 | scope: { 8 | search: '=' 9 | }, 10 | templateUrl: 'partials/sector-component.html', 11 | controller: 'SectorComponentCtrl' 12 | }; 13 | }) 14 | .controller('SectorComponentCtrl', ['$scope', 'backEndServer', function ($scope, backEndServer) { 15 | $scope.$watch('search.name', function () { 16 | backEndServer.getSectors($scope.search.json()).then(function (data) { 17 | $scope.sectors = data; 18 | }); 19 | }); 20 | 21 | $scope.chartOptions = { series: ['investedAmount', 'returnAmount'] }; 22 | }]); 23 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/controllersSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for controllers go here */ 4 | 5 | describe('controllers', function(){ 6 | beforeEach(module('testSPA.controllers')); 7 | beforeEach(module('testSPA.services')); 8 | 9 | describe('home controller', function () { 10 | it('should store search model object', inject(function ($controller, searchModel) { 11 | //spec body 12 | var scope = {}; 13 | var homeCtrl = $controller('HomeCtrl', { $scope: scope }); 14 | 15 | expect(scope.search).toBeDefined(); 16 | expect(scope.search.name).toEqual(''); 17 | 18 | // Change the filter name 19 | searchModel.name = 'new-filter'; 20 | expect(scope.search.name).toEqual('new-filter'); 21 | })); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/investments-component/investments-component.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "knockout", "text!./investments-component.html", 'extensions/custom-format'], function ($, ko, templateMarkup) { 2 | 3 | function InvestmentsComponentViewModel(params) { 4 | this.investments = ko.observableArray(); 5 | 6 | var getInvestments = function () { 7 | $.getJSON('http://localhost:54361/analysis', params.search.json(), this.investments); 8 | }; 9 | 10 | getInvestments.call(this); 11 | var subscription = params.search.json.subscribe(getInvestments, this); 12 | this.dispose = function () { subscription.dispose(); }; 13 | 14 | this.showInvestment = function () { 15 | location.hash = 'investment/' + this.id; 16 | }; 17 | } 18 | 19 | return { viewModel: InvestmentsComponentViewModel, template: templateMarkup }; 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/investments-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.investmentsComponent', []) 4 | .directive('investmentsComponent', function () { 5 | return { 6 | restrict: 'E', 7 | scope: { 8 | search: '=' 9 | }, 10 | templateUrl: 'partials/investments-component.html', 11 | controller: 'InvestmentsComponentCtrl' 12 | }; 13 | }) 14 | .controller('InvestmentsComponentCtrl', ['$scope', 'backEndServer', function ($scope, backEndServer) { 15 | $scope.$watch('search.name', function () { 16 | backEndServer.analysis($scope.search.json()).then(function (data) { 17 | $scope.investments = data; 18 | }); 19 | }); 20 | 21 | $scope.showInvestment = function () { 22 | location.hash = '/investment/' + this.investment.id; 23 | }; 24 | 25 | }]); 26 | -------------------------------------------------------------------------------- /Dist/Angular/partials/investment-page.html: -------------------------------------------------------------------------------- 1 |

{{name}}

Details

Status
{{open ? 'Open' : 'Closed'}}
Sector
{{sector}}
Return on Investment
{{returnOnInvestment | percent}}

Transactions

{{transaction.date | customDate}}{{transaction.amount | customCurrency}}{{transaction.valuation ? 'Valuation' : ''}}

-------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // Declare app level module which depends on filters, and services 5 | angular.module('testSPA', [ 6 | 'ngRoute', 7 | 'testSPA.filters', 8 | 'testSPA.services', 9 | 'testSPA.directives', 10 | 'testSPA.controllers', 11 | 'testSPA.navBar', 12 | 'testSPA.investmentsComponent', 13 | 'testSPA.sectorComponent', 14 | 'testSPA.transactionsComponent', 15 | 'testSPA.investmentFilter', 16 | 'testSPA.investmentPage', 17 | 'testSPA.mockServer' 18 | ]). 19 | config(['$routeProvider', function($routeProvider) { 20 | $routeProvider.when('/home', {templateUrl: 'partials/home-page.html', controller: 'HomeCtrl'}); 21 | $routeProvider.when('/about', {templateUrl: 'partials/about-page.html', controller: 'AboutCtrl'}); 22 | $routeProvider.when('/investment/:id', { templateUrl: 'partials/investment-page.html', controller: 'InvestmentPageCtrl' }); 23 | $routeProvider.otherwise({ redirectTo: '/home' }); 24 | }]); 25 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | 4 | basePath : '../', 5 | 6 | files : [ 7 | 'app/bower_components/angular/angular.js', 8 | 'app/bower_components/angular-route/angular-route.js', 9 | 'app/bower_components/angular-mocks/angular-mocks.js', 10 | 'app/bower_components/globalize/lib/globalize.js', 11 | 'app/bower_components/globalize/lib/cultures/globalize.culture.en-GB.js', 12 | 'app/js/**/*.js', 13 | 'test/unit/**/*.js' 14 | ], 15 | 16 | autoWatch : true, 17 | 18 | frameworks: ['jasmine'], 19 | 20 | browsers : ['Chrome'], 21 | 22 | plugins : [ 23 | 'karma-chrome-launcher', 24 | 'karma-firefox-launcher', 25 | 'karma-jasmine', 26 | 'karma-junit-reporter' 27 | ], 28 | 29 | junitReporter : { 30 | outputFile: 'test_out/unit.xml', 31 | suite: 'unit' 32 | } 33 | 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /Dist/Angular/index.html: -------------------------------------------------------------------------------- 1 | Test SPA Angular
-------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/investment-page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.investmentPage', []) 4 | .controller('InvestmentPageCtrl', ['$scope', '$routeParams', 'backEndServer', function ($scope, $routeParams, backEndServer) { 5 | $scope.message = "Investment " + $routeParams.id; 6 | 7 | backEndServer.getInvestment({ id: $routeParams.id }).then(function (data) { 8 | $scope.name = data.name; 9 | $scope.sector = data.sector; 10 | $scope.transactions = data.transactions; 11 | $scope.returnOnInvestment = data.analysis.returnOnInvestment; 12 | $scope.open = data.analysis.open; 13 | }); 14 | 15 | $scope.chartOptions = { 16 | x: 'date', 17 | y: 'amount', 18 | classFn: function (d) { 19 | return d.valuation ? "valuation" 20 | : d.amount > 0 ? "invested" 21 | : "returned"; 22 | } 23 | }; 24 | }]); 25 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/require.config.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // Resolve all AMD modules relative to the 'src' directory, to produce the 3 | // same behavior that occurs at runtime 4 | require.baseUrl = '../src/'; 5 | 6 | // It's not obvious, but this is a way of making Jasmine load and run in an AMD environment 7 | // Credit: http://stackoverflow.com/a/20851265 8 | var jasminePath = '../test/bower_modules/jasmine/lib/jasmine-core/'; 9 | require.paths['jasmine'] = jasminePath + 'jasmine'; 10 | require.paths['jasmine-html'] = jasminePath + 'jasmine-html'; 11 | require.paths['jasmine-boot'] = jasminePath + 'boot'; 12 | require.shim['jasmine'] = { exports: 'window.jasmineRequire' }; 13 | require.shim['jasmine-html'] = { deps: ['jasmine'], exports: 'window.jasmineRequire' }; 14 | require.shim['jasmine-boot'] = { deps: ['jasmine', 'jasmine-html'], exports: 'window.jasmineRequire' }; 15 | 16 | require.paths['jquery-mockjax'] = '../test/bower_modules/jquery-mockjax/jquery.mockjax'; 17 | require.shim['jquery-mockjax'] = { deps: ['jquery'] }; 18 | })(); 19 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/nav-bar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/transactions-component/transactions-component.js: -------------------------------------------------------------------------------- 1 | define(['knockout', 'text!./transactions-component.html', 'extensions/charts'], function (ko, templateMarkup) { 2 | 3 | function TransactionsComponent(params) { 4 | this.transactions = ko.observableArray(); 5 | 6 | var getTransactions = function () { 7 | $.getJSON('http://localhost:54361/getTransactions', params.search.json(), this.transactions); 8 | }; 9 | 10 | getTransactions.call(this); 11 | var subscription = params.search.json.subscribe(getTransactions, this); 12 | this.dispose = function () { subscription.dispose(); }; 13 | 14 | this.chartOptions = { 15 | x: 'date', 16 | y: 'amount', 17 | classFn: function (d) { 18 | return d.valuation ? "valuation" 19 | : d.amount > 0 ? "invested" 20 | : "returned"; 21 | } 22 | }; 23 | } 24 | 25 | return { viewModel: TransactionsComponent, template: templateMarkup }; 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/e2e/scenarios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* https://github.com/angular/protractor/blob/master/docs/getting-started.md */ 4 | 5 | describe('my app', function() { 6 | 7 | browser.get('/'); 8 | 9 | it('should automatically redirect to /home when location hash/fragment is empty', function() { 10 | expect(browser.getLocationAbsUrl()).toMatch("/home"); 11 | }); 12 | 13 | 14 | describe('home', function() { 15 | 16 | beforeEach(function() { 17 | browser.get('/#/home'); 18 | }); 19 | 20 | 21 | it('should render home when user navigates to /home', function() { 22 | expect(element.all(by.css('investments-component .panel-heading h3')).first().getText()). 23 | toMatch(/Investments/); 24 | }); 25 | 26 | }); 27 | 28 | 29 | describe('about', function() { 30 | 31 | beforeEach(function() { 32 | browser.get('/#/about'); 33 | }); 34 | 35 | 36 | it('should render about when user navigates to /about', function () { 37 | expect(element.all(by.css('.panel-heading h2')).first().getText()). 38 | toMatch(/About/); 39 | }); 40 | 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 DevAndyLee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/transactions-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.transactionsComponent', []) 4 | .directive('transactionsComponent', function () { 5 | return { 6 | restrict: 'E', 7 | scope: { 8 | search: '=' 9 | }, 10 | templateUrl: 'partials/transactions-component.html', 11 | controller: 'TransactionsComponentCtrl' 12 | }; 13 | }) 14 | .controller('TransactionsComponentCtrl', ['$scope', 'backEndServer', function ($scope, backEndServer) { 15 | $scope.$watch('search.name', function () { 16 | backEndServer.getTransactions($scope.search.json()).then(function (data) { 17 | $scope.transactions = data; 18 | }); 19 | }); 20 | 21 | $scope.chartOptions = { 22 | x: 'date', 23 | y: 'amount', 24 | classFn: function (d) { 25 | return d.valuation ? "valuation" 26 | : d.amount > 0 ? "invested" 27 | : "returned"; 28 | } 29 | }; 30 | }]); 31 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/mock_server/mock-server-stub.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "mock-server", "jquery-mockjax"], function ($, mockServer) { 2 | 3 | $.mockjax({ 4 | url: 'http://localhost:54361/analysis', 5 | responseTime: 30, response: function (settings) { 6 | this.responseText = mockServer.analysis(settings.data.name); 7 | } 8 | }); 9 | 10 | $.mockjax({ 11 | url: 'http://localhost:54361/getSectors', 12 | responseTime: 30, response: function (settings) { 13 | this.responseText = mockServer.getSectors(settings.data.name); 14 | } 15 | }); 16 | 17 | $.mockjax({ 18 | url: 'http://localhost:54361/getTransactions', 19 | responseTime: 30, response: function (settings) { 20 | this.responseText = mockServer.getTransactions(settings.data.name); 21 | } 22 | }); 23 | 24 | $.mockjax({ 25 | url: 'http://localhost:54361/getInvestment', 26 | responseTime: 30, response: function (settings) { 27 | this.responseText = mockServer.getInvestment(parseInt(settings.data.id)); 28 | } 29 | }); 30 | 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/filtersSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for filters go here */ 4 | 5 | describe('filter', function() { 6 | beforeEach(module('testSPA.filters')); 7 | 8 | describe('custom date', function() { 9 | it('should format a date as text', inject(function(customDateFilter) { 10 | expect(customDateFilter('2012-06-10T00:00:00')).toEqual('10 June 2012'); 11 | })); 12 | }); 13 | 14 | describe('duration', function () { 15 | it('should format number to two decimal places', inject(function (durationFilter) { 16 | expect(durationFilter(5.678)).toEqual('5.68'); 17 | })); 18 | }); 19 | 20 | describe('custom currency', function () { 21 | it('should format number as currency with £ symbol', inject(function (customCurrencyFilter) { 22 | expect(customCurrencyFilter(5.678)).toEqual('£5.68'); 23 | })); 24 | }); 25 | 26 | describe('percent', function () { 27 | it('should format number as percentage with no decimal places', inject(function (percentFilter) { 28 | expect(percentFilter(0.5678)).toEqual('57%'); 29 | })); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/js/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Directives */ 4 | 5 | 6 | angular.module('testSPA.directives', []) 7 | .directive('chart', [function() { 8 | var link = function link(scope, element, attrs) { 9 | 10 | var options = attrs.options ? scope[attrs.options] : {}; 11 | var ChartType = 12 | attrs.chart === "bar" ? window.testSPA.SVGBarChart 13 | : attrs.chart === "column" ? window.testSPA.SVGColumnChart 14 | : null; 15 | if (!ChartType) throw "Invalid chart type '" + attrs.chart + "'"; 16 | 17 | // Create the chart object bound to the element 18 | var chart = new ChartType(element[0], options); 19 | 20 | scope.$watch(attrs.data, function (value) { 21 | chart.setData(scope[attrs.data]); 22 | }); 23 | 24 | // Handle resizing 25 | var resize = function () { 26 | chart.resized(); 27 | }; 28 | 29 | $(window).bind("resize", resize); 30 | element.on('$destroy', function () { 31 | $(window).unbind("resize", resize); 32 | }); 33 | 34 | }; 35 | 36 | return { 37 | link: link 38 | }; 39 | }]); 40 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/nav-bar/nav-bar.html: -------------------------------------------------------------------------------- 1 | 6 | 32 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/extensions/custom-format.js: -------------------------------------------------------------------------------- 1 | define(["knockout", 'globalize', 'globalize-en'], function (ko, Globalize) { 2 | Globalize.culture("en-GB"); 3 | 4 | // Format date using Globalize library 5 | ko.bindingHandlers.date = { 6 | update: function (element, valueAccessor) { 7 | var value = ko.unwrap(valueAccessor()); 8 | $(element).text(Globalize.format(new Date(Date.parse(value)), 'D')); 9 | } 10 | }; 11 | 12 | ko.bindingHandlers.duration = { 13 | update: function (element, valueAccessor) { 14 | var value = ko.unwrap(valueAccessor()); 15 | $(element).text(Globalize.format(value, 'N2')); 16 | } 17 | }; 18 | 19 | ko.bindingHandlers.currency = { 20 | update: function (element, valueAccessor) { 21 | var value = ko.unwrap(valueAccessor()); 22 | $(element).text(Globalize.format(value, 'C')); 23 | } 24 | }; 25 | 26 | ko.bindingHandlers.percent = { 27 | update: function (element, valueAccessor) { 28 | var value = ko.unwrap(valueAccessor()); 29 | $(element).text(Globalize.format(parseFloat(value) * 100, 'N0') + '%'); 30 | } 31 | }; 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/investments-component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Investments

4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
NameInvestment DateHolding PeriodInvestedReturnReturn %
{{investment.name}}{{investment.startDate | customDate}}{{investment.holdingPeriod | duration}}{{investment.investedAmount | customCurrency}}{{investment.returnAmount | customCurrency}}{{investment.returnOnInvestment | percent}}
28 |
29 |
-------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/investments-component/investments-component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Investments

4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
NameInvestment DateHolding PeriodInvestedReturnReturn %
28 |
29 |
-------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/mock_server/mock-server-stub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('testSPA.mockServer', []) 4 | .service('backEndServer', ["$q", function ($q) { 5 | 6 | var promise = function (callFn) { 7 | var deferred = $q.defer(); 8 | setTimeout(function () { deferred.resolve(callFn()); }, 30); 9 | 10 | return deferred.promise; 11 | }; 12 | 13 | this.analysis = function (params) { 14 | return promise(function () { 15 | return window.testSPA.mockServer.analysis(params.name); 16 | }); 17 | }; 18 | 19 | this.getSectors = function (params) { 20 | return promise(function () { 21 | return window.testSPA.mockServer.getSectors(params.name); 22 | }); 23 | }; 24 | 25 | this.getTransactions = function (params) { 26 | return promise(function () { 27 | return window.testSPA.mockServer.getTransactions(params.name); 28 | }); 29 | }; 30 | 31 | this.getInvestment = function (params) { 32 | return promise(function () { 33 | return window.testSPA.mockServer.getInvestment(parseInt(params.id)); 34 | }); 35 | }; 36 | }]); 37 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/app/startup.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'knockout', './router', 'bootstrap', 'knockout-projections'], function ($, ko, router) { 2 | 3 | // Components can be packaged as AMD modules, such as the following: 4 | ko.components.register('nav-bar', { require: 'components/nav-bar/nav-bar' }); 5 | ko.components.register('home-page', { require: 'components/home-page/home' }); 6 | 7 | // ... or for template-only components, you can just point to a .html file directly: 8 | ko.components.register('about-page', { 9 | template: { require: 'text!components/about-page/about.html' } 10 | }); 11 | 12 | ko.components.register('investments-component', { require: 'components/investments-component/investments-component' }); 13 | ko.components.register('investment-filter', { require: 'components/investment-filter/investment-filter' }); 14 | ko.components.register('investment-page', { require: 'components/investment-page/investment-page' }); 15 | ko.components.register('sector-component', { require: 'components/sector-component/sector-component' }); 16 | ko.components.register('transactions-component', { require: 'components/transactions-component/transactions-component' }); 17 | 18 | // [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.] 19 | 20 | // Start the application 21 | ko.applyBindings({ route: router.currentRoute }); 22 | }); 23 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/extensions/charts.js: -------------------------------------------------------------------------------- 1 | define(["knockout", "svg-bar-chart", "svg-column-chart", "d3"], function (ko, SVGBarChart, SVGColumnChart) { 2 | 3 | var createBinding = function (SVGChart) { 4 | var binding = {}; 5 | binding.init = function (element, valueAccessor, allBindingsAccessor) { 6 | var options = allBindingsAccessor().chartOptions || {}; 7 | 8 | // Create the chart object bound to the element 9 | $(element).data('chart', new SVGChart(element, options)); 10 | binding.update(element, valueAccessor); 11 | 12 | // Handle resizing 13 | var resize = function () { 14 | $(element).data('chart').resized(); 15 | }; 16 | 17 | $(window).bind("resize", resize); 18 | ko.utils.domNodeDisposal.addDisposeCallback(element, function () { 19 | $(window).unbind("resize", resize); 20 | }); 21 | }; 22 | binding.update = function (element, valueAccessor) { 23 | // Update the chart data 24 | var data = ko.unwrap(valueAccessor()); 25 | $(element).data('chart').setData(data); 26 | }; 27 | 28 | return binding; 29 | }; 30 | 31 | ko.bindingHandlers.barChart = createBinding(SVGBarChart); 32 | ko.bindingHandlers.columnChart = createBinding(SVGColumnChart); 33 | }); 34 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/components/investment-page.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'knockout', 'components/investment-page/investment-page', 'jquery-mockjax'], function ($, ko, investmentPage) { 2 | var InvestmentPageViewModel = investmentPage.viewModel; 3 | 4 | var investmentData = { 5 | name: "investment-1", sector: "test-sector", 6 | transactions: [{ amount: 1 }, { amount: 2 }], 7 | analysis: { returnOnInvestment: 32.5, open: true } 8 | }; 9 | 10 | beforeEach(function () { 11 | $.mockjax({ 12 | url: 'http://localhost:54361/getInvestment', data: { id: 23 }, 13 | responseTime: 10, responseText: investmentData 14 | }); 15 | }); 16 | afterEach(function () { 17 | $.mockjaxClear(); 18 | }); 19 | 20 | describe('investment page view model', function () { 21 | it('should initialise and load investment details', function (done) { 22 | // act 23 | var instance = new InvestmentPageViewModel({ id: 23 }); 24 | 25 | // assert 26 | setTimeout(function () { 27 | expect(instance.name()).toEqual("investment-1"); 28 | expect(instance.sector()).toEqual("test-sector"); 29 | 30 | expect(instance.transactions()).toEqual([{ amount: 1 }, { amount: 2 }]); 31 | expect(instance.returnOnInvestment()).toEqual(32.5); 32 | expect(instance.open()).toBe(true); 33 | done(); 34 | }, 20); 35 | }); 36 | 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/investment-page/investment-page.js: -------------------------------------------------------------------------------- 1 | define(['knockout', 'text!./investment-page.html', 'extensions/custom-format', 'extensions/charts'], function (ko, templateMarkup) { 2 | 3 | function InvestmentPage(route) { 4 | var self = this; 5 | this.name = ko.observable(''); 6 | this.sector = ko.observable(''); 7 | this.transactions = ko.observableArray(); 8 | 9 | this.returnOnInvestment = ko.observable(0); 10 | this.open = ko.observable(false); 11 | 12 | $.getJSON('http://localhost:54361/getInvestment', { id: route.id }, function (result) { 13 | self.name(result.name); 14 | self.sector(result.sector); 15 | self.transactions(result.transactions); 16 | 17 | self.returnOnInvestment(result.analysis.returnOnInvestment); 18 | self.open(result.analysis.open); 19 | }, this); 20 | 21 | this.chartOptions = { 22 | x: 'date', 23 | y: 'amount', 24 | classFn: function (d) { 25 | return d.valuation ? "valuation" 26 | : d.amount > 0 ? "invested" 27 | : "returned"; 28 | } 29 | }; 30 | 31 | } 32 | 33 | // This runs when the component is torn down. Put here any logic necessary to clean up, 34 | // for example cancelling setTimeouts or disposing Knockout subscriptions/computeds. 35 | InvestmentPage.prototype.dispose = function() { }; 36 | 37 | return { viewModel: InvestmentPage, template: templateMarkup }; 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E0E0E0; 3 | } 4 | 5 | #page { 6 | margin: 80px 15px 0 15px; 7 | } 8 | 9 | .panel { 10 | background-color: #fff; 11 | padding: 9px; 12 | } 13 | 14 | .panel h2, .panel h3 { 15 | margin: 0; 16 | } 17 | 18 | .investments { 19 | height: 520px; 20 | overflow-y: auto; 21 | } 22 | 23 | .investments .table td:not(:first-child), .investments .table th:not(:first-child) { 24 | text-align: right; 25 | } 26 | 27 | .filter-form { 28 | width: 250px; 29 | position: fixed; 30 | right: 0; 31 | top: 0; 32 | padding: 8px; 33 | z-index: 1040; 34 | } 35 | 36 | .table-hover tbody tr { 37 | cursor: pointer; 38 | } 39 | 40 | .transactions-chart{ 41 | height: 250px; 42 | } 43 | 44 | .chart .axis line, .chart .axis path { 45 | fill: none; 46 | stroke-width: 1px; 47 | stroke: #505050; 48 | shape-rendering: crispEdges; 49 | } 50 | 51 | .chart .y-axis line, .chart .y-axis path { 52 | visibility: hidden; 53 | stroke: none; 54 | } 55 | 56 | .chart .bar, .chart .column { 57 | shape-rendering: crispEdges; 58 | stroke: none; 59 | } 60 | .chart .bar.series-0 { 61 | fill: #a94442; 62 | } 63 | .chart .bar.series-1 { 64 | fill: #3c763d; 65 | } 66 | 67 | .chart .column.invested { 68 | fill: #a94442; 69 | } 70 | .chart .column.returned { 71 | fill: #3c763d; 72 | } 73 | .chart .column.valuation { 74 | fill: #31708f; 75 | } 76 | 77 | @media (max-width: 768px) { 78 | .filter-form { 79 | right: 64px; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #E0E0E0; 3 | } 4 | 5 | #page { 6 | margin: 80px 15px 0 15px; 7 | } 8 | 9 | .panel { 10 | background-color: #fff; 11 | padding: 9px; 12 | } 13 | 14 | .panel h2, .panel h3 { 15 | margin: 0; 16 | } 17 | 18 | .investments { 19 | height: 520px; 20 | overflow-y: auto; 21 | } 22 | 23 | .investments .table td:not(:first-child), .investments .table th:not(:first-child) { 24 | text-align: right; 25 | } 26 | 27 | .filter-form { 28 | width: 250px; 29 | position: fixed; 30 | right: 0; 31 | top: 0; 32 | padding: 8px; 33 | z-index: 1040; 34 | } 35 | 36 | .table-hover tbody tr { 37 | cursor: pointer; 38 | } 39 | 40 | .transactions-chart{ 41 | height: 250px; 42 | } 43 | 44 | .chart .axis line, .chart .axis path { 45 | fill: none; 46 | stroke-width: 1px; 47 | stroke: #505050; 48 | shape-rendering: crispEdges; 49 | } 50 | 51 | .chart .y-axis line, .chart .y-axis path { 52 | visibility: hidden; 53 | stroke: none; 54 | } 55 | 56 | .chart .bar, .chart .column { 57 | shape-rendering: crispEdges; 58 | stroke: none; 59 | } 60 | .chart .bar.series-0 { 61 | fill: #a94442; 62 | } 63 | .chart .bar.series-1 { 64 | fill: #3c763d; 65 | } 66 | 67 | .chart .column.invested { 68 | fill: #a94442; 69 | } 70 | .chart .column.returned { 71 | fill: #3c763d; 72 | } 73 | .chart .column.valuation { 74 | fill: #31708f; 75 | } 76 | 77 | 78 | @media (max-width: 768px) { 79 | .filter-form { 80 | right: 64px; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/app/router.js: -------------------------------------------------------------------------------- 1 | define(["knockout", "crossroads", "hasher", "server-stub"], function (ko, crossroads, hasher) { 2 | 3 | // This module configures crossroads.js, a routing library. If you prefer, you 4 | // can use any other routing library (or none at all) as Knockout is designed to 5 | // compose cleanly with external libraries. 6 | // 7 | // You *don't* have to follow the pattern established here (each route entry 8 | // specifies a 'page', which is a Knockout component) - there's nothing built into 9 | // Knockout that requires or even knows about this technique. It's just one of 10 | // many possible ways of setting up client-side routes. 11 | 12 | return new Router({ 13 | routes: [ 14 | { url: '', params: { page: 'home-page' } }, 15 | { url: 'about', params: { page: 'about-page' } }, 16 | { url: 'investment/{id}', params: { page: 'investment-page' } } 17 | ] 18 | }); 19 | 20 | function Router(config) { 21 | var currentRoute = this.currentRoute = ko.observable({}); 22 | 23 | ko.utils.arrayForEach(config.routes, function(route) { 24 | crossroads.addRoute(route.url, function(requestParams) { 25 | currentRoute(ko.utils.extend(requestParams, route.params)); 26 | }); 27 | }); 28 | 29 | activateCrossroads(); 30 | } 31 | 32 | function activateCrossroads() { 33 | function parseHash(newHash, oldHash) { crossroads.parse(newHash); } 34 | crossroads.normalizeFn = crossroads.NORM_AS_OBJECT; 35 | hasher.initialized.add(parseHash); 36 | hasher.changed.add(parseHash); 37 | hasher.init(); 38 | } 39 | }); -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-seed", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "A starter project for AngularJS", 6 | "repository": "https://github.com/angular/angular-seed", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "bower": "^1.3.1", 10 | "chalk": "~0.4.0", 11 | "combined-stream": "0.0.5", 12 | "event-stream": "~3.1.0", 13 | "gulp": "^3.8.6", 14 | "gulp-angular-templatecache": "^1.2.1", 15 | "gulp-clean": "~0.2.4", 16 | "gulp-concat": "~2.2.0", 17 | "gulp-html-replace": "~1.0.0", 18 | "gulp-minify-css": "^0.3.7", 19 | "gulp-minify-html": "^0.1.4", 20 | "gulp-replace": "~0.2.0", 21 | "gulp-requirejs-bundler": "^0.1.1", 22 | "gulp-uglify": "~0.2.1", 23 | "http-server": "^0.6.1", 24 | "karma": "~0.10", 25 | "karma-junit-reporter": "^0.2.2", 26 | "protractor": "~0.20.1", 27 | "shelljs": "^0.2.6" 28 | }, 29 | "scripts": { 30 | "postinstall": "bower install", 31 | "prestart": "npm install", 32 | "start": "http-server -a localhost -p 8000", 33 | "pretest": "npm install", 34 | "test": "karma start test/karma.conf.js", 35 | "test-single-run": "karma start test/karma.conf.js --single-run", 36 | "preupdate-webdriver": "npm install", 37 | "update-webdriver": "webdriver-manager update", 38 | "preprotractor": "npm run update-webdriver", 39 | "protractor": "protractor test/protractor-conf.js", 40 | "update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('app/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\"" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/partials/investment-page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{name}}

5 |
6 |
7 |

8 |

9 |

Details

10 |
11 |
Status
12 |
{{open ? 'Open' : 'Closed'}}
13 |
Sector
14 |
{{sector}}
15 |
Return on Investment
16 |
{{returnOnInvestment | percent}}
17 |
18 |
19 |

20 |

21 |

22 |

Transactions

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
{{transaction.date | customDate}}{{transaction.amount | customCurrency}}{{transaction.valuation ? 'Valuation' : ''}}
32 |
33 |
34 |
35 |
36 |

37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/app/require.config.js: -------------------------------------------------------------------------------- 1 | // require.js looks for the following global when initializing 2 | var require = { 3 | baseUrl: ".", 4 | paths: { 5 | "bootstrap": "bower_modules/components-bootstrap/js/bootstrap.min", 6 | "crossroads": "bower_modules/crossroads/dist/crossroads.min", 7 | "hasher": "bower_modules/hasher/dist/js/hasher.min", 8 | "jquery": "bower_modules/jquery/dist/jquery", 9 | "knockout": "bower_modules/knockout/dist/knockout", 10 | "knockout-projections": "bower_modules/knockout-projections/dist/knockout-projections", 11 | "signals": "bower_modules/js-signals/dist/signals.min", 12 | "text": "bower_modules/requirejs-text/text", 13 | "globalize": "bower_modules/globalize/lib/globalize", 14 | "globalize-en": "bower_modules/globalize/lib/cultures/globalize.culture.en-GB", 15 | "d3": "bower_modules/d3/d3", 16 | "jquery-mockjax": "bower_modules/jquery-mockjax/jquery.mockjax", 17 | "mock-server": "mock_server/mock-server", 18 | "server-stub": "mock_server/mock-server-stub", 19 | "svg-bar-chart": "charts/svg-bar-chart", 20 | "svg-column-chart": "charts/svg-column-chart" 21 | }, 22 | shim: { 23 | "bootstrap": { deps: ["jquery"] }, 24 | "globalize": { deps: ["jquery"], exports: 'window.Globalize' }, 25 | "globalize-en": { deps: ["globalize"] }, 26 | "d3": { exports: 'window.d3' }, 27 | "jquery-mockjax": { deps: ["jquery"] }, 28 | "mock-server": { exports: 'window.testSPA.mockServer' }, 29 | "svg-bar-chart": { exports: 'window.testSPA.SVGBarChart' }, 30 | "svg-column-chart": { exports: 'window.testSPA.SVGColumnChart' } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/src/components/investment-page/investment-page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Investment

5 |
6 |
7 |

8 |

9 |

Details

10 |
11 |
Status
12 |
13 |
Sector
14 |
15 |
Return on Investment
16 |
17 |
18 |
19 |

20 |

21 |

22 |

Transactions

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |

37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular-vs-Knockout 2 | =================== 3 | 4 | An implementation of the same SPA in both AngularJS and KnockoutJS (with CrossroadsJS and RequireJS) 5 | 6 | The accompanying blog post is here: 7 | [Single Page Applications - Angular vs Knockout](http://www.scottlogic.com/blog/2014/07/30/spa-angular-knockout.html) 8 | 9 | ###Introduction 10 | The Angular version was built up from the angular-seed project, which is here: https://github.com/angular/angular-seed 11 | 12 | The Knockout version was scaffolded using yeoman, and includes CrossroadsJS and RequireJS. 13 | 14 | A live demo of both apps is available here: 15 | 16 | [Knockout Version](https://rawgit.com/DevAndyLee/Angular-vs-Knockout/master/Dist/Knockout/index.html) 17 | 18 | [Angular Version](https://rawgit.com/DevAndyLee/Angular-vs-Knockout/master/Dist/Angular/index.html) 19 | 20 | The app presents a list of investments in a portfolio, and lets you filter them by name while dynamically updating all the components. 21 | You can also click through view details of each investment or view the about page, all within the same SPA. 22 | 23 | Html, javascript and css is packaged up and minified for the app home screen, while navigating to additional screens triggers a request for the additional content. 24 | 25 | Both apps have a mocked back end, so that they behave as though they're making ajax requests without actually needing a server to respond to those requests. 26 | 27 | 28 | ###Installation: 29 | 30 | From both the AngularFrontEnd and KnockoutFrontEnd folders: 31 | ``` 32 | npm install 33 | ``` 34 | 35 | ###Start the web servers: 36 | 37 | Use the node http server to host the content locally: 38 | ``` 39 | call http-server AngularFrontEnd\app –p 8082 -o -c-1 40 | call http-server KnockoutFrontEnd\src –p 8081 -o -c-1 41 | ``` 42 | 43 | ###Build and optimise: 44 | 45 | From both the AngularFrontEnd and KnockoutFrontEnd folders: 46 | ``` 47 | gulp 48 | ``` 49 | 50 | Gulp creates a /dist folder containing the compiled and optimised version of the site. 51 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/extensions/custom-format.js: -------------------------------------------------------------------------------- 1 | define(["knockout", 'extensions/custom-format'], function (ko) { 2 | 3 | describe('custom format', function () { 4 | var $element, element; 5 | beforeEach(function () { 6 | $element = $(''); 7 | element = $element.get(0); 8 | }); 9 | 10 | describe('date', function () { 11 | it('should format a date as text', function () { 12 | // act 13 | ko.bindingHandlers.date.update(element, function () { return ko.observable('2012-06-10T00:00:00') }); 14 | 15 | // assert 16 | expect($element.text()).toEqual('10 June 2012'); 17 | }); 18 | 19 | }); 20 | 21 | describe('duration', function () { 22 | it('should format number to two decimal places', function () { 23 | // act 24 | ko.bindingHandlers.duration.update(element, function () { return ko.observable(5.678) }); 25 | 26 | // assert 27 | expect($element.text()).toEqual('5.68'); 28 | }); 29 | 30 | }); 31 | 32 | describe('currency', function () { 33 | it('should format number as currency with £ symbol', function () { 34 | // act 35 | ko.bindingHandlers.currency.update(element, function () { return ko.observable(5.678) }); 36 | 37 | // assert 38 | expect($element.text()).toEqual('£5.68'); 39 | }); 40 | 41 | }); 42 | 43 | describe('percent', function () { 44 | it('should format number as percentage with no decimal places', function () { 45 | // act 46 | ko.bindingHandlers.percent.update(element, function () { return ko.observable(0.5678) }); 47 | 48 | // assert 49 | expect($element.text()).toEqual('57%'); 50 | }); 51 | 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/components/sector-component.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'knockout', 'components/sector-component/sector-component', 'jquery-mockjax'], function ($, ko, sectorComponent) { 2 | var SectorComponentViewModel = sectorComponent.viewModel; 3 | 4 | beforeEach(function () { 5 | $.mockjax({ 6 | url: 'http://localhost:54361/getSectors', data: { name: '' }, 7 | responseTime: 10, responseText: [{ name: "sector-1" }] 8 | }); 9 | }); 10 | afterEach(function () { 11 | $.mockjaxClear(); 12 | }); 13 | 14 | describe('sector component view model', function () { 15 | it('should initialise and load sectors', function (done) { 16 | // act 17 | var instance = new SectorComponentViewModel({ search: { json: ko.observable() } }); 18 | 19 | // assert 20 | setTimeout(function () { 21 | expect(instance.sectors()).toEqual([{ name: "sector-1" }]); 22 | done(); 23 | }, 20); 24 | }); 25 | 26 | it('should query filtered sector list when search changes', function (done) { 27 | // arrange 28 | $.mockjax({ 29 | url: 'http://localhost:54361/getSectors', data: { name: 'test-filter' }, 30 | responseTime: 10, responseText: [{ name: "filtered-1" }] 31 | }); 32 | 33 | var search = { json: ko.observable() }; 34 | var instance = new SectorComponentViewModel({ search: search }); 35 | 36 | setTimeout(function () { 37 | // Initial loaded list 38 | expect(instance.sectors()).toEqual([{ name: "sector-1" }]); 39 | 40 | // act 41 | search.json({ name: 'test-filter' }); 42 | 43 | setTimeout(function () { 44 | expect(instance.sectors()).toEqual([{ name: "filtered-1" }]); 45 | done(); 46 | }, 20); 47 | }, 20); 48 | }); 49 | 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | // base path that will be used to resolve all patterns (eg. files, exclude) 5 | basePath: '', 6 | 7 | 8 | // frameworks to use 9 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 10 | frameworks: ['jasmine', 'requirejs'], 11 | 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'src/app/require.config.js', 16 | 'test/require.config.js', 17 | 'test/SpecRunner.karma.js', 18 | { pattern: 'src/**/*.js', included: false }, 19 | { pattern: 'src/**/*.html', included: false }, 20 | { pattern: 'test/**/*.js', included: false } 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [ 26 | 27 | ], 28 | 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: { 33 | '**/require.config.js': ['requireglobal'] 34 | }, 35 | 36 | 37 | // test results reporter to use 38 | // possible values: 'dots', 'progress' 39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 40 | reporters: ['progress'], 41 | 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | 47 | // enable / disable colors in the output (reporters and logs) 48 | colors: true, 49 | 50 | 51 | // level of logging 52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 53 | logLevel: config.LOG_INFO, 54 | 55 | 56 | // enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: true, 58 | 59 | 60 | // start these browsers 61 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 62 | browsers: ['Chrome'], 63 | 64 | 65 | // Continuous Integration mode 66 | // if true, Karma captures browsers, runs the tests and exits 67 | singleRun: false 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/components/transactions-component.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'knockout', 'components/transactions-component/transactions-component', 'jquery-mockjax'], function ($, ko, transactionsComponent) { 2 | var TransactionsComponentViewModel = transactionsComponent.viewModel; 3 | 4 | beforeEach(function () { 5 | $.mockjax({ 6 | url: 'http://localhost:54361/getTransactions', data: { name: '' }, 7 | responseTime: 10, responseText: [{ name: "trans-1" }] 8 | }); 9 | }); 10 | afterEach(function () { 11 | $.mockjaxClear(); 12 | }); 13 | 14 | describe('transactions component view model', function () { 15 | it('should initialise and load full transactions list', function (done) { 16 | // act 17 | var instance = new TransactionsComponentViewModel({ search: { json: ko.observable() } }); 18 | 19 | // assert 20 | setTimeout(function () { 21 | expect(instance.transactions()).toEqual([{ name: "trans-1" }]); 22 | done(); 23 | }, 20); 24 | }); 25 | 26 | it('should query filtered transaction list when search changes', function (done) { 27 | // arrange 28 | $.mockjax({ 29 | url: 'http://localhost:54361/getTransactions', data: { name: 'test-filter' }, 30 | responseTime: 10, responseText: [{ name: "filtered-1" }] 31 | }); 32 | 33 | var search = { json: ko.observable() }; 34 | var instance = new TransactionsComponentViewModel({ search: search }); 35 | 36 | setTimeout(function () { 37 | // Initial loaded list 38 | expect(instance.transactions()).toEqual([{ name: "trans-1" }]); 39 | 40 | // act 41 | search.json({ name: 'test-filter' }); 42 | 43 | setTimeout(function () { 44 | expect(instance.transactions()).toEqual([{ name: "filtered-1" }]); 45 | done(); 46 | }, 20); 47 | }, 20); 48 | }); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/investmentPageSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('investment-page', function () { 4 | var controller, scope, backEndServer; 5 | 6 | beforeEach(function () { 7 | this.addMatchers({ 8 | toEqualData: function (expected) { 9 | return angular.equals(this.actual, expected); 10 | } 11 | }); 12 | }); 13 | 14 | beforeEach(module('testSPA.investmentPage')); 15 | 16 | beforeEach(inject(function ($rootScope, $controller) { 17 | backEndServer = { 18 | data: {}, done: false, isDone: function () { return backEndServer.done; }, 19 | getInvestment: jasmine.createSpy().andCallFake(function (params) { 20 | return { 21 | then: function (fn) { 22 | setTimeout(function () { fn(backEndServer.data); backEndServer.done = true; }, 10); 23 | } 24 | }; 25 | }) 26 | }; 27 | 28 | scope = $rootScope.$new(); 29 | scope.search = { name: '', json: function () { return { name: scope.search.name }; } }; 30 | controller = $controller('InvestmentPageCtrl', { $scope: scope, $routeParams: { id: 23 }, backEndServer: backEndServer }); 31 | })); 32 | 33 | it('should initialise and load full investments list', function () { 34 | runs(function () { 35 | backEndServer.data = { 36 | name: "investment-1", sector: "test-sector", 37 | transactions: [{ amount: 1 }, { amount: 2 }], 38 | analysis: { returnOnInvestment: 32.5, open: true } 39 | }; 40 | expect(scope.name).toBeUndefined(); 41 | }); 42 | 43 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 44 | 45 | runs(function () { 46 | expect(backEndServer.getInvestment).toHaveBeenCalledWith({ id: 23 }); 47 | 48 | expect(scope.name).toEqual('investment-1'); 49 | expect(scope.sector).toEqual('test-sector'); 50 | expect(scope.transactions).toEqual([{ amount: 1 }, { amount: 2 }]); 51 | expect(scope.returnOnInvestment).toEqual(32.5); 52 | expect(scope.open).toBe(true); 53 | }); 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/sectorComponentSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('sector-component', function () { 4 | var controller, scope, backEndServer; 5 | 6 | beforeEach(function () { 7 | this.addMatchers({ 8 | toEqualData: function (expected) { 9 | return angular.equals(this.actual, expected); 10 | } 11 | }); 12 | }); 13 | 14 | beforeEach(module('testSPA.sectorComponent')); 15 | 16 | beforeEach(inject(function ($rootScope, $controller) { 17 | backEndServer = { 18 | data: {}, done: false, isDone: function () { return backEndServer.done; }, 19 | getSectors: jasmine.createSpy().andCallFake(function (params) { 20 | return { 21 | then: function (fn) { 22 | setTimeout(function () { fn(backEndServer.data); backEndServer.done = true; }, 10); 23 | } 24 | }; 25 | }) 26 | }; 27 | 28 | scope = $rootScope.$new(); 29 | scope.search = { name: '', json: function () { return { name: scope.search.name }; } }; 30 | controller = $controller('SectorComponentCtrl', { $scope: scope, backEndServer: backEndServer }); 31 | })); 32 | 33 | it('should initialise and load full sector list', function () { 34 | runs(function () { 35 | backEndServer.data = [{ name: 'sector-1' }]; 36 | expect(scope.sectors).toBeUndefined(); 37 | scope.$digest(); 38 | }); 39 | 40 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 41 | 42 | runs(function () { 43 | expect(scope.sectors).toEqualData([{ name: 'sector-1' }]); 44 | }); 45 | }); 46 | 47 | it('should query filtered sector list when search changes', function (done) { 48 | runs(function () { 49 | backEndServer.data = [{ name: 'filtered-1' }]; 50 | expect(scope.sectors).toBeUndefined(); 51 | 52 | scope.search.name = 'test-filter'; 53 | scope.$digest(); 54 | }); 55 | 56 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 57 | 58 | runs(function () { 59 | expect(backEndServer.getSectors).toHaveBeenCalledWith({ name: 'test-filter' }); 60 | expect(scope.sectors).toEqualData([{ name: 'filtered-1' }]); 61 | }); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/transactionsComponentSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('transactions-component', function () { 4 | var controller, scope, backEndServer; 5 | 6 | beforeEach(function () { 7 | this.addMatchers({ 8 | toEqualData: function (expected) { 9 | return angular.equals(this.actual, expected); 10 | } 11 | }); 12 | }); 13 | 14 | beforeEach(module('testSPA.transactionsComponent')); 15 | 16 | beforeEach(inject(function ($rootScope, $controller) { 17 | backEndServer = { 18 | data: {}, done: false, isDone: function () { return backEndServer.done; }, 19 | getTransactions: jasmine.createSpy().andCallFake(function (params) { 20 | return { 21 | then: function (fn) { 22 | setTimeout(function () { fn(backEndServer.data); backEndServer.done = true; }, 10); 23 | } 24 | }; 25 | }) 26 | }; 27 | 28 | scope = $rootScope.$new(); 29 | scope.search = { name: '', json: function () { return { name: scope.search.name }; } }; 30 | controller = $controller('TransactionsComponentCtrl', { $scope: scope, backEndServer: backEndServer }); 31 | })); 32 | 33 | it('should initialise and load full transactions list', function () { 34 | runs(function () { 35 | backEndServer.data = [{ name: 'trans-1' }]; 36 | expect(scope.transactions).toBeUndefined(); 37 | scope.$digest(); 38 | }); 39 | 40 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 41 | 42 | runs(function () { 43 | expect(scope.transactions).toEqualData([{ name: 'trans-1' }]); 44 | }); 45 | }); 46 | 47 | it('should query filtered transaction list when search changes', function (done) { 48 | runs(function () { 49 | backEndServer.data = [{ name: 'filtered-1' }]; 50 | expect(scope.transactions).toBeUndefined(); 51 | 52 | scope.search.name = 'test-filter'; 53 | scope.$digest(); 54 | }); 55 | 56 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 57 | 58 | runs(function () { 59 | expect(backEndServer.getTransactions).toHaveBeenCalledWith({ name: 'test-filter' }); 60 | expect(scope.transactions).toEqualData([{ name: 'filtered-1' }]); 61 | }); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/test/components/investments-component.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 'knockout', 'components/investments-component/investments-component', 'jquery-mockjax'], function ($, ko, investmentsComponent) { 2 | var InvestmentsComponentViewModel = investmentsComponent.viewModel; 3 | 4 | beforeEach(function () { 5 | $.mockjax({ 6 | url: 'http://localhost:54361/analysis', data: { name: '' }, 7 | responseTime: 10, responseText: [{ name: "investment-1" }] 8 | }); 9 | }); 10 | afterEach(function () { 11 | $.mockjaxClear(); 12 | }); 13 | 14 | describe('investments component view model', function () { 15 | it('should initialise and load full investments list', function (done) { 16 | // act 17 | var instance = new InvestmentsComponentViewModel({ search: { json: ko.observable() } }); 18 | 19 | // assert 20 | setTimeout(function () { 21 | expect(instance.investments()).toEqual([{ name: "investment-1" }]); 22 | done(); 23 | }, 20); 24 | }); 25 | 26 | it('should query filtered investment list when search changes', function (done) { 27 | // arrange 28 | $.mockjax({ 29 | url: 'http://localhost:54361/analysis', data: { name: 'test-filter' }, 30 | responseTime: 10, responseText: [{ name: "filtered-1" }] 31 | }); 32 | 33 | var search = { json: ko.observable() }; 34 | var instance = new InvestmentsComponentViewModel({ search: search }); 35 | 36 | setTimeout(function () { 37 | // Initial loaded list 38 | expect(instance.investments()).toEqual([{ name: "investment-1" }]); 39 | 40 | // act 41 | search.json({ name: 'test-filter' }); 42 | 43 | setTimeout(function () { 44 | expect(instance.investments()).toEqual([{ name: "filtered-1" }]); 45 | done(); 46 | }, 20); 47 | }, 20); 48 | }); 49 | 50 | it('showInvestment should navigate to investment page', function () { 51 | // arrange 52 | var instance = new InvestmentsComponentViewModel({ search: { json: ko.observable() } }); 53 | 54 | // act 55 | instance.showInvestment.call({ id: 23 }); 56 | 57 | // assert 58 | expect(location.hash).toEqual("#investment/23"); 59 | }); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /Dist/Knockout/investment-page.js: -------------------------------------------------------------------------------- 1 | define("text!components/investment-page/investment-page.html",[],function(){return'
\r\n
\r\n
\r\n

Investment

\r\n
\r\n
\r\n

\r\n

\r\n

Details

\r\n
\r\n
Status
\r\n
\r\n
Sector
\r\n
\r\n
Return on Investment
\r\n
\r\n
\r\n
\r\n

\r\n

\r\n

\r\n

Transactions

\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n

\r\n
\r\n
\r\n
\n'}),define("components/investment-page/investment-page",["knockout","text!./investment-page.html","extensions/custom-format","extensions/charts"],function(n,t){function e(t){var e=this;this.name=n.observable(""),this.sector=n.observable(""),this.transactions=n.observableArray(),this.returnOnInvestment=n.observable(0),this.open=n.observable(!1),$.getJSON("http://localhost:54361/getInvestment",{id:t.id},function(n){e.name(n.name),e.sector(n.sector),e.transactions(n.transactions),e.returnOnInvestment(n.analysis.returnOnInvestment),e.open(n.analysis.open)},this),this.chartOptions={x:"date",y:"amount",classFn:function(n){return n.valuation?"valuation":n.amount>0?"invested":"returned"}}}return e.prototype.dispose=function(){},{viewModel:e,template:t}}); -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/test/unit/investmentsComponentSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('investments-component', function () { 4 | var controller, scope, backEndServer; 5 | 6 | beforeEach(function () { 7 | this.addMatchers({ 8 | toEqualData: function (expected) { 9 | return angular.equals(this.actual, expected); 10 | } 11 | }); 12 | }); 13 | 14 | beforeEach(module('testSPA.investmentsComponent')); 15 | 16 | beforeEach(inject(function ($rootScope, $controller) { 17 | backEndServer = { 18 | data: {}, done: false, isDone: function () { return backEndServer.done; }, 19 | analysis: jasmine.createSpy().andCallFake(function (params) { 20 | return { 21 | then: function (fn) { 22 | setTimeout(function () { fn(backEndServer.data); backEndServer.done = true; }, 10); 23 | } 24 | }; 25 | }) 26 | }; 27 | 28 | scope = $rootScope.$new(); 29 | scope.search = { name: '', json: function () { return { name: scope.search.name }; } }; 30 | controller = $controller('InvestmentsComponentCtrl', { $scope: scope, backEndServer: backEndServer }); 31 | })); 32 | 33 | it('should initialise and load full investments list', function () { 34 | runs(function () { 35 | backEndServer.data = [{ name: 'investment-1' }]; 36 | expect(scope.investments).toBeUndefined(); 37 | scope.$digest(); 38 | }); 39 | 40 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 41 | 42 | runs(function () { 43 | expect(scope.investments).toEqualData([{ name: 'investment-1' }]); 44 | }); 45 | }); 46 | 47 | it('should query filtered investment list when search changes', function (done) { 48 | runs(function () { 49 | backEndServer.data = [{ name: 'filtered-1' }]; 50 | expect(scope.investments).toBeUndefined(); 51 | 52 | scope.search.name = 'test-filter'; 53 | scope.$digest(); 54 | }); 55 | 56 | waitsFor(backEndServer.isDone, "Server call should be complete", 20); 57 | 58 | runs(function () { 59 | expect(backEndServer.analysis).toHaveBeenCalledWith({ name: 'test-filter' }); 60 | expect(scope.investments).toEqualData([{ name: 'filtered-1' }]); 61 | }); 62 | }); 63 | 64 | it('showInvestment should navigate to investment page', function () { 65 | // act 66 | scope.showInvestment.call({ investment: { id: 23 } }); 67 | 68 | // assert 69 | expect(location.hash).toEqual("#/investment/23"); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Test SPA Angular 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /SPA/AngularFrontEnd/app/index-async.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 43 | My AngularJS App 44 | 45 | 46 | 47 | 51 | 52 |
53 | 54 |
Angular seed app: v
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /SPA/KnockoutFrontEnd/gulpfile.js: -------------------------------------------------------------------------------- 1 | // Node modules 2 | var fs = require('fs'), vm = require('vm'), merge = require('deeply'), chalk = require('chalk'), es = require('event-stream'), cs = require('combined-stream'); 3 | 4 | // Gulp and plugins 5 | var gulp = require('gulp'), rjs = require('gulp-requirejs-bundler'), concat = require('gulp-concat'), clean = require('gulp-clean'), 6 | replace = require('gulp-replace'), uglify = require('gulp-uglify'), htmlreplace = require('gulp-html-replace'), 7 | minifyCSS = require('gulp-minify-css'), minifyHTML = require('gulp-minify-html'); 8 | 9 | // Config 10 | var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require.config.js') + '; require;'); 11 | requireJsOptimizerConfig = merge(requireJsRuntimeConfig, { 12 | out: 'scripts.js', 13 | baseUrl: './src', 14 | name: 'app/startup', 15 | paths: { 16 | requireLib: 'bower_modules/requirejs/require' 17 | }, 18 | include: [ 19 | 'requireLib', 20 | 'extensions/custom-format', 21 | 'components/nav-bar/nav-bar', 22 | 'components/home-page/home', 23 | 'components/investments-component/investments-component', 24 | 'components/sector-component/sector-component', 25 | 'components/transactions-component/transactions-component', 26 | 'components/investment-filter/investment-filter' 27 | ], 28 | insertRequire: ['app/startup'], 29 | bundles: { 30 | 'about-stuff': ['text!components/about-page/about.html'], 31 | 'investment-page': ['components/investment-page/investment-page'] 32 | } 33 | }); 34 | 35 | // Discovers all AMD dependencies, concatenates together all required .js files, minifies them 36 | gulp.task('js', function () { 37 | return rjs(requireJsOptimizerConfig) 38 | .pipe(uglify({ preserveComments: 'some' })) 39 | .pipe(gulp.dest('./dist/')); 40 | }); 41 | 42 | // Concatenates CSS files, rewrites relative paths to Bootstrap fonts, copies Bootstrap fonts 43 | gulp.task('css', function () { 44 | var bowerCss = gulp.src('src/bower_modules/components-bootstrap/css/bootstrap.min.css') 45 | .pipe(replace(/url\((')?\.\.\/fonts\//g, 'url($1fonts/')), 46 | appCss = gulp.src('src/css/*.css').pipe(minifyCSS()), 47 | combinedStream = cs.create(), 48 | fontFiles = gulp.src('./src/bower_modules/components-bootstrap/fonts/*', { base: './src/bower_modules/components-bootstrap/' }); 49 | 50 | combinedStream.append(bowerCss); 51 | combinedStream.append(appCss); 52 | combinedCss = combinedStream 53 | .pipe(concat('css.css')); 54 | 55 | return es.concat(combinedCss, fontFiles) 56 | .pipe(gulp.dest('./dist/')); 57 | }); 58 | 59 | // Copies index.html, replacing