├── .clang-format ├── .editorconfig ├── .gitignore ├── README.md ├── angular-cli-build.js ├── angular-cli.json ├── backend └── db.json ├── config ├── environment.dev.ts ├── environment.js ├── environment.prod.ts ├── karma-test-shim.js ├── karma.conf.js └── protractor.conf.js ├── e2e ├── app.e2e-spec.ts ├── app.e2e.ts ├── app.po.ts ├── tsconfig.json └── typings.d.ts ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── public └── .npmignore ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.model.ts │ ├── app.module.ts │ ├── app.routing.ts │ ├── core │ │ └── core.module.ts │ ├── environment.ts │ ├── index.ts │ ├── notes │ │ ├── components │ │ │ ├── add-button.component.css │ │ │ ├── add-button.component.html │ │ │ ├── add-button.component.ts │ │ │ ├── note.component.css │ │ │ ├── note.component.html │ │ │ ├── note.component.ts │ │ │ ├── notes.component.css │ │ │ ├── notes.component.html │ │ │ ├── notes.component.spec.ts │ │ │ └── notes.component.ts │ │ ├── index.ts │ │ ├── notes.module.ts │ │ ├── notes.routing.ts │ │ ├── reducers │ │ │ ├── note.reducer.ts │ │ │ └── notes.reducer.ts │ │ └── services │ │ │ ├── notes.data.service.ts │ │ │ ├── notes.effects.service.ts │ │ │ └── notes.service.ts │ └── shared │ │ ├── draggable.directive.ts │ │ ├── index.ts │ │ └── shared.module.ts ├── assets │ └── .npmignore ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.json └── typings.d.ts ├── tslint.json └── typings.json /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /src/app/**/*.js 7 | /src/app/**/*.js.map 8 | 9 | # dependencies 10 | /node_modules 11 | /bower_components 12 | 13 | # IDEs and editors 14 | /.idea 15 | /.vscode 16 | 17 | # misc 18 | /.sass-cache 19 | /connect.lock 20 | /coverage/* 21 | /libpeerconnection.log 22 | npm-debug.log 23 | testem.log 24 | /typings 25 | 26 | # e2e 27 | /e2e/*.js 28 | /e2e/*.map 29 | 30 | #System Files 31 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Evolving State and Data Management with @ngrx/store and @ngrx/effects 2 | 3 | ## Watch the Presentation 4 | 5 | [![Watch the Presentation](http://img.youtube.com/vi/pjwVq8B-ZAw/0.jpg)](https://www.youtube.com/watch?v=pjwVq8B-ZAw "Watch the Presentation") 6 | 7 | ## What is it? 8 | This is a bare bones version of a demonstration project that attempts to explore and illustrate the progression of approaches to data management from the 'Angular 1 Way', 9 | through changes to http with observables all the way to the current state of the art utilising the flux/redux implementation @ngrx/store. 10 | 11 | Also, While there are some other examples of using flux/redux/@ngrx/store with http, I didn't find any of them to be easy to understand. 12 | 13 | The intent of this repo is to be the basis for a presentation on the subject. 14 | 15 | ## Prerequisites 16 | You will need to have [Git](https://git-scm.com/) and [Node.js + NPM](http://nodejs.org) installed on your machine. 17 | 18 | You will also need to install the `typings` NPM package globally via `npm i -g typings`. 19 | 20 | You will also need to install the `angular-cli` NPM package globally via `npm i -g angular-cli`. 21 | 22 | You will also need to install the `json-server` NPM package globally via `npm i -g json-server`. 23 | 24 | 25 | ## Make it go 26 | This is a standard angular-cli generated application so you can use all of the ng XXX commands to manage the application. 27 | 28 | ``` 29 | # Download the code 30 | $ git clone https://github.com/JavascriptMick/ng2-state-demo.git 31 | $ cd ng2-state-demo 32 | 33 | # Install dependencies 34 | $ npm i 35 | 36 | # Install typescript definitions 37 | $ typings install 38 | 39 | # Run the backend server 40 | $npm run backend 41 | 42 | # Build and serve the app 43 | $ ng serve 44 | 45 | # Continuously run the tests 46 | $ ng test 47 | 48 | ``` 49 | 50 | Then navigate to [http://localhost:4200](http://localhost:4200) in your browser. 51 | 52 | ## Tags 53 | This repo has been carefully constructed to progress from no state management, through simple 'toy' approaches, all the way to @ngrx/effects in 7 stages! 54 | You can use the tags to retrieve the repo at any point in this progression. Note though that the tests may not run successfully in some of the intermediate stages. 55 | 56 | You can look at the code for each tag by creating a branch at a specific tag e.g. 57 | ``` 58 | git checkout -b version5 v5.0 59 | ``` 60 | 61 | * v1.0 - talk.start() - Just the UI components, no state or data management at all 62 | * v2.0 - basic in-component state management with ref equality 63 | * v3.0 - introduce NotesService, async, Observables and ChangeDetection.onPush 64 | * v4.0 - Immutable state and Id 65 | * v5.0 - Introduce @ngrx/store 66 | * v6.0 - introduce HTTP and orchestration in the server 67 | * v7.0 - Introduce @ngrx/effects to replace http orchestration in the service 68 | 69 | ## The Original Demo 70 | With a lot more documentation and some non-@ngrx, service only approaches that work. Can be found here https://github.com/JavascriptMick/ng2-state-demo -------------------------------------------------------------------------------- /angular-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | 3 | var Angular2App = require('angular-cli/lib/broccoli/angular2-app'); 4 | 5 | module.exports = function(defaults) { 6 | return new Angular2App(defaults, { 7 | vendorNpmFiles: [ 8 | 'systemjs/dist/system-polyfills.js', 9 | 'systemjs/dist/system.src.js', 10 | 'zone.js/dist/*.js', 11 | 'es6-shim/es6-shim.js', 12 | 'reflect-metadata/*.js', 13 | 'rxjs/**/*.js', 14 | '@angular/**/*.js', 15 | '@ngrx/**/*.js', 16 | 'node-uuid/uuid.js' 17 | ] 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "0.2.0", 4 | "name": "ng2-state-demo" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": "assets", 11 | "index": "index.html", 12 | "main": "main.ts", 13 | "test": "test.ts", 14 | "tsconfig": "tsconfig.json", 15 | "prefix": "app", 16 | "mobile": false, 17 | "styles": [ 18 | "styles.css" 19 | ], 20 | "scripts": [], 21 | "environments": { 22 | "source": "environments/environment.ts", 23 | "dev": "environments/environment.ts", 24 | "prod": "environments/environment.prod.ts" 25 | } 26 | } 27 | ], 28 | "addons": [], 29 | "packages": [], 30 | "e2e": { 31 | "protractor": { 32 | "config": "./protractor.conf.js" 33 | } 34 | }, 35 | "test": { 36 | "karma": { 37 | "config": "./karma.conf.js" 38 | } 39 | }, 40 | "defaults": { 41 | "styleExt": "css", 42 | "prefixInterfaces": false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "notes": [ 3 | { 4 | "id": 1, 5 | "text": "Angular 2 Rocks!", 6 | "colour": "red", 7 | "left": 200, 8 | "top": 100 9 | }, 10 | { 11 | "id": 2, 12 | "text": "Thanks @robwormald & @MikeRyan52 for @ngrx.", 13 | "colour": "blue", 14 | "left": 300, 15 | "top": 200 16 | }, 17 | { 18 | "id": 3, 19 | "text": "And everybody from the angular, angular-cli, @ngrx/store and @ngrx/effects gitter.im channels who answered my stoopid questions.", 20 | "colour": "yellow", 21 | "left": 400, 22 | "top": 300 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /config/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | return { 5 | environment: environment, 6 | baseURL: '/', 7 | locationType: 'auto' 8 | }; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /config/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 4 | 5 | __karma__.loaded = function () { 6 | }; 7 | 8 | var distPath = '/base/dist/'; 9 | var appPath = distPath + 'app/'; 10 | 11 | function isJsFile(path) { 12 | return path.slice(-3) == '.js'; 13 | } 14 | 15 | function isSpecFile(path) { 16 | return path.slice(-8) == '.spec.js'; 17 | } 18 | 19 | function isAppFile(path) { 20 | return isJsFile(path) && (path.substr(0, appPath.length) == appPath); 21 | } 22 | 23 | var allSpecFiles = Object.keys(window.__karma__.files) 24 | .filter(isSpecFile) 25 | .filter(isAppFile); 26 | 27 | // Load our SystemJS configuration. 28 | System.config({ 29 | baseURL: distPath 30 | }); 31 | 32 | System.import('system-config.js').then(function() { 33 | // Load and configure the TestComponentBuilder. 34 | return Promise.all([ 35 | System.import('@angular/core/testing'), 36 | System.import('@angular/platform-browser-dynamic/testing') 37 | ]).then(function (providers) { 38 | var testing = providers[0]; 39 | var testingBrowser = providers[1]; 40 | 41 | testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, 42 | testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); 43 | }); 44 | }).then(function() { 45 | // Finally, load all spec files. 46 | // This will run the tests directly. 47 | return Promise.all( 48 | allSpecFiles.map(function (moduleName) { 49 | return System.import(moduleName); 50 | })); 51 | }).then(__karma__.start, __karma__.error); -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '..', 4 | frameworks: ['jasmine'], 5 | plugins: [ 6 | require('karma-jasmine'), 7 | require('karma-chrome-launcher') 8 | ], 9 | customLaunchers: { 10 | // chrome setup for travis CI using chromium 11 | Chrome_travis_ci: { 12 | base: 'Chrome', 13 | flags: ['--no-sandbox'] 14 | } 15 | }, 16 | files: [ 17 | { pattern: 'dist/vendor/es6-shim/es6-shim.js', included: true, watched: false }, 18 | { pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false }, 19 | { pattern: 'dist/vendor/reflect-metadata/Reflect.js', included: true, watched: false }, 20 | { pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false }, 21 | { pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false }, 22 | { pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false }, 23 | 24 | { pattern: 'config/karma-test-shim.js', included: true, watched: true }, 25 | 26 | // Distribution folder. 27 | { pattern: 'dist/**/*', included: false, watched: true } 28 | ], 29 | exclude: [ 30 | // Vendor packages might include spec files. We don't want to use those. 31 | 'dist/vendor/**/*.spec.js' 32 | ], 33 | preprocessors: {}, 34 | reporters: ['progress'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /config/protractor.conf.js: -------------------------------------------------------------------------------- 1 | /*global jasmine */ 2 | var SpecReporter = require('jasmine-spec-reporter'); 3 | 4 | exports.config = { 5 | allScriptsTimeout: 11000, 6 | specs: [ 7 | '../e2e/**/*.e2e.ts' 8 | ], 9 | capabilities: { 10 | 'browserName': 'chrome' 11 | }, 12 | directConnect: true, 13 | baseUrl: 'http://localhost:4200/', 14 | framework: 'jasmine', 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function() {} 19 | }, 20 | useAllAngular2AppRoots: true, 21 | beforeLaunch: function() { 22 | require('ts-node').register({ 23 | project: 'e2e' 24 | }); 25 | }, 26 | onPrepare: function() { 27 | jasmine.getEnv().addReporter(new SpecReporter()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { MyPage } from './app.po'; 2 | 3 | describe('ng2-state-demo App', function() { 4 | let page: MyPage; 5 | 6 | beforeEach(() => { 7 | page = new MyPage(); 8 | }); 9 | 10 | it('should display message saying Angular2 State Management Demo', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Angular2 State Management Demo'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.e2e.ts: -------------------------------------------------------------------------------- 1 | import { MyPage } from './app.po'; 2 | 3 | describe('my App', function() { 4 | let page: MyPage; 5 | 6 | beforeEach(() => { 7 | page = new MyPage(); 8 | }) 9 | 10 | it('should display message text', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Angular2 State Management Demo'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class MyPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('my-app h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /e2e/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | remapIstanbulReporter: { 21 | reports: { 22 | html: 'coverage', 23 | lcovonly: './coverage/coverage.lcov' 24 | } 25 | }, 26 | angularCli: { 27 | config: './angular-cli.json', 28 | environment: 'dev' 29 | }, 30 | reporters: ['progress', 'karma-remap-istanbul'], 31 | port: 9876, 32 | colors: true, 33 | logLevel: config.LOG_INFO, 34 | autoWatch: true, 35 | browsers: ['Chrome'], 36 | singleRun: false 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-state-talk", 3 | "version": "0.0.0", 4 | "description": "Exploring State and Data management with @ngrx/store and http in an Angular2 demo", 5 | "license": "MIT", 6 | "author": { 7 | "name": "@JavascriptMick" 8 | }, 9 | "angular-cli": {}, 10 | "scripts": { 11 | "start": "ng serve", 12 | "lint": "tslint \"src/**/*.ts\"", 13 | "test": "ng test", 14 | "pree2e": "webdriver-manager update", 15 | "e2e": "protractor", 16 | "backend": "json-server --watch backend/db.json --delay 2000" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/common": "2.0.1", 21 | "@angular/compiler": "2.0.1", 22 | "@angular/core": "2.0.1", 23 | "@angular/forms": "2.0.1", 24 | "@angular/http": "2.0.1", 25 | "@angular/platform-browser": "2.0.1", 26 | "@angular/platform-browser-dynamic": "2.0.1", 27 | "@angular/router": "3.0.1", 28 | "core-js": "^2.4.1", 29 | "es6-shim": "0.35.1", 30 | "reflect-metadata": "0.1.3", 31 | "rxjs": "5.0.0-beta.12", 32 | "ts-helpers": "^1.1.1", 33 | "zone.js": "0.6.21", 34 | "@ngrx/core": "^1.0.0", 35 | "@ngrx/effects": "^2.0.0", 36 | "@ngrx/store": "^2.0.0", 37 | "node-uuid": "^1.4.7" 38 | }, 39 | "devDependencies": { 40 | "@types/jasmine": "^2.2.30", 41 | "@types/node-uuid": "0.0.28", 42 | "angular-cli": "1.0.0-beta.16", 43 | "codelyzer": "~0.0.26", 44 | "jasmine-core": "2.4.1", 45 | "jasmine-spec-reporter": "2.5.0", 46 | "karma": "1.2.0", 47 | "karma-chrome-launcher": "^2.0.0", 48 | "karma-cli": "^1.0.1", 49 | "karma-jasmine": "^1.0.2", 50 | "karma-remap-istanbul": "^0.2.1", 51 | "protractor": "4.0.9", 52 | "ts-node": "1.2.1", 53 | "tslint": "3.13.0", 54 | "typescript": "2.0.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/docs/referenceConf.js 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /public/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavascriptMick/ng2-state-talk/de2f1c1b40aabcbaf3b93bbea0d56ca99a32aa72/public/.npmignore -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Gloria+Hallelujah); 2 | * { box-sizing:border-box; } 3 | html,body { 4 | padding:0; 5 | margin:0; 6 | height:100%; 7 | min-height:100%; 8 | } 9 | 10 | body { background:url(http://subtlepatterns.com/patterns/little_pluses.png) #cacaca; } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{title}} 3 | 4 |

