├── .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 | [](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 |
--------------------------------------------------------------------------------