5 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { AppComponent } from './app.component'; 5 | import { AppModule } from './app.module'; 6 | 7 | describe('App: Ng2StateTalk', () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ AppModule ] 11 | }); 12 | }); 13 | 14 | it('should create the app', async(() => { 15 | let fixture = TestBed.createComponent(AppComponent); 16 | let app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it('should have as title \'Angular2 State Management Demo\'', async(() => { 21 | let fixture = TestBed.createComponent(AppComponent); 22 | let app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('Angular2 State Management Demo'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | let fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | let compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('Angular2 State Management Demo'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'my-app', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'Angular2 State Management Demo'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.model.ts: -------------------------------------------------------------------------------- 1 | export interface Note { 2 | text: string, 3 | colour: string, 4 | left: number, 5 | top: number, 6 | id: string, 7 | dirty: boolean 8 | } 9 | 10 | export interface AppState { 11 | notes: Note[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { NotesModule } from './notes/notes.module'; 6 | import { CoreModule } from './core/core.module'; 7 | import { routing } from './app.routing'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | NotesModule, // If new Feature modules are loaded you don't have to add them. 13 | CoreModule, // NotesModule is here because it's designated first in app.routing 14 | routing 15 | ], 16 | declarations: [ 17 | AppComponent 18 | ], 19 | bootstrap: [ 20 | AppComponent 21 | ] 22 | }) 23 | export class AppModule { } 24 | -------------------------------------------------------------------------------- /src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | export const routes: Routes = [ 5 | { path: '', redirectTo: 'notes', pathMatch: 'full'} 6 | ]; 7 | 8 | export const routing: ModuleWithProviders = RouterModule.forRoot(routes); 9 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | // From Style guide item 4-11 2 | // https://angular.io/docs/ts/latest/guide/style-guide.html#04-11 3 | 4 | import { 5 | NgModule, 6 | Optional, SkipSelf } from '@angular/core'; 7 | import { CommonModule } from '@angular/common'; 8 | import { Dispatcher } from '@ngrx/store'; 9 | import { provideStore } from '@ngrx/store'; 10 | 11 | import { notes } from '../notes/reducers/notes.reducer'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | ], 16 | imports: [ 17 | CommonModule 18 | ], 19 | providers: [ 20 | Dispatcher, 21 | provideStore({notes}, {notes: []}) 22 | ] 23 | }) 24 | export class CoreModule { 25 | 26 | constructor (@Optional() @SkipSelf() parentModule: CoreModule) { 27 | if (parentModule) { 28 | throw new Error( 29 | 'CoreModule is already loaded. Import it in the AppModule only'); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/app/environment.ts: -------------------------------------------------------------------------------- 1 | // The file for the current environment will overwrite this one during build 2 | // Different environments can be found in config/environment.{dev|prod}.ts 3 | // The build system defaults to the dev environment 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module' 3 | export {Note, AppState} from './app.model' -------------------------------------------------------------------------------- /src/app/notes/components/add-button.component.css: -------------------------------------------------------------------------------- 1 | .yellowButton { 2 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #f9efaf), color-stop(1, #f7e98d)); 3 | background:-moz-linear-gradient(top, #f9efaf 5%, #f7e98d 100%); 4 | background:-webkit-linear-gradient(top, #f9efaf 5%, #f7e98d 100%); 5 | background:-o-linear-gradient(top, #f9efaf 5%, #f7e98d 100%); 6 | background:-ms-linear-gradient(top, #f9efaf 5%, #f7e98d 100%); 7 | background:linear-gradient(to bottom, #f9efaf 5%, #f7e98d 100%); 8 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9efaf', endColorstr='#f7e98d',GradientType=0); 9 | background-color:#f9efaf; 10 | -moz-border-radius:42px; 11 | -webkit-border-radius:42px; 12 | border-radius:42px; 13 | display:inline-block; 14 | cursor:pointer; 15 | color:#d9633f; 16 | font-family:Arial; 17 | font-size:17px; 18 | padding:16px 19px; 19 | text-decoration:none; 20 | } 21 | .yellowButton:hover { 22 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #f7e98d), color-stop(1, #f9efaf)); 23 | background:-moz-linear-gradient(top, #f7e98d 5%, #f9efaf 100%); 24 | background:-webkit-linear-gradient(top, #f7e98d 5%, #f9efaf 100%); 25 | background:-o-linear-gradient(top, #f7e98d 5%, #f9efaf 100%); 26 | background:-ms-linear-gradient(top, #f7e98d 5%, #f9efaf 100%); 27 | background:linear-gradient(to bottom, #f7e98d 5%, #f9efaf 100%); 28 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7e98d', endColorstr='#f9efaf',GradientType=0); 29 | background-color:#f7e98d; 30 | } 31 | .yellowButton:active { 32 | position:relative; 33 | top:1px; 34 | } 35 | .redButton { 36 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #f9afaf), color-stop(1, #f78d8d)); 37 | background:-moz-linear-gradient(top, #f9afaf 5%, #f78d8d 100%); 38 | background:-webkit-linear-gradient(top, #f9afaf 5%, #f78d8d 100%); 39 | background:-o-linear-gradient(top, #f9afaf 5%, #f78d8d 100%); 40 | background:-ms-linear-gradient(top, #f9afaf 5%, #f78d8d 100%); 41 | background:linear-gradient(to bottom, #f9afaf 5%, #f78d8d 100%); 42 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9afaf', endColorstr='#f78d8d',GradientType=0); 43 | background-color:#f9afaf; 44 | -moz-border-radius:42px; 45 | -webkit-border-radius:42px; 46 | border-radius:42px; 47 | display:inline-block; 48 | cursor:pointer; 49 | color:#d9633f; 50 | font-family:Arial; 51 | font-size:17px; 52 | padding:16px 19px; 53 | text-decoration:none; 54 | } 55 | .redButton:hover { 56 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #f78d8d), color-stop(1, #f9afaf)); 57 | background:-moz-linear-gradient(top, #f78d8d 5%, #f9afaf 100%); 58 | background:-webkit-linear-gradient(top, #f78d8d 5%, #f9afaf 100%); 59 | background:-o-linear-gradient(top, #f78d8d 5%, #f9afaf 100%); 60 | background:-ms-linear-gradient(top, #f78d8d 5%, #f9afaf 100%); 61 | background:linear-gradient(to bottom, #f78d8d 5%, #f9afaf 100%); 62 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f78d8d', endColorstr='#f9afaf',GradientType=0); 63 | background-color:#f78d8d; 64 | } 65 | .redButton:active { 66 | position:relative; 67 | top:1px; 68 | } 69 | 70 | .greenButton { 71 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #aff9b0), color-stop(1, #9ff78d)); 72 | background:-moz-linear-gradient(top, #aff9b0 5%, #9ff78d 100%); 73 | background:-webkit-linear-gradient(top, #aff9b0 5%, #9ff78d 100%); 74 | background:-o-linear-gradient(top, #aff9b0 5%, #9ff78d 100%); 75 | background:-ms-linear-gradient(top, #aff9b0 5%, #9ff78d 100%); 76 | background:linear-gradient(to bottom, #aff9b0 5%, #9ff78d 100%); 77 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#aff9b0', endColorstr='#9ff78d',GradientType=0); 78 | background-color:#aff9b0; 79 | -moz-border-radius:42px; 80 | -webkit-border-radius:42px; 81 | border-radius:42px; 82 | display:inline-block; 83 | cursor:pointer; 84 | color:#d9633f; 85 | font-family:Arial; 86 | font-size:17px; 87 | padding:16px 19px; 88 | text-decoration:none; 89 | } 90 | .greenButton:hover { 91 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #9ff78d), color-stop(1, #aff9b0)); 92 | background:-moz-linear-gradient(top, #9ff78d 5%, #aff9b0 100%); 93 | background:-webkit-linear-gradient(top, #9ff78d 5%, #aff9b0 100%); 94 | background:-o-linear-gradient(top, #9ff78d 5%, #aff9b0 100%); 95 | background:-ms-linear-gradient(top, #9ff78d 5%, #aff9b0 100%); 96 | background:linear-gradient(to bottom, #9ff78d 5%, #aff9b0 100%); 97 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9ff78d', endColorstr='#aff9b0',GradientType=0); 98 | background-color:#9ff78d; 99 | } 100 | .greenButton:active { 101 | position:relative; 102 | top:1px; 103 | } 104 | 105 | .blueButton { 106 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #b2aff9), color-stop(1, #968df7)); 107 | background:-moz-linear-gradient(top, #b2aff9 5%, #968df7 100%); 108 | background:-webkit-linear-gradient(top, #b2aff9 5%, #968df7 100%); 109 | background:-o-linear-gradient(top, #b2aff9 5%, #968df7 100%); 110 | background:-ms-linear-gradient(top, #b2aff9 5%, #968df7 100%); 111 | background:linear-gradient(to bottom, #b2aff9 5%, #968df7 100%); 112 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#b2aff9', endColorstr='#968df7',GradientType=0); 113 | background-color:#b2aff9; 114 | -moz-border-radius:42px; 115 | -webkit-border-radius:42px; 116 | border-radius:42px; 117 | display:inline-block; 118 | cursor:pointer; 119 | color:#d9633f; 120 | font-family:Arial; 121 | font-size:17px; 122 | padding:16px 19px; 123 | text-decoration:none; 124 | } 125 | .blueButton:hover { 126 | background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #968df7), color-stop(1, #b2aff9)); 127 | background:-moz-linear-gradient(top, #968df7 5%, #b2aff9 100%); 128 | background:-webkit-linear-gradient(top, #968df7 5%, #b2aff9 100%); 129 | background:-o-linear-gradient(top, #968df7 5%, #b2aff9 100%); 130 | background:-ms-linear-gradient(top, #968df7 5%, #b2aff9 100%); 131 | background:linear-gradient(to bottom, #968df7 5%, #b2aff9 100%); 132 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#968df7', endColorstr='#b2aff9',GradientType=0); 133 | background-color:#968df7; 134 | } 135 | .blueButton:active { 136 | position:relative; 137 | top:1px; 138 | } 139 | -------------------------------------------------------------------------------- /src/app/notes/components/add-button.component.html: -------------------------------------------------------------------------------- 1 | + -------------------------------------------------------------------------------- /src/app/notes/components/add-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'add-button', 5 | templateUrl: 'add-button.component.html', 6 | styleUrls: ['add-button.component.css'] 7 | }) 8 | export class AddButtonComponent { 9 | @Input() colour: string; 10 | @Output() add: EventEmitter = new EventEmitter(); 11 | 12 | onClick($event) { 13 | $event.preventDefault(); 14 | this.add.emit(this.colour); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/notes/components/note.component.css: -------------------------------------------------------------------------------- 1 | textarea { 2 | display: block; 3 | padding:25px 25px 40px; 4 | margin:0 auto 20px auto; 5 | width:250px; 6 | height:150px; 7 | font:20px 'Gloria Hallelujah', cursive; 8 | line-height:1.5; 9 | border:0; 10 | border-radius:3px; 11 | background: -webkit-linear-gradient(#F9EFAF, #F7E98D); 12 | background: -moz-linear-gradient(#F9EFAF, #F7E98D); 13 | background: -o-linear-gradient(#F9EFAF, #F7E98D); 14 | background: -ms-linear-gradient(#F9EFAF, #F7E98D); 15 | background: linear-gradient(#F9EFAF, #F7E98D); 16 | box-shadow:0 4px 6px rgba(0,0,0,0.1); 17 | overflow:hidden; 18 | transition:box-shadow 0.5s ease; 19 | font-smoothing:subpixel-antialiased; 20 | max-width:520px; 21 | max-height:250px; 22 | /*position: absolute;*/ 23 | } 24 | 25 | textarea.yellow-note{ 26 | background: -webkit-linear-gradient(#F9EFAF, #F7E98D); 27 | background: -moz-linear-gradient(#F9EFAF, #F7E98D); 28 | background: -o-linear-gradient(#F9EFAF, #F7E98D); 29 | background: -ms-linear-gradient(#F9EFAF, #F7E98D); 30 | background: linear-gradient(#F9EFAF, #F7E98D); 31 | } 32 | 33 | textarea.red-note{ 34 | background: -webkit-linear-gradient(#F9AFAF, #F78D8D); 35 | background: -moz-linear-gradient(#F9AFAF, #F78D8D); 36 | background: -o-linear-gradient(#F9AFAF, #F78D8D); 37 | background: -ms-linear-gradient(#F9AFAF, #F78D8D); 38 | background: linear-gradient(#F9AFAF, #F78D8D); 39 | } 40 | 41 | textarea.green-note{ 42 | background: -webkit-linear-gradient(#AFF9B0, #9FF78D); 43 | background: -moz-linear-gradient(#AFF9B0, #9FF78D); 44 | background: -o-linear-gradient(#AFF9B0, #9FF78D); 45 | background: -ms-linear-gradient(#AFF9B0, #9FF78D); 46 | background: linear-gradient(#AFF9B0, #9FF78D); 47 | } 48 | 49 | textarea.blue-note{ 50 | background: -webkit-linear-gradient(#B3AFF9, #968DF7); 51 | background: -moz-linear-gradient(#B3AFF9, #968DF7); 52 | background: -o-linear-gradient(#B3AFF9, #968DF7); 53 | background: -ms-linear-gradient(#B3AFF9, #968DF7); 54 | background: linear-gradient(#B3AFF9, #968DF7); 55 | } 56 | textarea:hover { box-shadow:0 5px 8px rgba(0,0,0,0.15); } 57 | textarea:focus { box-shadow:0 5px 12px rgba(0,0,0,0.2); outline:none; } -------------------------------------------------------------------------------- /src/app/notes/components/note.component.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/notes/components/note.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-note', 5 | templateUrl: 'note.component.html', 6 | styleUrls: ['note.component.css'] 7 | }) 8 | export class NoteComponent { 9 | @Input() text: string; 10 | @Input() top: number; 11 | @Input() left: number; 12 | @Input() colour: string; 13 | @Input() disabled: boolean; 14 | 15 | @Output() changeNoteText = new EventEmitter(false); 16 | @Output() changeNotePosition = new EventEmitter(false); 17 | 18 | constructor() {} 19 | 20 | handleChangeNotePosition(newPosition) { 21 | if (newPosition.left != this.left || newPosition.top != this.top) { 22 | this.changeNotePosition.emit(newPosition); 23 | } 24 | } 25 | handleChangeNoteText(text) { 26 | if (text != this.text) { 27 | this.changeNoteText.emit(text); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/notes/components/notes.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavascriptMick/ng2-state-talk/de2f1c1b40aabcbaf3b93bbea0d56ca99a32aa72/src/app/notes/components/notes.component.css -------------------------------------------------------------------------------- /src/app/notes/components/notes.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 14 | {{$notes | async | json}} 15 |
16 | -------------------------------------------------------------------------------- /src/app/notes/components/notes.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { inject, async } from '@angular/core/testing'; 3 | import { Component } from '@angular/core'; 4 | import { By } from '@angular/platform-browser'; 5 | import { provideStore } from '@ngrx/store'; 6 | 7 | import { AddButtonComponent } from './add-button.component'; 8 | import { NoteComponent } from './note.component'; 9 | import { NotesService } from '../services/notes.service'; 10 | import { notes } from '../reducers/notes.reducer'; 11 | import { Draggable } from '../../shared'; 12 | 13 | // Object under test 14 | import { NotesComponent } from './notes.component'; 15 | 16 | describe('Component: Notes', () => { 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ 21 | NotesComponent, 22 | NoteComponent, 23 | AddButtonComponent, 24 | Draggable, 25 | NotesComponentTestController 26 | ], 27 | providers: [ 28 | provideStore({notes}, {notes: []}), 29 | NotesService, 30 | NotesComponent] 31 | }); 32 | }); 33 | 34 | it('should inject the component', inject([NotesComponent], 35 | (component: NotesComponent) => { 36 | expect(component).toBeTruthy(); 37 | })); 38 | 39 | it('should create the component', 40 | async(() => { 41 | TestBed 42 | .compileComponents() 43 | .then(() => { 44 | let fixture = TestBed.createComponent(NotesComponentTestController); 45 | let query = fixture.debugElement.query(By.directive(NotesComponent)); 46 | expect(query).toBeTruthy(); 47 | expect(query.componentInstance).toBeTruthy(); 48 | }); 49 | })); 50 | }); 51 | 52 | @Component({ 53 | selector: 'notes-test', 54 | template: ` 55 | 56 | ` 57 | }) 58 | class NotesComponentTestController { 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/app/notes/components/notes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | 4 | import { Note } from '../../index'; 5 | import { NotesService } from '../services/notes.service'; 6 | 7 | @Component({ 8 | selector: 'app-notes', 9 | templateUrl: 'notes.component.html', 10 | styleUrls: ['notes.component.css'], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class NotesComponent implements OnInit { 14 | $notes: Observable; 15 | 16 | constructor(private notesService: NotesService) { 17 | this.$notes = this.notesService.getNotes(); 18 | } 19 | 20 | onAddNote(colour) { 21 | this.notesService.addNote('', colour, 200, 100); 22 | } 23 | 24 | onChangeNoteText(newText: string, note: Note) { 25 | this.notesService.changeNoteText(newText, note); 26 | } 27 | 28 | onChangeNotePosition(newPosition: any, note: Note) { 29 | this.notesService.changeNotePosition(newPosition.left, newPosition.top, note); 30 | } 31 | 32 | ngOnInit() { 33 | this.notesService.initialise(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/notes/index.ts: -------------------------------------------------------------------------------- 1 | export { NotesComponent } from './components/notes.component'; 2 | export { AddButtonComponent } from './components/add-button.component'; 3 | export { NotesModule } from './notes.module'; 4 | 5 | -------------------------------------------------------------------------------- /src/app/notes/notes.module.ts: -------------------------------------------------------------------------------- 1 | // From Style guide item 4-09 - Feature Modules 2 | // https://angular.io/docs/ts/latest/guide/style-guide.html#04-09 3 | 4 | import { NgModule } from '@angular/core'; 5 | import { HttpModule } from '@angular/http'; 6 | import { EffectsModule } from '@ngrx/effects'; 7 | 8 | import { NotesDataService } from './services/notes.data.service'; 9 | import { NotesEffectsService } from './services/notes.effects.service'; 10 | import { NotesService } from './services/notes.service'; 11 | import { NoteComponent } from './components/note.component'; 12 | import { AddButtonComponent } from './components/add-button.component'; 13 | import { NotesComponent } from './components/notes.component'; 14 | import { routing } from './notes.routing'; 15 | import { SharedModule } from '../shared/shared.module'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | NotesComponent, 20 | NoteComponent, 21 | AddButtonComponent 22 | ], 23 | imports: [ 24 | SharedModule, 25 | routing, 26 | HttpModule, 27 | EffectsModule.run(NotesEffectsService) 28 | ], 29 | providers: [ 30 | NotesDataService, 31 | NotesService 32 | ] 33 | }) 34 | export class NotesModule { } 35 | -------------------------------------------------------------------------------- /src/app/notes/notes.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, 3 | RouterModule } from '@angular/router'; 4 | 5 | import { NotesComponent } from './components/notes.component'; 6 | 7 | const routes: Routes = [ 8 | { path: 'notes', component: NotesComponent } 9 | ]; 10 | 11 | export const routing: ModuleWithProviders = RouterModule.forChild(routes); 12 | -------------------------------------------------------------------------------- /src/app/notes/reducers/note.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { Note } from '../../index'; 4 | 5 | export const note = (n: Note = null, action: Action) => { 6 | switch (action.type) { 7 | case 'ADD_NOTE': 8 | return Object.assign({}, action.payload, {dirty: true}); 9 | case 'UPDATE_NOTE_TEXT': 10 | if (n.id == action.payload.id) { 11 | return Object.assign({}, n, {text: action.payload.text}, {dirty: true}); 12 | } else { 13 | return n; 14 | } 15 | case 'UPDATE_NOTE_POSITION': 16 | if (n.id == action.payload.id) { 17 | return Object.assign( 18 | {}, 19 | n, 20 | {left: action.payload.left, top: action.payload.top}, 21 | {dirty: true}); 22 | } else { 23 | return n; 24 | } 25 | case 'ADD_NOTE_FROM_SERVER': 26 | return Object.assign({}, action.payload, {dirty: false}); 27 | case 'UPDATE_NOTE_FROM_SERVER': 28 | if (n.id == action.payload.note.id) { 29 | return Object.assign({}, action.payload.note, {dirty: false}); 30 | } else { 31 | return n; 32 | } 33 | default: 34 | return n; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/notes/reducers/notes.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { Note } from '../../index'; 4 | import { note } from './note.reducer'; 5 | 6 | export const notes = (ns: Array = [], action: Action) => { 7 | switch (action.type) { 8 | case 'ADD_NOTE': 9 | case 'ADD_NOTE_FROM_SERVER': 10 | return [...ns, note(null, action)]; 11 | case 'UPDATE_NOTE_TEXT': 12 | case 'UPDATE_NOTE_POSITION': 13 | case 'UPDATE_NOTE_FROM_SERVER': 14 | return ns.map(n => note(n, action)); 15 | default: 16 | return ns; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/notes/services/notes.data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response, Headers } from '@angular/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import 'rxjs/add/operator/map'; 6 | import 'rxjs/add/observable/from'; 7 | 8 | import { Note } from '../../app.model'; 9 | 10 | @Injectable() 11 | export class NotesDataService { 12 | private API_ROOT: String = 'http://localhost:3000'; 13 | private JSON_HEADER = { headers: new Headers({ 'Content-Type': 'application/json' }) }; 14 | 15 | constructor(public http: Http) { } 16 | 17 | getNotes(): Observable> { 18 | return this.http.get(`${this.API_ROOT}/notes`) 19 | .map((response: Response) => response.json()); 20 | } 21 | 22 | addOrUpdateNote(note: Note): Observable { 23 | return this.http.post(`${this.API_ROOT}/notes`, JSON.stringify(note), this.JSON_HEADER) 24 | .map((response: Response) => response.json()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/notes/services/notes.effects.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Actions, Effect } from '@ngrx/effects'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import 'rxjs/add/operator/catch'; 6 | import 'rxjs/add/observable/of'; 7 | import 'rxjs/add/operator/switchMap'; 8 | import 'rxjs/add/operator/mergeMap'; 9 | import 'rxjs/add/operator/map'; 10 | import 'rxjs/add/operator/filter'; 11 | import 'rxjs/add/operator/withLatestFrom'; 12 | 13 | import { Note } from '../../app.model'; 14 | import { NotesDataService } from './notes.data.service'; 15 | 16 | @Injectable() 17 | export class NotesEffectsService { 18 | constructor(private store: Store, 19 | private notesDataService: NotesDataService, 20 | private action$: Actions) {} 21 | 22 | @Effect() update$ = this.action$ 23 | .ofType('UPDATE_NOTE_TEXT', 'UPDATE_NOTE_POSITION', 'ADD_NOTE') 24 | .withLatestFrom(this.store.select('notes')) 25 | .switchMap(([{}, notes]) => Observable // first element is action, but it isn't used 26 | .from(notes) 27 | .filter((note: Note) => note.dirty) 28 | .switchMap((note: Note) => this.notesDataService.addOrUpdateNote(note)) 29 | .map((responseNote: Note) => ({ 30 | type: 'UPDATE_NOTE_FROM_SERVER', 31 | payload: { note: responseNote } })) 32 | ); 33 | 34 | @Effect() init$ = this.action$ 35 | .ofType('INIT_NOTES') 36 | .switchMap(() => this.notesDataService.getNotes().mergeMap(notes => Observable.from(notes)) 37 | .map(res => ({ type: 'ADD_NOTE_FROM_SERVER', payload: res })) 38 | .catch(() => Observable.of({ type: 'FETCH_FAILED' })) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/notes/services/notes.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import 'rxjs/add/operator/mergeMap'; 5 | 6 | import { Note, AppState } from '../../index'; 7 | 8 | let uuid = require('node-uuid'); 9 | 10 | @Injectable() 11 | export class NotesService { 12 | store: Store; 13 | 14 | constructor(store: Store) { 15 | this.store = store; 16 | } 17 | 18 | getNotes(): Observable { 19 | return this.store.select('notes'); 20 | } 21 | addNote(text: string, colour: string, left: number, top: number): void { 22 | this.store.dispatch({ type: 'ADD_NOTE', payload: {text, colour, left, top, id: uuid.v1()} }); 23 | } 24 | changeNoteText(text: string, note: Note): void { 25 | console.log('changeNoteText'); 26 | this.store.dispatch({type: 'UPDATE_NOTE_TEXT', payload: {id: note.id, text: text}}); 27 | } 28 | changeNotePosition(left: number, top: number, note: Note): void { 29 | this.store.dispatch({type: 'UPDATE_NOTE_POSITION', 30 | payload: {id: note.id, left: left, top: top}}); 31 | } 32 | 33 | initialise(): void { 34 | this.store.dispatch({ type: 'INIT_NOTES', payload: { } }); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/shared/draggable.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, Output, EventEmitter, ElementRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[draggable]', 5 | host: { 6 | '(mousedown)': 'onMouseDown($event)', 7 | '(mousemove)': 'onMouseMove($event)', 8 | '(mouseup)': 'onMouseUp($event)' 9 | } 10 | }) 11 | export class Draggable { 12 | isDragging: boolean = false; 13 | originalClientX: number; 14 | originalClientY: number; 15 | originalTop: number; 16 | originalLeft: number; 17 | hasDragged: boolean = false; 18 | 19 | @Output('draggable') endDragEvent = new EventEmitter(false); 20 | 21 | constructor(public element: ElementRef) { 22 | this.element.nativeElement.style.position = 'absolute'; 23 | } 24 | onMouseDown($event) { 25 | if ($event.target.style.position === 'absolute' 26 | && $event.target.style.left && $event.target.style.top) { 27 | this.hasDragged = false; 28 | this.isDragging = true; 29 | this.originalLeft = parseInt($event.target.style.left, 10); 30 | this.originalTop = parseInt($event.target.style.top, 10); 31 | this.originalClientX = $event.clientX; 32 | this.originalClientY = $event.clientY; 33 | }else { 34 | console.log('draggable: Error! the annotated ' + $event.target.nodeName 35 | + ' element needs to be inline styled with position, top and left'); 36 | } 37 | } 38 | 39 | onMouseMove($event) { 40 | if (this.isDragging) { 41 | this.hasDragged = true; 42 | this.element.nativeElement.style.top = 43 | (this.originalTop + ($event.clientY - this.originalClientY)) + 'px'; 44 | this.element.nativeElement.style.left = 45 | (this.originalLeft + ($event.clientX - this.originalClientX)) + 'px'; 46 | } 47 | } 48 | 49 | onMouseUp($event) { 50 | if (this.isDragging) { 51 | this.isDragging = false; 52 | if (this.hasDragged) { 53 | this.endDragEvent.emit({ 54 | left: this.originalLeft + ($event.clientX - this.originalClientX), 55 | top: this.originalTop + ($event.clientY - this.originalClientY)}); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { Draggable } from './draggable.directive'; 2 | export { SharedModule } from './shared.module'; 3 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | // From Style guide item 4-10 2 | // https://angular.io/docs/ts/latest/guide/style-guide.html#04-10 3 | 4 | import { NgModule } from '@angular/core'; 5 | import { CommonModule } from '@angular/common'; 6 | import { FormsModule } from '@angular/forms'; 7 | 8 | import { Draggable } from './draggable.directive'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule 13 | ], 14 | declarations: [ 15 | Draggable 16 | ], 17 | exports: [ 18 | CommonModule, 19 | FormsModule, 20 | Draggable 21 | ] 22 | }) 23 | export class SharedModule { } 24 | -------------------------------------------------------------------------------- /src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavascriptMick/ng2-state-talk/de2f1c1b40aabcbaf3b93bbea0d56ca99a32aa72/src/assets/.npmignore -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavascriptMick/ng2-state-talk/de2f1c1b40aabcbaf3b93bbea0d56ca99a32aa72/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular2 State Management 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { provideStore } from '@ngrx/store'; 6 | import { environment } from './environments/environment'; 7 | import { AppModule } from './app/'; 8 | import { notes } from './app/notes/reducers/notes.reducer'; 9 | import { NotesDataService } from './app/notes/services/notes.data.service'; 10 | import { NotesEffectsService } from './app/notes/services/notes.effects.service'; 11 | import { EffectsModule } from '@ngrx/effects'; 12 | if (environment.production) { 13 | enableProdMode(); 14 | } 15 | 16 | platformBrowserDynamic().bootstrapModule(AppModule); -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | 10 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 11 | declare var __karma__: any; 12 | declare var require: any; 13 | 14 | // Prevent Karma from running prematurely. 15 | __karma__.loaded = function () {}; 16 | 17 | 18 | Promise.all([ 19 | System.import('@angular/core/testing'), 20 | System.import('@angular/platform-browser-dynamic/testing') 21 | ]) 22 | // First, initialize the Angular testing environment. 23 | .then(([testing, testingBrowser]) => { 24 | testing.getTestBed().initTestEnvironment( 25 | testingBrowser.BrowserDynamicTestingModule, 26 | testingBrowser.platformBrowserDynamicTesting() 27 | ); 28 | }) 29 | // Then we find all the tests. 30 | .then(() => require.context('./', true, /\.spec\.ts/)) 31 | // And load the modules. 32 | .then(context => context.keys().map(context)) 33 | // Finally, start Karma to run the tests. 34 | .then(__karma__.start, __karma__.error); 35 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "dom"], 7 | "mapRoot": "./", 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "outDir": "../dist/out-tsc", 11 | "sourceMap": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "../node_modules/@types" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, see links for more information 2 | // https://github.com/typings/typings 3 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 4 | 5 | declare var System: any; 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "max-line-length": [true, 100], 5 | "no-inferrable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "eofline": true, 16 | "no-duplicate-variable": true, 17 | "no-eval": true, 18 | "no-arg": true, 19 | "no-internal-module": true, 20 | "no-trailing-whitespace": true, 21 | "no-bitwise": true, 22 | "no-shadowed-variable": true, 23 | "no-unused-expression": true, 24 | "no-unused-variable": true, 25 | "one-line": [ 26 | true, 27 | "check-catch", 28 | "check-else", 29 | "check-open-brace", 30 | "check-whitespace" 31 | ], 32 | "quotemark": [ 33 | true, 34 | "single", 35 | "avoid-escape" 36 | ], 37 | "semicolon": [true, "always"], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "curly": true, 49 | "variable-name": [ 50 | true, 51 | "ban-keywords", 52 | "check-format", 53 | "allow-trailing-underscore" 54 | ], 55 | "whitespace": [ 56 | true, 57 | "check-branch", 58 | "check-decl", 59 | "check-operator", 60 | "check-separator", 61 | "check-type" 62 | ], 63 | "component-selector-name": [true, "kebab-case"], 64 | "component-selector-type": [true, "element"] 65 | } 66 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDevDependencies": { 3 | "angular-protractor": "registry:dt/angular-protractor#1.5.0+20160425143459", 4 | "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", 5 | "selenium-webdriver": "registry:dt/selenium-webdriver#2.44.0+20160317120654" 6 | }, 7 | "ambientDependencies": { 8 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654" 9 | }, 10 | "dependencies": { 11 | "node-uuid": "registry:npm/node-uuid#1.4.7+20160503013124" 12 | } 13 | } 14 | --------------------------------------------------------------------------------