├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── .test.bats ├── README.md ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── redux-counter ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── package.json ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.state.ts │ │ ├── app.store.ts │ │ ├── counter.actions.ts │ │ └── counter.reducer.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── tsconfig.app.json │ └── typings.d.ts ├── tsconfig.json └── tslint.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.reducer.ts │ ├── app.store.ts │ ├── chat-message │ │ ├── chat-message.component.css │ │ ├── chat-message.component.html │ │ ├── chat-message.component.spec.ts │ │ └── chat-message.component.ts │ ├── chat-nav-bar │ │ ├── chat-nav-bar.component.css │ │ ├── chat-nav-bar.component.html │ │ ├── chat-nav-bar.component.spec.ts │ │ └── chat-nav-bar.component.ts │ ├── chat-page │ │ ├── chat-page.component.css │ │ ├── chat-page.component.html │ │ ├── chat-page.component.spec.ts │ │ └── chat-page.component.ts │ ├── chat-thread │ │ ├── chat-thread.component.css │ │ ├── chat-thread.component.html │ │ ├── chat-thread.component.spec.ts │ │ └── chat-thread.component.ts │ ├── chat-threads │ │ ├── chat-threads.component.css │ │ ├── chat-threads.component.html │ │ ├── chat-threads.component.spec.ts │ │ └── chat-threads.component.ts │ ├── chat-window │ │ ├── chat-window.component.css │ │ ├── chat-window.component.html │ │ ├── chat-window.component.spec.ts │ │ └── chat-window.component.ts │ ├── data │ │ └── chat-example-data.ts │ ├── message │ │ └── message.model.ts │ ├── pipes │ │ ├── from-now.pipe.spec.ts │ │ └── from-now.pipe.ts │ ├── thread │ │ ├── thread.actions.ts │ │ ├── thread.model.ts │ │ └── threads.reducer.ts │ ├── user │ │ ├── user.actions.ts │ │ ├── user.model.ts │ │ └── users.reducer.ts │ └── util │ │ └── uuid.ts ├── assets │ ├── .gitkeep │ ├── css │ │ ├── chat.scss │ │ └── styles.scss │ ├── fonts │ │ └── bootstrap │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ └── images │ │ ├── avatars │ │ ├── female-avatar-1.png │ │ ├── female-avatar-2.png │ │ ├── female-avatar-3.png │ │ ├── female-avatar-4.png │ │ ├── male-avatar-1.png │ │ ├── male-avatar-2.png │ │ ├── male-avatar-3.png │ │ └── male-avatar-4.png │ │ ├── logos │ │ ├── Angular2ReduxChatHeaderImage.png │ │ └── ng-book-2-minibook.png │ │ └── readme │ │ ├── full-chat-preview.png │ │ ├── minimal-redux-ts.png │ │ ├── ng-book-2-as-book-cover-pigment.png │ │ ├── redux-chat-echo-bot.png │ │ ├── redux-chat-initial-state.png │ │ ├── redux-chat-models.png │ │ ├── redux-chat-top-level-components.png │ │ └── working-counter-app.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tslint.json └── tutorial ├── .gitignore ├── .test └── bats.sh ├── 01-identity-reducer.ts ├── 02-adjusting-reducer.ts ├── 03-adjusting-reducer-switch.ts ├── 04-plus-action.ts ├── 05-minimal-store.ts ├── 06-rx-store.ts ├── 06-store-w-subscribe.ts ├── 06b-rx-store.ts ├── 07-messages-reducer.ts ├── 08-action-creators.ts ├── 09-real-redux.ts ├── lib └── miniRedux.ts ├── package.json ├── redux2.ts └── tsconfig.json /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "angular-redux-chat" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.css" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json" 40 | }, 41 | { 42 | "project": "src/tsconfig.spec.json" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json" 46 | } 47 | ], 48 | "test": { 49 | "karma": { 50 | "config": "./karma.conf.js" 51 | } 52 | }, 53 | "defaults": { 54 | "styleExt": "css", 55 | "component": {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.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 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage/* 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | #System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /.test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | DIR=$(dirname $BATS_TEST_FILENAME) 3 | 4 | load "${NGBOOK_ROOT}/scripts/bats/fullstack.bats" 5 | load "${NGBOOK_ROOT}/scripts/bats-support/load.bash" 6 | load "${NGBOOK_ROOT}/scripts/bats-assert/load.bash" 7 | 8 | # @test "angular-redux-chat unit tests pass" { 9 | # cd $DIR 10 | # run ng test --single-run 11 | # assert_output --partial 'SUCCESS' 12 | # } 13 | 14 | @test "angular-redux-chat e2e tests pass" { 15 | cd $DIR 16 | run_ng_e2e $TEST_TMP_DIR 17 | run cat ${TEST_TMP_DIR}/log.txt 18 | assert_output --partial 'SUCCESS' 19 | } 20 | 21 | @test "angular-redux-chat linting passes" { 22 | cd $DIR 23 | run npm run lint 24 | assert_output --partial 'All files pass linting' 25 | } 26 | 27 | setup() { 28 | echo "travis_fold:start:angular-redux-chat" 29 | cd $DIR 30 | TEST_TMP_DIR="$(mktemp -d -t fullstackXXX)" 31 | kill_ng_cli || : 32 | kill_by_port 4200 33 | true 34 | } 35 | 36 | teardown() { 37 | cd $DIR 38 | kill_ng_cli || : 39 | kill_by_port 4200 40 | echo "travis_fold:end:angular-redux-chat" 41 | true 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Angular 2 Redux Chat 3 |

4 | 5 | # Angular 2 Redux Chat [![Join the chat at https://gitter.im/ng-book/ng-book](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ng-book/ng-book?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | > An Angular 2 chat app using [Angular 2](https://angular.io/), [Redux](https://github.com/reactjs/redux), [Angular CLI](https://github.com/angular/angular-cli), [Webpack](https://webpack.github.io/), [TypeScript](http://www.typescriptlang.org/), Services, Injectables, [Karma](http://karma-runner.github.io/), Forms, [SCSS](http://sass-lang.com/), and [tslint](http://palantir.github.io/tslint/) by the [ng-book 2 team](https://ng-book.com/2) 8 | 9 | This repo shows an example chat application using Redux and Angular 2. The goal is to show how to use the Redux data architecture pattern within Angular 2, using the core Redux library. It also features: 10 | 11 | * A step-by-step tutorial on how to write a [minimal Redux clone in Typescript](minimal/tutorial) 12 | * How to write a [minimal counter app with Redux and Angular 2](minimal) 13 | * Webpack configuration with TypeScript, Karma, SCSS, and tslint 14 | * How to write injectable services in Angular 2 15 | * How to separate presentational vs. container components 16 | * Using action creators 17 | * How to compose reducers 18 | * And much more 19 | 20 | > ### Try the live [demo here](http://redux-chat.ng-book.com) 21 | 22 |

23 | Angular 2 Redux Chat 24 |

25 | 26 | 27 | ## Quick start 28 | 29 | ```bash 30 | # clone the repo 31 | git clone https://github.com/ng-book/angular2-redux-chat.git 32 | 33 | # change into the repo directory 34 | cd angular2-redux-chat 35 | 36 | # install 37 | npm install 38 | 39 | # run 40 | npm start 41 | ``` 42 | 43 | Then visit [http://localhost:4200](http://localhost:4200) in your browser. 44 | 45 | ## Architecture 46 | 47 | The app has three models: 48 | 49 | * [`Message`](src/app/message/message.model.ts) - holds individual chat messages 50 | * [`Thread`](src/app/thread/thread.model.ts) - holds metadata for a group of `Message`s 51 | * [`User`](src/app/user/user.model.ts) - holds data about an individual user 52 | 53 |

54 | Model Diagram 55 |

56 | 57 | There are two reducers: 58 | 59 | * [`ThreadsReducer`](src/app/thread/threads.reducer.ts) - manages the `Thread`s and their `Message`s 60 | * [`UsersReducer`](src/app/user/users.reducer.ts) - manages the current `User` 61 | 62 | There are also three top-level components: 63 | 64 | * [`ChatNavBar`](src/app/chat-nav-bar/chat-nav-bar.component.ts) - for the top navigation bar and unread messages count 65 | * [`ChatThreads`](src/app/chat-threads/chat-threads.component.ts) - for our clickable list of threads 66 | * [`ChatWindow`](src/app/chat-window/chat-window.component.ts) - where we hold our current conversation 67 | 68 |

69 | Angular 2 Redux Chat 70 |

71 | 72 | ## Components Subscribe to the Store 73 | 74 | In this project, we're using the [official Redux library](https://github.com/reactjs/redux) instead of a wrapper or Redux-inspired spin-off. At the top of our app we create a new Redux Store and [provide it to the dependency injection system](src/app/app.ts#L55). This let's us inject it into our components. 75 | 76 | Our [container components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.a2dspl7hn) inject the Redux Store and subscribe to any changes. Consider this excerpt from the nav-bar which keeps the count of unread messages: 77 | 78 | ```typescript 79 | export default class ChatNavBar { 80 | unreadMessagesCount: number; 81 | 82 | constructor(@Inject(AppStore) private store: Store) { 83 | store.subscribe(() => this.updateState()); 84 | this.updateState(); 85 | } 86 | 87 | updateState() { 88 | // getUnreadMessagesCount is a selector function 89 | this.unreadMessagesCount = getUnreadMessagesCount(this.store.getState()); 90 | } 91 | } 92 | ``` 93 | 94 | You can see that in the constructor we inject our `Store` (which is typed to `AppState`). We immediately subscribe to any changes in the store. This callback will not be called unless an action is dispatched to the store, so we need to make sure we load the initial data. To do this, we call `this.updateState()` one time after the subscription. 95 | 96 | `updateState` reads the state from the store (`this.store.getState()`) and then calls the _selector function_ `getUnreadMessagesCount` (you can [find the implementation of that here](src/app/reducers/ThreadsReducer.ts#L138)). `getUnreadMessagesCount` calculates the number of unread messages. We then take that value and set it to `this.unreadMessagesCount`. Because `unreadMessagesCount` is an instance variable which appears in the template, Angular will rerender this component when the value changes. 97 | 98 | This pattern is used throughout the app. 99 | 100 | Understanding how everything fits together with Redux can be tricky, but this code is heavily commented. One strategy to understand this code is to start at the components and see how they read the Store with selectors, dispatch actions, and follow that through the reducers. The other strategy is to get a copy of [ng-book 2](https://ng-book.com/2) where we explain each line in detail over ~60 pages. 101 | 102 | ## State 103 | 104 | The top-level state has two keys: `users` and `threads`: 105 | 106 | ```typescript 107 | interface AppState { 108 | users: UsersState; 109 | threads: ThreadsState; 110 | } 111 | 112 | interface UsersState { 113 | currentUser: User; 114 | }; 115 | 116 | export interface ThreadsEntities { 117 | [id: string]: Thread; 118 | } 119 | 120 | export interface ThreadsState { 121 | ids: string[]; 122 | entities: ThreadsEntities; 123 | currentThreadId?: string; 124 | }; 125 | ``` 126 | 127 | ThreadsState stores the list of Threads indexed by id in `entities`, as well as a complete list of the ids in `ids`. 128 | 129 | We also store the id of the current thread so that we know what the user is currently looking at - this is valuable for the unread messages count, for instance. 130 | 131 | In this app, we store the Messages in their respective Thread and we don't store the Messages apart from that Thread. In your app you may find it useful to separate Messages into their own Messages reducer and keep only a list of Message ids in your Threads. 132 | 133 | Here's a screenshot using [Redux Devtools](https://github.com/gaearon/redux-devtools) of the initial state: 134 | 135 |

136 | Angular 2 Redux Chat State Tree 137 |

138 | 139 | 140 | ## Bots 141 | 142 | This app implements a few simple chat bots. For instance: 143 | 144 | * Echo bot 145 | * Reversing bot 146 | * Waiting bot 147 | 148 | Angular 2 Redux Chat Bots 149 | 150 |
151 | 152 | 153 | ## Detailed Installation 154 | 155 | **Step 1: Install Node.js from the [Node Website](http://nodejs.org/).** 156 | 157 | We recommend Node version 4.1 or above. You can check your node version by running this: 158 | 159 | ```bash 160 | $ node -v 161 | vv4.1... 162 | ``` 163 | 164 | **Step 2: Install Dependencies** 165 | 166 | ```bash 167 | npm install 168 | ``` 169 | 170 | ## Running the App 171 | 172 | ```bash 173 | npm start 174 | ``` 175 | 176 | Then visit [http://localhost:4200](http://localhost:4200) in your browser. 177 | 178 | ## Running the Tests 179 | 180 | You can run the unit tests with: 181 | 182 | ```bash 183 | npm run test 184 | ``` 185 | 186 | ## Build Redux in TypeScript Tutorial 187 | 188 | This repository contains a step-by-step tutorial on how to build a minimal-redux store in Typescript. You can read a [blog post explaining this code here](#). You can also find the code in [`minimal/tutorial`](minimal/tutorial). The final result looks like this (with or without Observables): 189 | 190 |

191 | Minimal Redux in TypeScript 192 |

193 | 194 | ## Minimal Angular 2 Redux Integration 195 | 196 | This repository also contains an example of a minimal integration of Redux with Angular 2 to build a counter app. You can also read about how to build this project [here at the ng-book blog](#). 197 | 198 |

199 | Minimal Redux and Angular 2 Counter 200 |

201 | 202 | ## Series 203 | 204 | This repo is part of a series of projects that discuss data architecture with Angular 2. You can find this same project implemented with Observable streams instead of Redux here: 205 | 206 | * [`angular2-rxjs-chat`](https://github.com/ng-book/angular2-rxjs-chat) 207 | 208 | ## Contributing 209 | 210 | There are lots of other little things that need cleaned up such as: 211 | 212 | - More tests 213 | - Cleaning up the vendor scripts / typings 214 | 215 | If you'd like to contribute, feel free to submit a pull request and we'll likely merge it in. 216 | 217 | ## Getting Help 218 | 219 | If you're having trouble getting this project running, feel free to [open an issue](https://github.com/ng-book/angular2-redux-chat/issues), join us on [Gitter](https://gitter.im/ng-book/ng-book?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge), or [email us](mailto:us@fullstack.io)! 220 | 221 | ___ 222 | 223 | # ng-book 2 224 | 225 | 226 | ng-book 2 227 | 228 | 229 | This repo was written and is maintained by the [ng-book 2](https://ng-book.com/2) team. In the book we talk about each line of code in this app and explain why it's there and how it works. 230 | 231 | This app is only one of several apps we have in the book. If you're looking to learn Angular 2, there's no faster way than by spending a few hours with ng-book 2. 232 | 233 |
234 | 235 | ## License 236 | [MIT](/LICENSE.md) 237 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AngularReduxChatPage } from './app.po'; 2 | 3 | describe('angular-redux-chat App', () => { 4 | let page: AngularReduxChatPage; 5 | 6 | beforeEach(() => { 7 | page = new AngularReduxChatPage(); 8 | }); 9 | 10 | it('should load the page', () => { 11 | page.navigateTo(); 12 | 13 | expect(page.unreadCount()).toMatch(`3`); 14 | 15 | page.clickThread(1); 16 | expect(page.unreadCount()).toMatch(`2`); 17 | 18 | page.clickThread(2); 19 | expect(page.unreadCount()).toMatch(`1`); 20 | 21 | page.clickThread(3); 22 | expect(page.unreadCount()).toMatch(`0`); 23 | 24 | page.sendMessage('3'); 25 | expect(page.unreadCount()).toMatch(`0`); 26 | 27 | page.clickThread(0); 28 | // expect(page.unreadCount()).toMatch(`1`); 29 | expect(page.getConversationText(3)).toContain(`I waited 3 seconds`); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class AngularReduxChatPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | 12 | getHeaderText() { 13 | return element(by.css('h1')).getText(); 14 | } 15 | 16 | unreadCount() { 17 | return element(by.css('.badge')).getText(); 18 | } 19 | 20 | clickThread(i) { 21 | return element.all(by.css('chat-thread')).get(i).click(); 22 | } 23 | 24 | sendMessage(msg) { 25 | element(by.css('.chat-input')).sendKeys(msg); 26 | return element(by.buttonText('Send')).click(); 27 | } 28 | 29 | getConversationText(i) { 30 | return element.all(by.css('.conversation')).get(i).getText(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016" 10 | ], 11 | "outDir": "../dist/out-tsc-e2e", 12 | "module": "commonjs", 13 | "target": "es6", 14 | "types":[ 15 | "jasmine", 16 | "node" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false } 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular/cli'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-redux-chat", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "4.2.0", 16 | "@angular/compiler": "4.2.0", 17 | "@angular/core": "4.2.0", 18 | "@angular/forms": "4.2.0", 19 | "@angular/http": "4.2.0", 20 | "@angular/platform-browser": "4.2.0", 21 | "@angular/platform-browser-dynamic": "4.2.0", 22 | "@angular/router": "4.2.0", 23 | "core-js": "2.4.1", 24 | "moment": "2.18.0", 25 | "redux": "3.6.0", 26 | "reselect": "2.5.4", 27 | "rxjs": "5.0.1", 28 | "zone.js": "0.8.10", 29 | "reflect-metadata": "0.1.3", 30 | "@types/jasmine": "2.5.40" 31 | }, 32 | "devDependencies": { 33 | "@angular/cli": "1.2.0-beta.1", 34 | "@angular/compiler-cli": "4.2.0", 35 | "@types/jasmine": "2.5.38", 36 | "@types/node": "~6.0.60", 37 | "codelyzer": "~2.0.0", 38 | "jasmine-core": "~2.5.2", 39 | "jasmine-spec-reporter": "~3.2.0", 40 | "karma": "~1.4.1", 41 | "karma-chrome-launcher": "~2.0.0", 42 | "karma-cli": "~1.0.1", 43 | "karma-jasmine": "~1.1.0", 44 | "karma-jasmine-html-reporter": "0.2.2", 45 | "karma-coverage-istanbul-reporter": "0.2.0", 46 | "protractor": "~5.1.0", 47 | "ts-node": "~2.0.0", 48 | "tslint": "~4.4.2", 49 | "typescript": "2.3.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | beforeLaunch: function() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | }, 27 | onPrepare() { 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /redux-counter/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "redux-counter" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.css" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json" 40 | }, 41 | { 42 | "project": "src/tsconfig.spec.json" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json" 46 | } 47 | ], 48 | "test": { 49 | "karma": { 50 | "config": "./karma.conf.js" 51 | } 52 | }, 53 | "defaults": { 54 | "styleExt": "css", 55 | "component": {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /redux-counter/.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 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /redux-counter/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage/* 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | #System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /redux-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-counter", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "4.2.0", 16 | "@angular/compiler": "4.2.0", 17 | "@angular/core": "4.2.0", 18 | "@angular/forms": "4.2.0", 19 | "@angular/http": "4.2.0", 20 | "@angular/platform-browser": "4.2.0", 21 | "@angular/platform-browser-dynamic": "4.2.0", 22 | "@angular/router": "4.2.0", 23 | "core-js": "2.4.1", 24 | "redux": "3.6.0", 25 | "rxjs": "5.0.1", 26 | "zone.js": "0.8.10", 27 | "reflect-metadata": "0.1.3", 28 | "@types/jasmine": "2.5.40" 29 | }, 30 | "devDependencies": { 31 | "@angular/cli": "1.2.0-beta.1", 32 | "@angular/compiler-cli": "4.2.0", 33 | "@types/jasmine": "2.5.38", 34 | "@types/node": "~6.0.60", 35 | "codelyzer": "~2.0.0", 36 | "jasmine-core": "~2.5.2", 37 | "jasmine-spec-reporter": "~3.2.0", 38 | "karma": "~1.4.1", 39 | "karma-chrome-launcher": "~2.0.0", 40 | "karma-cli": "~1.0.1", 41 | "karma-jasmine": "~1.1.0", 42 | "karma-jasmine-html-reporter": "0.2.2", 43 | "karma-coverage-istanbul-reporter": "0.2.0", 44 | "protractor": "~5.1.0", 45 | "ts-node": "~2.0.0", 46 | "tslint": "~4.4.2", 47 | "typescript": "2.3.2" 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-counter/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/redux-counter/src/app/app.component.css -------------------------------------------------------------------------------- /redux-counter/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Counter

6 |

Custom Store

7 | 8 |

9 | The counter value is: 10 | {{ counter }} 11 |

12 | 13 |

14 | 18 | 22 |

23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /redux-counter/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | xdescribe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app works!'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app works!'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /redux-counter/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { Store } from 'redux'; 3 | import { AppStore } from './app.store'; 4 | import { AppState } from './app.state'; 5 | import * as CounterActions from './counter.actions'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.css'] 11 | }) 12 | export class AppComponent { 13 | counter: number; 14 | 15 | constructor(@Inject(AppStore) private store: Store) { 16 | store.subscribe(() => this.readState()); 17 | this.readState(); 18 | } 19 | 20 | readState() { 21 | const state: AppState = this.store.getState() as AppState; 22 | this.counter = state.counter; 23 | } 24 | 25 | increment() { 26 | this.store.dispatch(CounterActions.increment()); 27 | } 28 | 29 | decrement() { 30 | this.store.dispatch(CounterActions.decrement()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /redux-counter/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { appStoreProviders } from './app.store'; 7 | 8 | import { AppComponent } from './app.component'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent 13 | ], 14 | imports: [ 15 | BrowserModule, 16 | FormsModule, 17 | HttpModule 18 | ], 19 | providers: [ appStoreProviders ], 20 | bootstrap: [AppComponent] 21 | }) 22 | export class AppModule { } 23 | -------------------------------------------------------------------------------- /redux-counter/src/app/app.state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * minimal counter app state 3 | * 4 | * In this case, our app state is simply a single number (the counter). But we 5 | * put it here because in the future, when our state is more complicated 6 | * 7 | */ 8 | 9 | export interface AppState { 10 | counter: number; 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /redux-counter/src/app/app.store.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { 3 | createStore, 4 | Store, 5 | compose, 6 | StoreEnhancer 7 | } from 'redux'; 8 | 9 | import { AppState } from './app.state'; 10 | import { 11 | counterReducer as reducer 12 | } from './counter.reducer'; 13 | 14 | export const AppStore = new InjectionToken('App.store'); 15 | 16 | const devtools: StoreEnhancer = 17 | window['devToolsExtension'] ? 18 | window['devToolsExtension']() : f => f; 19 | 20 | export function createAppStore(): Store { 21 | return createStore( 22 | reducer, 23 | compose(devtools) 24 | ); 25 | } 26 | 27 | export const appStoreProviders = [ 28 | { provide: AppStore, useFactory: createAppStore } 29 | ]; 30 | -------------------------------------------------------------------------------- /redux-counter/src/app/counter.actions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | ActionCreator 4 | } from 'redux'; 5 | 6 | export const INCREMENT: string = 'INCREMENT'; 7 | export const increment: ActionCreator = () => ({ 8 | type: INCREMENT 9 | }); 10 | 11 | export const DECREMENT: string = 'DECREMENT'; 12 | export const decrement: ActionCreator = () => ({ 13 | type: DECREMENT 14 | }); 15 | -------------------------------------------------------------------------------- /redux-counter/src/app/counter.reducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Counter Reducer 3 | */ 4 | import { Reducer, Action } from 'redux'; 5 | import { AppState } from './app.state'; 6 | import { 7 | INCREMENT, 8 | DECREMENT 9 | } from './counter.actions'; 10 | 11 | const initialState: AppState = { counter: 0 }; 12 | 13 | // Create our reducer that will handle changes to the state 14 | export const counterReducer: Reducer = 15 | (state: AppState = initialState, action: Action): AppState => { 16 | switch (action.type) { 17 | case INCREMENT: 18 | return Object.assign({}, state, { counter: state.counter + 1 }); 19 | case DECREMENT: 20 | return Object.assign({}, state, { counter: state.counter - 1 }); 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /redux-counter/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /redux-counter/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 | -------------------------------------------------------------------------------- /redux-counter/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/redux-counter/src/favicon.ico -------------------------------------------------------------------------------- /redux-counter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReduxCounter 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /redux-counter/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /redux-counter/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /redux-counter/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /redux-counter/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /redux-counter/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /redux-counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /redux-counter/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | xdescribe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app works!'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app works!'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import * as Redux from 'redux'; 3 | 4 | import { AppStore } from './app.store'; 5 | import { AppState } from './app.reducer'; 6 | import { ChatExampleData } from './data/chat-example-data'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.css'] 12 | }) 13 | export class AppComponent { 14 | constructor(@Inject(AppStore) private store: Redux.Store) { 15 | ChatExampleData(store); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { 7 | AppState, 8 | default as reducer 9 | } from './app.reducer'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { ChatMessageComponent } from './chat-message/chat-message.component'; 13 | import { ChatThreadComponent } from './chat-thread/chat-thread.component'; 14 | import { ChatNavBarComponent } from './chat-nav-bar/chat-nav-bar.component'; 15 | import { ChatThreadsComponent } from './chat-threads/chat-threads.component'; 16 | import { ChatWindowComponent } from './chat-window/chat-window.component'; 17 | import { ChatPageComponent } from './chat-page/chat-page.component'; 18 | import { FromNowPipe } from './pipes/from-now.pipe'; 19 | 20 | import { 21 | AppStore, 22 | appStoreProviders 23 | } from './app.store'; 24 | 25 | @NgModule({ 26 | declarations: [ 27 | AppComponent, 28 | ChatMessageComponent, 29 | ChatThreadComponent, 30 | ChatNavBarComponent, 31 | ChatThreadsComponent, 32 | ChatWindowComponent, 33 | ChatPageComponent, 34 | FromNowPipe 35 | ], 36 | imports: [ 37 | BrowserModule, 38 | FormsModule, 39 | HttpModule 40 | ], 41 | providers: [ 42 | appStoreProviders 43 | ], 44 | 45 | bootstrap: [AppComponent] 46 | }) 47 | export class AppModule { } 48 | -------------------------------------------------------------------------------- /src/app/app.reducer.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:typedef */ 2 | 3 | import { 4 | Reducer, 5 | combineReducers 6 | } from 'redux'; 7 | import { 8 | UsersState, 9 | UsersReducer 10 | } from './user/users.reducer'; 11 | export * from './user/users.reducer'; 12 | import { 13 | ThreadsState, 14 | ThreadsReducer 15 | } from './thread/threads.reducer'; 16 | export * from './thread/threads.reducer'; 17 | 18 | export interface AppState { 19 | users: UsersState; 20 | threads: ThreadsState; 21 | } 22 | 23 | const rootReducer: Reducer = combineReducers({ 24 | users: UsersReducer, 25 | threads: ThreadsReducer 26 | }); 27 | 28 | export default rootReducer; 29 | -------------------------------------------------------------------------------- /src/app/app.store.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { 3 | createStore, 4 | Store, 5 | compose, 6 | StoreEnhancer 7 | } from 'redux'; 8 | 9 | import { 10 | AppState, 11 | default as reducer 12 | } from './app.reducer'; 13 | 14 | export const AppStore = new InjectionToken('App.store'); 15 | 16 | const devtools: StoreEnhancer = 17 | window['devToolsExtension'] ? 18 | window['devToolsExtension']() : f => f; 19 | 20 | export function createAppStore(): Store { 21 | return createStore( 22 | reducer, 23 | compose(devtools) 24 | ); 25 | } 26 | 27 | export const appStoreProviders = [ 28 | { provide: AppStore, useFactory: createAppStore } 29 | ]; 30 | -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/chat-message/chat-message.component.css -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
6 | 7 |
8 | 9 |
11 |

{{message.text}}

12 |

{{message.sender}} • {{message.sentAt | fromNow}}

13 |
14 | 15 |
17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatMessageComponent } from './chat-message.component'; 4 | 5 | xdescribe('ChatMessageComponent', () => { 6 | let component: ChatMessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatMessageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatMessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input 5 | } from '@angular/core'; 6 | import { Message } from '../message/message.model'; 7 | 8 | @Component({ 9 | selector: 'chat-message', 10 | templateUrl: './chat-message.component.html', 11 | styleUrls: ['./chat-message.component.css'] 12 | }) 13 | export class ChatMessageComponent implements OnInit { 14 | @Input() message: Message; 15 | incoming: boolean; 16 | 17 | ngOnInit(): void { 18 | this.incoming = !this.message.author.isClient; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/chat-nav-bar/chat-nav-bar.component.css -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatNavBarComponent } from './chat-nav-bar.component'; 4 | 5 | xdescribe('ChatNavBarComponent', () => { 6 | let component: ChatNavBarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatNavBarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatNavBarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { AppStore } from '../app.store'; 3 | import * as Redux from 'redux'; 4 | import { 5 | AppState, 6 | getUnreadMessagesCount 7 | } from '../app.reducer'; 8 | 9 | @Component({ 10 | selector: 'chat-nav-bar', 11 | templateUrl: './chat-nav-bar.component.html', 12 | styleUrls: ['./chat-nav-bar.component.css'] 13 | }) 14 | export class ChatNavBarComponent { 15 | unreadMessagesCount: number; 16 | 17 | constructor(@Inject(AppStore) private store: Redux.Store) { 18 | store.subscribe(() => this.updateState()); 19 | this.updateState(); 20 | } 21 | 22 | updateState() { 23 | this.unreadMessagesCount = getUnreadMessagesCount(this.store.getState()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/chat-page/chat-page.component.css -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatPageComponent } from './chat-page.component'; 4 | 5 | xdescribe('ChatPageComponent', () => { 6 | let component: ChatPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'chat-page', 5 | templateUrl: './chat-page.component.html', 6 | styleUrls: ['./chat-page.component.css'] 7 | }) 8 | export class ChatPageComponent implements OnInit { 9 | constructor() { } 10 | ngOnInit() { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/chat-thread/chat-thread.component.css -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |
6 |
7 |
{{thread.name}} 8 | 9 |
10 | 11 | {{thread.messages[thread.messages.length - 1].text}} 12 | 13 |
14 | Select 15 |
16 | -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatThreadComponent } from './chat-thread.component'; 4 | 5 | xdescribe('ChatThreadComponent', () => { 6 | let component: ChatThreadComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatThreadComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatThreadComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input, 5 | Output, 6 | EventEmitter 7 | } from '@angular/core'; 8 | import { Thread } from '../thread/thread.model'; 9 | 10 | @Component({ 11 | selector: 'chat-thread', 12 | templateUrl: './chat-thread.component.html', 13 | styleUrls: ['./chat-thread.component.css'] 14 | }) 15 | export class ChatThreadComponent implements OnInit { 16 | @Input() thread: Thread; 17 | @Input() selected: boolean; 18 | @Output() onThreadSelected: EventEmitter; 19 | 20 | constructor() { 21 | this.onThreadSelected = new EventEmitter(); 22 | } 23 | 24 | ngOnInit() { } 25 | 26 | clicked(event: any): void { 27 | this.onThreadSelected.emit(this.thread); 28 | event.preventDefault(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/chat-threads/chat-threads.component.css -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatThreadsComponent } from './chat-threads.component'; 4 | 5 | xdescribe('ChatThreadsComponent', () => { 6 | let component: ChatThreadsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatThreadsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatThreadsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Inject 5 | } from '@angular/core'; 6 | import { AppStore } from '../app.store'; 7 | import * as Redux from 'redux'; 8 | import { 9 | Thread 10 | } from '../thread/thread.model'; 11 | import * as ThreadActions from '../thread/thread.actions'; 12 | import { 13 | AppState, 14 | getCurrentThread, 15 | getAllThreads 16 | } from '../app.reducer'; 17 | 18 | @Component({ 19 | selector: 'chat-threads', 20 | templateUrl: './chat-threads.component.html', 21 | styleUrls: ['./chat-threads.component.css'] 22 | }) 23 | export class ChatThreadsComponent { 24 | threads: Thread[]; 25 | currentThreadId: string; 26 | 27 | constructor(@Inject(AppStore) private store: Redux.Store) { 28 | store.subscribe(() => this.updateState()); 29 | this.updateState(); 30 | } 31 | 32 | updateState() { 33 | const state = this.store.getState(); 34 | 35 | // Store the threads list 36 | this.threads = getAllThreads(state); 37 | 38 | // We want to mark the current thread as selected, 39 | // so we store the currentThreadId as a value 40 | this.currentThreadId = getCurrentThread(state).id; 41 | } 42 | 43 | handleThreadClicked(thread: Thread) { 44 | this.store.dispatch(ThreadActions.selectThread(thread)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/app/chat-window/chat-window.component.css -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |

9 | 10 | Chat - {{currentThread.name}} 11 |

12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 | 22 | 23 |
24 | 25 | 39 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatWindowComponent } from './chat-window.component'; 4 | 5 | xdescribe('ChatWindowComponent', () => { 6 | let component: ChatWindowComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatWindowComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatWindowComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Inject, 4 | ElementRef 5 | } from '@angular/core'; 6 | import * as Redux from 'redux'; 7 | 8 | import { AppStore } from '../app.store'; 9 | import { User } from '../user/user.model'; 10 | import { Thread } from '../thread/thread.model'; 11 | import * as ThreadActions from '../thread/thread.actions'; 12 | import { 13 | AppState, 14 | getCurrentThread, 15 | getCurrentUser 16 | } from '../app.reducer'; 17 | 18 | @Component({ 19 | selector: 'chat-window', 20 | templateUrl: './chat-window.component.html', 21 | styleUrls: ['./chat-window.component.css'] 22 | }) 23 | export class ChatWindowComponent { 24 | currentThread: Thread; 25 | draftMessage: { text: string }; 26 | currentUser: User; 27 | 28 | constructor(@Inject(AppStore) private store: Redux.Store, 29 | private el: ElementRef) { 30 | store.subscribe(() => this.updateState() ); 31 | this.updateState(); 32 | this.draftMessage = { text: '' }; 33 | } 34 | 35 | updateState() { 36 | const state = this.store.getState(); 37 | this.currentThread = getCurrentThread(state); 38 | this.currentUser = getCurrentUser(state); 39 | this.scrollToBottom(); 40 | } 41 | 42 | scrollToBottom(): void { 43 | const scrollPane: any = this.el 44 | .nativeElement.querySelector('.msg-container-base'); 45 | if (scrollPane) { 46 | setTimeout(() => scrollPane.scrollTop = scrollPane.scrollHeight); 47 | } 48 | } 49 | 50 | sendMessage(): void { 51 | this.store.dispatch(ThreadActions.addMessage( 52 | this.currentThread, 53 | { 54 | author: this.currentUser, 55 | isRead: true, 56 | text: this.draftMessage.text 57 | } 58 | )); 59 | this.draftMessage = { text: '' }; 60 | } 61 | 62 | onEnter(event: any): void { 63 | this.sendMessage(); 64 | event.preventDefault(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/data/chat-example-data.ts: -------------------------------------------------------------------------------- 1 | import * as Redux from 'redux'; 2 | import { 3 | AppState, 4 | getAllMessages 5 | } from '../app.reducer'; 6 | import { uuid } from '../util/uuid'; 7 | import * as moment from 'moment'; 8 | import { Thread } from '../thread/thread.model'; 9 | import * as ThreadActions from '../thread/thread.actions'; 10 | import { User } from '../user/user.model'; 11 | import * as UserActions from '../user/user.actions'; 12 | 13 | /** 14 | * ChatExampleData sets up the initial data for our chats as well as 15 | * configuring the "bots". 16 | */ 17 | 18 | // the person using the app is Juliet 19 | const me: User = { 20 | id: uuid(), 21 | isClient: true, // <-- notice we're specifying the client as this User 22 | name: 'Juliet', 23 | avatarSrc: 'assets/images/avatars/female-avatar-1.png' 24 | }; 25 | 26 | const ladycap: User = { 27 | id: uuid(), 28 | name: 'Lady Capulet', 29 | avatarSrc: 'assets/images/avatars/female-avatar-2.png' 30 | }; 31 | 32 | const echo: User = { 33 | id: uuid(), 34 | name: 'Echo Bot', 35 | avatarSrc: 'assets/images/avatars/male-avatar-1.png' 36 | }; 37 | 38 | const rev: User = { 39 | id: uuid(), 40 | name: 'Reverse Bot', 41 | avatarSrc: 'assets/images/avatars/female-avatar-4.png' 42 | }; 43 | 44 | const wait: User = { 45 | id: uuid(), 46 | name: 'Waiting Bot', 47 | avatarSrc: 'assets/images/avatars/male-avatar-2.png' 48 | }; 49 | 50 | const tLadycap: Thread = { 51 | id: 'tLadycap', 52 | name: ladycap.name, 53 | avatarSrc: ladycap.avatarSrc, 54 | messages: [] 55 | }; 56 | 57 | const tEcho: Thread = { 58 | id: 'tEcho', 59 | name: echo.name, 60 | avatarSrc: echo.avatarSrc, 61 | messages: [] 62 | }; 63 | 64 | const tRev: Thread = { 65 | id: 'tRev', 66 | name: rev.name, 67 | avatarSrc: rev.avatarSrc, 68 | messages: [] 69 | }; 70 | 71 | const tWait: Thread = { 72 | id: 'tWait', 73 | name: wait.name, 74 | avatarSrc: wait.avatarSrc, 75 | messages: [] 76 | }; 77 | 78 | export function ChatExampleData(store: Redux.Store) { 79 | 80 | // set the current User 81 | store.dispatch(UserActions.setCurrentUser(me)); 82 | 83 | // create a new thread and add messages 84 | store.dispatch(ThreadActions.addThread(tLadycap)); 85 | store.dispatch(ThreadActions.addMessage(tLadycap, { 86 | author: me, 87 | sentAt: moment().subtract(45, 'minutes').toDate(), 88 | text: 'Yet let me weep for such a feeling loss.' 89 | })); 90 | store.dispatch(ThreadActions.addMessage(tLadycap, { 91 | author: ladycap, 92 | sentAt: moment().subtract(20, 'minutes').toDate(), 93 | text: 'So shall you feel the loss, but not the friend which you weep for.' 94 | })); 95 | 96 | // create a few more threads 97 | store.dispatch(ThreadActions.addThread(tEcho)); 98 | store.dispatch(ThreadActions.addMessage(tEcho, { 99 | author: echo, 100 | sentAt: moment().subtract(1, 'minutes').toDate(), 101 | text: 'I\'ll echo whatever you send me' 102 | })); 103 | 104 | store.dispatch(ThreadActions.addThread(tRev)); 105 | store.dispatch(ThreadActions.addMessage(tRev, { 106 | author: rev, 107 | sentAt: moment().subtract(3, 'minutes').toDate(), 108 | text: 'I\'ll reverse whatever you send me' 109 | })); 110 | 111 | store.dispatch(ThreadActions.addThread(tWait)); 112 | store.dispatch(ThreadActions.addMessage(tWait, { 113 | author: wait, 114 | sentAt: moment().subtract(4, 'minutes').toDate(), 115 | text: `I\'ll wait however many seconds you send to me before responding.` + 116 | ` Try sending '3'` 117 | })); 118 | 119 | // select the first thread 120 | store.dispatch(ThreadActions.selectThread(tLadycap)); 121 | 122 | // Now we set up the "bots". We do this by watching for new messages and 123 | // depending on which thread the message was sent to, the bot will respond 124 | // in kind. 125 | 126 | const handledMessages = {}; 127 | 128 | store.subscribe( () => { 129 | getAllMessages(store.getState()) 130 | // bots only respond to messages sent by the user, so 131 | // only keep messages sent by the current user 132 | .filter(message => message.author.id === me.id) 133 | .map(message => { 134 | 135 | // This is a bit of a hack and we're stretching the limits of a faux 136 | // chat app. Every time there is a new message, we only want to keep the 137 | // new ones. This is a case where some sort of queue would be a better 138 | // model 139 | if (handledMessages.hasOwnProperty(message.id)) { 140 | return; 141 | } 142 | handledMessages[message.id] = true; 143 | 144 | switch (message.thread.id) { 145 | case tEcho.id: 146 | // echo back the same message to the user 147 | store.dispatch(ThreadActions.addMessage(tEcho, { 148 | author: echo, 149 | text: message.text 150 | })); 151 | 152 | break; 153 | case tRev.id: 154 | // echo back the message reveresed to the user 155 | store.dispatch(ThreadActions.addMessage(tRev, { 156 | author: rev, 157 | text: message.text.split('').reverse().join('') 158 | })); 159 | 160 | break; 161 | case tWait.id: 162 | let waitTime: number = parseInt(message.text, 10); 163 | let reply: string; 164 | 165 | if (isNaN(waitTime)) { 166 | waitTime = 0; 167 | reply = `I didn\'t understand ${message}. Try sending me a number`; 168 | } else { 169 | reply = `I waited ${waitTime} seconds to send you this.`; 170 | } 171 | 172 | setTimeout( 173 | () => { 174 | store.dispatch(ThreadActions.addMessage(tWait, { 175 | author: wait, 176 | text: reply 177 | })); 178 | }, 179 | waitTime * 1000); 180 | 181 | break; 182 | default: 183 | break; 184 | } 185 | }); 186 | }); 187 | } 188 | -------------------------------------------------------------------------------- /src/app/message/message.model.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user/user.model'; 2 | import { Thread } from '../thread/thread.model'; 3 | 4 | /** 5 | * Message represents one message being sent in a Thread 6 | */ 7 | export interface Message { 8 | id?: string; 9 | sentAt?: Date; 10 | isRead?: boolean; 11 | thread?: Thread; 12 | author: User; 13 | text: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/pipes/from-now.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FromNowPipe } from './from-now.pipe'; 2 | 3 | xdescribe('FromNowPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FromNowPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/pipes/from-now.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as moment from 'moment'; 3 | 4 | /** 5 | * FromNowPipe let's us convert a date into a human-readable relative-time 6 | * such as "10 minutes ago". 7 | */ 8 | @Pipe({ 9 | name: 'fromNow' 10 | }) 11 | export class FromNowPipe implements PipeTransform { 12 | transform(value: any, args: Array): string { 13 | return moment(value).fromNow(); 14 | } 15 | } 16 | 17 | export const fromNowPipeInjectables: Array = [ 18 | FromNowPipe 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/thread/thread.actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016, Fullstack.io, LLC. 3 | * 4 | * This source code is licensed under the MIT-style license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { 10 | Action, 11 | ActionCreator 12 | } from 'redux'; 13 | import { uuid } from '../util/uuid'; 14 | import { Message } from '../message/message.model'; 15 | import { User } from '../user/user.model'; 16 | import { Thread } from '../thread/thread.model'; 17 | 18 | /** 19 | * ThreadActions specifies _action creators_ (i.e. objects that describe 20 | * changes to the reducers) that are concerned with Threads and Messages 21 | */ 22 | export const ADD_THREAD = '[Thread] Add'; 23 | export interface AddThreadAction extends Action { 24 | thread: Thread; 25 | } 26 | export const addThread: ActionCreator = 27 | (thread) => ({ 28 | type: ADD_THREAD, 29 | thread: thread 30 | }); 31 | 32 | export const ADD_MESSAGE = '[Thread] Add Message'; 33 | export interface AddMessageAction extends Action { 34 | thread: Thread; 35 | message: Message; 36 | } 37 | export const addMessage: ActionCreator = 38 | (thread: Thread, messageArgs: Message): AddMessageAction => { 39 | const defaults = { 40 | id: uuid(), 41 | sentAt: new Date(), 42 | isRead: false, 43 | thread: thread 44 | }; 45 | const message: Message = Object.assign({}, defaults, messageArgs); 46 | 47 | return { 48 | type: ADD_MESSAGE, 49 | thread: thread, 50 | message: message 51 | }; 52 | }; 53 | 54 | export const SELECT_THREAD = '[Thread] Select'; 55 | export interface SelectThreadAction extends Action { 56 | thread: Thread; 57 | } 58 | export const selectThread: ActionCreator = 59 | (thread) => ({ 60 | type: SELECT_THREAD, 61 | thread: thread 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/thread/thread.model.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../message/message.model'; 2 | 3 | /** 4 | * Thread represents a group of Users exchanging Messages 5 | */ 6 | export interface Thread { 7 | id: string; 8 | name: string; 9 | avatarSrc: string; 10 | messages: Message[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/thread/threads.reducer.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-switch-case-fall-through */ 2 | import { Action } from 'redux'; 3 | import { createSelector } from 'reselect'; 4 | 5 | import { Thread } from './thread.model'; 6 | import { Message } from '../message/message.model'; 7 | import * as ThreadActions from './thread.actions'; 8 | 9 | /** 10 | * This file describes the state concerning Threads, how to modify them through 11 | * the reducer, and how to query the state via selectors. 12 | * 13 | * ThreadsState stores the list of Threads indexed by id in `entities`, as well 14 | * as a complete list of the ids in `ids`. 15 | * 16 | * We also store the id of the current thread so that we know what the user is 17 | * currently looking at - this is valuable for the unread messages count, for 18 | * instance. 19 | * 20 | * In this app, we store the Messages in their respective Thread and we don't 21 | * store the Messages apart from that Thread. In your app you may find it useful 22 | * to separate Messages into their own Messages reducer and keep only a list 23 | * of Message ids in your Threads. 24 | */ 25 | export interface ThreadsEntities { 26 | [id: string]: Thread; 27 | } 28 | 29 | export interface ThreadsState { 30 | ids: string[]; 31 | entities: ThreadsEntities; 32 | currentThreadId?: string; 33 | }; 34 | 35 | const initialState: ThreadsState = { 36 | ids: [], 37 | currentThreadId: null, 38 | entities: {} 39 | }; 40 | 41 | /** 42 | * The `ThreadsReducer` describes how to modify the `ThreadsState` given a 43 | * particular action. 44 | */ 45 | export const ThreadsReducer = 46 | function(state: ThreadsState = initialState, action: Action): ThreadsState { 47 | switch (action.type) { 48 | 49 | // Adds a new Thread to the list of entities 50 | case ThreadActions.ADD_THREAD: { 51 | const thread = (action).thread; 52 | 53 | if (state.ids.includes(thread.id)) { 54 | return state; 55 | } 56 | 57 | return { 58 | ids: [ ...state.ids, thread.id ], 59 | currentThreadId: state.currentThreadId, 60 | entities: Object.assign({}, state.entities, { 61 | [thread.id]: thread 62 | }) 63 | }; 64 | } 65 | 66 | // Adds a new Message to a particular Thread 67 | case ThreadActions.ADD_MESSAGE: { 68 | const thread = (action).thread; 69 | const message = (action).message; 70 | 71 | // special case: if the message being added is in the current thread, then 72 | // mark it as read 73 | const isRead = message.thread.id === state.currentThreadId ? 74 | true : message.isRead; 75 | const newMessage = Object.assign({}, message, { isRead: isRead }); 76 | 77 | // grab the old thraed from entities 78 | const oldThread = state.entities[thread.id]; 79 | 80 | // create a new thread which has our newMessage 81 | const newThread = Object.assign({}, oldThread, { 82 | messages: [...oldThread.messages, newMessage] 83 | }); 84 | 85 | return { 86 | ids: state.ids, // unchanged 87 | currentThreadId: state.currentThreadId, // unchanged 88 | entities: Object.assign({}, state.entities, { 89 | [thread.id]: newThread 90 | }) 91 | }; 92 | } 93 | 94 | // Select a particular thread in the UI 95 | case ThreadActions.SELECT_THREAD: { 96 | const thread = (action).thread; 97 | const oldThread = state.entities[thread.id]; 98 | 99 | // mark the messages as read 100 | const newMessages = oldThread.messages.map( 101 | (message) => Object.assign({}, message, { isRead: true })); 102 | 103 | // give them to this new thread 104 | const newThread = Object.assign({}, oldThread, { 105 | messages: newMessages 106 | }); 107 | 108 | return { 109 | ids: state.ids, 110 | currentThreadId: thread.id, 111 | entities: Object.assign({}, state.entities, { 112 | [thread.id]: newThread 113 | }) 114 | }; 115 | } 116 | 117 | default: 118 | return state; 119 | } 120 | }; 121 | 122 | export const getThreadsState = (state): ThreadsState => state.threads; 123 | 124 | export const getThreadsEntities = createSelector( 125 | getThreadsState, 126 | ( state: ThreadsState ) => state.entities ); 127 | 128 | export const getAllThreads = createSelector( 129 | getThreadsEntities, 130 | ( entities: ThreadsEntities ) => Object.keys(entities) 131 | .map((threadId) => entities[threadId])); 132 | 133 | export const getUnreadMessagesCount = createSelector( 134 | getAllThreads, 135 | ( threads: Thread[] ) => threads.reduce( 136 | (unreadCount: number, thread: Thread) => { 137 | thread.messages.forEach((message: Message) => { 138 | if (!message.isRead) { 139 | ++unreadCount; 140 | } 141 | }); 142 | return unreadCount; 143 | }, 144 | 0)); 145 | 146 | // This selector emits the current thread 147 | export const getCurrentThread = createSelector( 148 | getThreadsEntities, 149 | getThreadsState, 150 | ( entities: ThreadsEntities, state: ThreadsState ) => 151 | entities[state.currentThreadId] ); 152 | 153 | export const getAllMessages = createSelector( 154 | getAllThreads, 155 | ( threads: Thread[] ) => 156 | threads.reduce( // gather all messages 157 | (messages, thread) => [...messages, ...thread.messages], 158 | []).sort((m1, m2) => m1.sentAt - m2.sentAt)); // sort them by time 159 | -------------------------------------------------------------------------------- /src/app/user/user.actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016, Fullstack.io, LLC. 3 | * 4 | * This source code is licensed under the MIT-style license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { 10 | Action, 11 | ActionCreator 12 | } from 'redux'; 13 | import { 14 | User 15 | } from './user.model'; 16 | 17 | /** 18 | * UserActions specifies action creators concerning Users 19 | */ 20 | export const SET_CURRENT_USER = '[User] Set Current'; 21 | export interface SetCurrentUserAction extends Action { 22 | user: User; 23 | } 24 | export const setCurrentUser: ActionCreator = 25 | (user) => ({ 26 | type: SET_CURRENT_USER, 27 | user: user 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/user/user.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A User represents an agent that sends messages 3 | */ 4 | export interface User { 5 | id: string; 6 | name: string; 7 | avatarSrc: string; 8 | isClient?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/user/users.reducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016, Fullstack.io, LLC. 3 | * 4 | * This source code is licensed under the MIT-style license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { Action } from 'redux'; 10 | import { User } from './user.model'; 11 | import * as UserActions from './user.actions'; 12 | import { createSelector } from 'reselect'; 13 | 14 | /** 15 | * This file describes the state concerning Users, how to modify it through 16 | * the reducer, and the selectors. 17 | */ 18 | export interface UsersState { 19 | currentUser: User; 20 | }; 21 | 22 | const initialState: UsersState = { 23 | currentUser: null 24 | }; 25 | 26 | export const UsersReducer = 27 | function(state: UsersState = initialState, action: Action): UsersState { 28 | switch (action.type) { 29 | case UserActions.SET_CURRENT_USER: 30 | const user: User = (action).user; 31 | return { 32 | currentUser: user 33 | }; 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export const getUsersState = (state): UsersState => state.users; 40 | 41 | export const getCurrentUser = createSelector( 42 | getUsersState, 43 | ( state: UsersState ) => state.currentUser ); 44 | -------------------------------------------------------------------------------- /src/app/util/uuid.ts: -------------------------------------------------------------------------------- 1 | /* jshint bitwise:false, node:true */ 2 | /* tslint:disable:no-bitwise no-var-keyword typedef */ 3 | 4 | // taken from TodoMVC 5 | export function uuid() { 6 | var i, random; 7 | var result = ''; 8 | 9 | for (i = 0; i < 32; i++) { 10 | random = Math.random() * 16 | 0; 11 | if (i === 8 || i === 12 || i === 16 || i === 20) { 12 | result += '-'; 13 | } 14 | result += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) 15 | .toString(16); 16 | } 17 | 18 | return result; 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/css/chat.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Google Hangouts-like bootstrap css originally from: 3 | http://bootsnipp.com/snippets/featured/like-hangout-chat 4 | by SrPatinhas 5 | 6 | as well as some css from "Chat": http://bootsnipp.com/snippets/featured/chat 7 | by hardiksondagar 8 | */ 9 | 10 | .conversation-wrap { 11 | box-shadow: -2px 0 3px #ddd; 12 | padding:0; 13 | @extend .col-lg-3; 14 | 15 | .conversation { 16 | position: relative; 17 | display: block; 18 | padding:5px; 19 | border-bottom:1px solid #ddd; 20 | margin:0; 21 | 22 | .avatar { 23 | width: 50px; 24 | height: 50px; 25 | } 26 | 27 | 28 | &:hover { 29 | cursor: hand; 30 | cursor: pointer; 31 | opacity: .8; 32 | } 33 | 34 | // http://ctrlq.org/code/19639-turn-div-clickable-link 35 | .div-link { 36 | position: absolute; 37 | width: 100%; 38 | height: 100%; 39 | top: 0; 40 | left: 0; 41 | text-decoration: none; 42 | /* Makes sure the link doesn't get underlined */ 43 | z-index: 10; 44 | /* raises anchor tag above everything else in div */ 45 | background-color: white; 46 | /*workaround to make clickable in IE */ 47 | opacity: 0; 48 | /*workaround to make clickable in IE */ 49 | filter: alpha(opacity=0); 50 | /*workaround to make clickable in IE */ 51 | } 52 | 53 | } 54 | 55 | &::-webkit-scrollbar { 56 | width: 6px; 57 | } 58 | 59 | &::-webkit-scrollbar-track { 60 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 61 | } 62 | 63 | &::-webkit-scrollbar-thumb { 64 | background:#ddd; 65 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); 66 | } 67 | &::-webkit-scrollbar-thumb:window-inactive { 68 | background: #ddd; 69 | } 70 | } 71 | 72 | .chat-window-container { 73 | @extend .row; 74 | } 75 | 76 | .chat-window { 77 | @extend .col-xs-10, .col-sm-5; 78 | 79 | bottom: 0; 80 | right: 0; 81 | position: fixed; 82 | float: right; 83 | margin-left: 10px; 84 | 85 | .panel-container { 86 | @extend .col-xs-12; 87 | @extend .col-md-12; 88 | } 89 | 90 | > div > .panel { 91 | border-radius: 5px 5px 0 0; 92 | } 93 | 94 | .panel-footer { 95 | .chat-input { 96 | @extend .input-sm; 97 | @extend .form-control; 98 | } 99 | 100 | .btn-chat { 101 | @extend .btn; 102 | @extend .btn-primary; 103 | @extend .btn-sm; 104 | } 105 | } 106 | 107 | .col-md-2, .col-md-10 { 108 | padding: 0; 109 | } 110 | 111 | .panel { 112 | margin-bottom: 0px; 113 | } 114 | 115 | .icon_minim { 116 | padding: 2px 10px; 117 | } 118 | 119 | .msg-container-base { 120 | background: #e5e5e5; 121 | margin: 0; 122 | padding: 0 10px 10px; 123 | max-height: 300px; 124 | overflow-x: hidden; 125 | } 126 | 127 | .top-bar { 128 | background: #666; 129 | color: white; 130 | padding: 10px; 131 | position: relative; 132 | overflow: hidden; 133 | 134 | .panel-title-container { 135 | @extend .col-md-10; 136 | @extend .col-xs-10; 137 | } 138 | .panel-buttons-container { 139 | @extend .col-md-2; 140 | @extend .col-xs-2; 141 | } 142 | } 143 | 144 | .msg-receive { 145 | padding-left: 0; 146 | margin-left: 0; 147 | } 148 | 149 | .msg-sent { 150 | padding-bottom: 20px !important; 151 | margin-right: 0; 152 | } 153 | 154 | .messages { 155 | @extend .col-md-10; 156 | @extend .col-xs-10; 157 | 158 | background: white; 159 | padding: 10px; 160 | border-radius: 2px; 161 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 162 | max-width: 100%; 163 | > { 164 | p { 165 | font-size: 13px; 166 | margin: 0 0 0.2rem 0; 167 | } 168 | .time { 169 | font-size: 11px; 170 | color: #ccc; 171 | } 172 | } 173 | } 174 | 175 | .msg-container { 176 | @extend .row; 177 | padding: 10px; 178 | overflow: hidden; 179 | display: flex; 180 | 181 | .avatar { 182 | @extend .col-md-2; 183 | @extend .col-xs-2; 184 | 185 | img { 186 | @extend .img-responsive; 187 | } 188 | } 189 | } 190 | 191 | img { 192 | display: block; 193 | width: 100%; 194 | } 195 | 196 | .avatar { 197 | position: relative; 198 | } 199 | 200 | .base-receive > .avatar:after { 201 | content: ""; 202 | position: absolute; 203 | top: 0; 204 | right: 0; 205 | width: 0; 206 | height: 0; 207 | border: 5px solid #FFF; 208 | border-left-color: rgba(0, 0, 0, 0); 209 | border-bottom-color: rgba(0, 0, 0, 0); 210 | } 211 | 212 | .base-sent { 213 | justify-content: flex-end; 214 | align-items: flex-end; 215 | > .avatar:after { 216 | content: ""; 217 | position: absolute; 218 | bottom: 0; 219 | left: 0; 220 | width: 0; 221 | height: 0; 222 | border: 5px solid white; 223 | border-right-color: transparent; 224 | border-top-color: transparent; 225 | box-shadow: 1px 1px 2px rgba(black, 0.2); 226 | } 227 | } 228 | 229 | .msg-sent > .time { 230 | float: right; 231 | } 232 | 233 | .msg-container-base { 234 | &::-webkit-scrollbar-track { 235 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 236 | background-color: #F5F5F5; 237 | } 238 | &::-webkit-scrollbar { 239 | width: 12px; 240 | background-color: #F5F5F5; 241 | } 242 | &::-webkit-scrollbar-thumb { 243 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 244 | background-color: #555; 245 | } 246 | } 247 | 248 | .btn-group.dropup { 249 | position: fixed; 250 | left: 0px; 251 | bottom: 0; 252 | } 253 | 254 | } 255 | 256 | .navbar-brand { 257 | img { 258 | float: left; 259 | top: -10px; 260 | position: relative; 261 | margin-right: 10px; 262 | } 263 | 264 | } 265 | 266 | .navbar-text { 267 | padding-right: 110px; 268 | } 269 | -------------------------------------------------------------------------------- /src/assets/css/styles.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; 2 | 3 | @import "~bootstrap-sass/assets/stylesheets/_bootstrap"; 4 | @import "chat"; 5 | -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/female-avatar-1.png -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/female-avatar-2.png -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/female-avatar-3.png -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/female-avatar-4.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/male-avatar-1.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/male-avatar-2.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/male-avatar-3.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/avatars/male-avatar-4.png -------------------------------------------------------------------------------- /src/assets/images/logos/Angular2ReduxChatHeaderImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/logos/Angular2ReduxChatHeaderImage.png -------------------------------------------------------------------------------- /src/assets/images/logos/ng-book-2-minibook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/logos/ng-book-2-minibook.png -------------------------------------------------------------------------------- /src/assets/images/readme/full-chat-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/full-chat-preview.png -------------------------------------------------------------------------------- /src/assets/images/readme/minimal-redux-ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/minimal-redux-ts.png -------------------------------------------------------------------------------- /src/assets/images/readme/ng-book-2-as-book-cover-pigment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/ng-book-2-as-book-cover-pigment.png -------------------------------------------------------------------------------- /src/assets/images/readme/redux-chat-echo-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/redux-chat-echo-bot.png -------------------------------------------------------------------------------- /src/assets/images/readme/redux-chat-initial-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/redux-chat-initial-state.png -------------------------------------------------------------------------------- /src/assets/images/readme/redux-chat-models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/redux-chat-models.png -------------------------------------------------------------------------------- /src/assets/images/readme/redux-chat-top-level-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/redux-chat-top-level-components.png -------------------------------------------------------------------------------- /src/assets/images/readme/working-counter-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/assets/images/readme/working-counter-app.png -------------------------------------------------------------------------------- /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/ng-book/angular2-redux-chat/54d78b2a0413c01dfd6d64d1eba19f52e9a0f8c8/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-book 2: AngularReduxChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 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 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "use-input-property-decorator": true, 103 | "use-output-property-decorator": true, 104 | "use-host-property-decorator": true, 105 | "no-input-rename": true, 106 | "no-output-rename": true, 107 | "use-life-cycle-interface": true, 108 | "use-pipe-transform-interface": true, 109 | "component-class-suffix": true, 110 | "directive-class-suffix": true, 111 | "no-access-missing-member": true, 112 | "templates-use-public": true, 113 | "invoke-injectable": true 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /tutorial/.test/bats.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "redux1.js works" { 4 | run bash -c "./node_modules/.bin/babel-node redux1.js" 5 | [ "$status" -eq 0 ] 6 | [ "${lines[0]}" = "true" ] 7 | } 8 | 9 | @test "redux2.js works" { 10 | run bash -c "./node_modules/.bin/babel-node redux2.js" 11 | EXPECTED="$(cat <<'EOF' 12 | 1 13 | 2 14 | 3 15 | true 16 | EOF 17 | )" 18 | [ "$status" -eq 0 ] 19 | [ "$output" = "$EXPECTED" ] 20 | } 21 | 22 | @test "identity-reducer.js works" { 23 | run bash -c "./node_modules/.bin/babel-node identity-reducer.js" 24 | EXPECTED="$(cat <<'EOF' 25 | 0 26 | EOF 27 | )" 28 | [ "$status" -eq 0 ] 29 | [ "$output" = "$EXPECTED" ] 30 | } 31 | 32 | @test "adjusting-reducer.js works" { 33 | run bash -c "./node_modules/.bin/babel-node adjusting-reducer.js" 34 | EXPECTED="$(cat <<'EOF' 35 | 1 36 | 2 37 | 99 38 | EOF 39 | )" 40 | [ "$status" -eq 0 ] 41 | [ "$output" = "$EXPECTED" ] 42 | } 43 | 44 | @test "adjusting-reducer-switch.js works" { 45 | run bash -c "./node_modules/.bin/babel-node adjusting-reducer-switch.js" 46 | EXPECTED="$(cat <<'EOF' 47 | 1 48 | 2 49 | 99 50 | 100 51 | EOF 52 | )" 53 | [ "$status" -eq 0 ] 54 | [ "$output" = "$EXPECTED" ] 55 | } 56 | 57 | @test "plus-action.js works" { 58 | run bash -c "./node_modules/.bin/babel-node plus-action.js" 59 | EXPECTED="$(cat <<'EOF' 60 | 10 61 | 9003 62 | 1 63 | EOF 64 | )" 65 | [ "$status" -eq 0 ] 66 | [ "$output" = "$EXPECTED" ] 67 | } 68 | 69 | @test "action-arguments works" { 70 | run bash -c "./node_modules/.bin/babel-node action-arguments.js" 71 | EXPECTED="$(cat <<'EOF' 72 | 1 73 | 2 74 | 99 75 | EOF 76 | )" 77 | [ "$status" -eq 0 ] 78 | [ "$output" = "$EXPECTED" ] 79 | } 80 | 81 | @test "minimal-store works" { 82 | run bash -c "./node_modules/.bin/babel-node minimal-store.js" 83 | EXPECTED="$(cat <<'EOF' 84 | null 85 | 1 86 | 2 87 | 1 88 | EOF 89 | )" 90 | [ "$status" -eq 0 ] 91 | [ "$output" = "$EXPECTED" ] 92 | } 93 | 94 | -------------------------------------------------------------------------------- /tutorial/01-identity-reducer.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | let reducer: Reducer = (state: number, action: Action) => { 11 | return state; 12 | }; 13 | 14 | console.log( reducer(0, null) ); // -> 0 15 | -------------------------------------------------------------------------------- /tutorial/02-adjusting-reducer.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | let reducer: Reducer = (state: number, action: Action) => { 11 | if (action.type === 'INCREMENT') { 12 | return state + 1; 13 | } 14 | if (action.type === 'DECREMENT') { 15 | return state - 1; 16 | } 17 | return state; 18 | }; 19 | 20 | let incrementAction: Action = { type: 'INCREMENT' }; 21 | 22 | console.log( reducer(0, incrementAction )); // -> 1 23 | console.log( reducer(1, incrementAction )); // -> 2 24 | 25 | let decrementAction: Action = { type: 'DECREMENT' }; 26 | 27 | console.log( reducer(100, decrementAction )); // -> 99 28 | -------------------------------------------------------------------------------- /tutorial/03-adjusting-reducer-switch.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | let reducer: Reducer = (state: number, action: Action) => { 11 | switch (action.type) { 12 | case 'INCREMENT': 13 | return state + 1; 14 | case 'DECREMENT': 15 | return state - 1; 16 | default: 17 | return state; // <-- dont forget! 18 | } 19 | }; 20 | 21 | let incrementAction: Action = { type: 'INCREMENT' }; 22 | console.log(reducer(0, incrementAction)); // -> 1 23 | console.log(reducer(1, incrementAction)); // -> 2 24 | 25 | let decrementAction: Action = { type: 'DECREMENT' }; 26 | console.log(reducer(100, decrementAction)); // -> 99 27 | 28 | // any other action just returns the input state 29 | let unknownAction: Action = { type: 'UNKNOWN' }; 30 | console.log(reducer(100, unknownAction)); // -> 100 31 | 32 | -------------------------------------------------------------------------------- /tutorial/04-plus-action.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | let reducer: Reducer = (state: number, action: Action) => { 11 | switch (action.type) { 12 | case 'INCREMENT': 13 | return state + 1; 14 | case 'DECREMENT': 15 | return state - 1; 16 | case 'PLUS': 17 | return state + action.payload; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | console.log( reducer(3, { type: 'PLUS', payload: 7}) ); // -> 10 24 | console.log( reducer(3, { type: 'PLUS', payload: 9000}) ); // -> 9003 25 | console.log( reducer(3, { type: 'PLUS', payload: -2}) ); // -> 1 26 | -------------------------------------------------------------------------------- /tutorial/05-minimal-store.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | class Store { 11 | private _state: T; 12 | 13 | constructor( 14 | private reducer: Reducer, 15 | initialState: T 16 | ) { 17 | this._state = initialState; 18 | } 19 | 20 | getState(): T { 21 | return this._state; 22 | } 23 | 24 | dispatch(action: Action): void { 25 | this._state = this.reducer(this._state, action); 26 | } 27 | } 28 | 29 | // same reducer as before 30 | let reducer: Reducer = (state: number, action: Action) => { 31 | switch (action.type) { 32 | case 'INCREMENT': 33 | return state + 1; 34 | case 'DECREMENT': 35 | return state - 1; 36 | case 'PLUS': 37 | return state + action.payload; 38 | default: 39 | return state; 40 | } 41 | }; 42 | 43 | // create a new store 44 | let store = new Store(reducer, 0); 45 | console.log(store.getState()); // -> 0 46 | 47 | store.dispatch({ type: 'INCREMENT' }); 48 | console.log(store.getState()); // -> 1 49 | 50 | store.dispatch({ type: 'INCREMENT' }); 51 | console.log(store.getState()); // -> 2 52 | 53 | store.dispatch({ type: 'DECREMENT' }); 54 | console.log(store.getState()); // -> 1 55 | -------------------------------------------------------------------------------- /tutorial/06-rx-store.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 3 | import { Subject } from 'rxjs/Subject'; 4 | import 'rxjs/add/operator/scan'; 5 | 6 | interface Action { 7 | type: string; 8 | payload?: any; 9 | } 10 | 11 | interface Reducer { 12 | (state: T, action: Action): T; 13 | } 14 | 15 | class Store extends BehaviorSubject { 16 | private _dispatcher: Subject; 17 | 18 | constructor( 19 | private _reducer: Reducer, 20 | initialState: T 21 | ) { 22 | super(initialState); 23 | 24 | this._dispatcher = new Subject(); 25 | this._dispatcher 26 | .scan( 27 | (state: T, action: Action) => this._reducer(state, action), 28 | initialState) 29 | .subscribe((state) => super.emit(state)); 30 | } 31 | 32 | getState(): T { 33 | return this.value; 34 | } 35 | 36 | dispatch(action: Action): void { 37 | this._dispatcher.emit(action); 38 | } 39 | } 40 | 41 | // same reducer as before (!) 42 | let reducer: Reducer = (state: number, action: Action) => { 43 | switch (action.type) { 44 | case 'INCREMENT': 45 | return state + 1; 46 | case 'DECREMENT': 47 | return state - 1; 48 | case 'PLUS': 49 | return state + action.payload; 50 | default: 51 | return state; 52 | } 53 | }; 54 | 55 | // create a new store 56 | console.log('-- store --'); 57 | let store = new Store(reducer, 0); 58 | console.log(store.getState()); // -> 0 59 | 60 | store.dispatch({ type: 'INCREMENT' }); 61 | console.log(store.getState()); // -> 1 62 | 63 | store.dispatch({ type: 'INCREMENT' }); 64 | console.log(store.getState()); // -> 2 65 | 66 | store.dispatch({ type: 'DECREMENT' }); 67 | console.log(store.getState()); // -> 1 68 | 69 | // observing! 70 | console.log('-- store2 --'); 71 | let store2 = new Store(reducer, 0); 72 | store2.subscribe((newState => console.log("state: ", newState))); // -> state: 0 73 | store2.dispatch({ type: 'INCREMENT' }); // -> state: 1 74 | store2.dispatch({ type: 'INCREMENT' }); // -> state: 2 75 | store2.dispatch({ type: 'DECREMENT' }); // -> state: 1 76 | -------------------------------------------------------------------------------- /tutorial/06-store-w-subscribe.ts: -------------------------------------------------------------------------------- 1 | interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | interface ListenerCallback { 11 | (): void; 12 | } 13 | 14 | interface UnsubscribeCallback { 15 | (): void; 16 | } 17 | 18 | class Store { 19 | private _state: T; 20 | private _listeners: ListenerCallback[] = []; 21 | 22 | constructor( 23 | private reducer: Reducer, 24 | initialState: T 25 | ) { 26 | this._state = initialState; 27 | } 28 | 29 | getState(): T { 30 | return this._state; 31 | } 32 | 33 | dispatch(action: Action): void { 34 | this._state = this.reducer(this._state, action); 35 | this._listeners.forEach((listener: ListenerCallback) => listener()); 36 | } 37 | 38 | subscribe(listener: ListenerCallback): UnsubscribeCallback { 39 | this._listeners.push(listener); 40 | return () => { // returns an "unsubscribe" function 41 | this._listeners = this._listeners.filter(l => l !== listener); 42 | }; 43 | } 44 | } 45 | 46 | // same reducer as before 47 | let reducer: Reducer = (state: number, action: Action) => { 48 | switch (action.type) { 49 | case 'INCREMENT': 50 | return state + 1; 51 | case 'DECREMENT': 52 | return state - 1; 53 | case 'PLUS': 54 | return state + action.payload; 55 | default: 56 | return state; 57 | } 58 | }; 59 | 60 | // create a new store 61 | let store = new Store(reducer, 0); 62 | console.log(store.getState()); // -> 0 63 | 64 | // subscribe 65 | let unsubscribe = store.subscribe(() => { 66 | console.log('subscribed: ', store.getState()); 67 | }); 68 | 69 | store.dispatch({ type: 'INCREMENT' }); // -> subscribed: 1 70 | store.dispatch({ type: 'INCREMENT' }); // -> subscribed: 2 71 | 72 | unsubscribe(); 73 | store.dispatch({ type: 'DECREMENT' }); // (nothing logged) 74 | 75 | // decrement happened, even though we weren't listening for it 76 | console.log(store.getState()); // -> 1 77 | -------------------------------------------------------------------------------- /tutorial/06b-rx-store.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 3 | import { Subject } from 'rxjs/Subject'; 4 | import 'rxjs/add/operator/scan'; 5 | 6 | interface Action { 7 | type: string; 8 | payload?: any; 9 | } 10 | 11 | interface Reducer { 12 | (state: T, action: Action): T; 13 | } 14 | 15 | class Store extends BehaviorSubject { 16 | private _dispatcher: Subject; 17 | 18 | constructor( 19 | private _reducer: Reducer, 20 | initialState: T 21 | ) { 22 | super(initialState); 23 | 24 | this._dispatcher = new Subject(); 25 | this._dispatcher 26 | .scan( 27 | (state: T, action: Action) => this._reducer(state, action), 28 | initialState) 29 | .subscribe((state) => super.emit(state)); 30 | } 31 | 32 | getState(): T { 33 | return this.value; 34 | } 35 | 36 | dispatch(action: Action): void { 37 | this._dispatcher.emit(action); 38 | } 39 | } 40 | 41 | // same reducer as before (!) 42 | let reducer: Reducer = (state: number, action: Action) => { 43 | switch (action.type) { 44 | case 'INCREMENT': 45 | return state + 1; 46 | case 'DECREMENT': 47 | return state - 1; 48 | case 'PLUS': 49 | return state + action.payload; 50 | default: 51 | return state; 52 | } 53 | }; 54 | 55 | // create a new store 56 | console.log('-- store --'); 57 | let store = new Store(reducer, 0); 58 | console.log(store.getState()); // -> 0 59 | 60 | store.dispatch({ type: 'INCREMENT' }); 61 | console.log(store.getState()); // -> 1 62 | 63 | store.dispatch({ type: 'INCREMENT' }); 64 | console.log(store.getState()); // -> 2 65 | 66 | store.dispatch({ type: 'DECREMENT' }); 67 | console.log(store.getState()); // -> 1 68 | 69 | // observing! 70 | console.log('-- store2 --'); 71 | let store2 = new Store(reducer, 0); 72 | store2.subscribe((newState => console.log("state: ", newState))); // -> state: 0 73 | store2.dispatch({ type: 'INCREMENT' }); // -> state: 1 74 | store2.dispatch({ type: 'INCREMENT' }); // -> state: 2 75 | store2.dispatch({ type: 'DECREMENT' }); // -> state: 1 76 | -------------------------------------------------------------------------------- /tutorial/07-messages-reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Reducer, 4 | Store 5 | } from './lib/miniRedux'; 6 | 7 | interface AppState { 8 | messages: string[]; 9 | } 10 | 11 | interface AddMessageAction extends Action { 12 | message: string; 13 | } 14 | 15 | interface DeleteMessageAction extends Action { 16 | index: number; 17 | } 18 | 19 | let reducer: Reducer = 20 | (state: AppState, action: Action): AppState => { 21 | switch (action.type) { 22 | case 'ADD_MESSAGE': 23 | return { 24 | messages: state.messages.concat( 25 | (action).message 26 | ), 27 | }; 28 | case 'DELETE_MESSAGE': 29 | let idx = (action).index; 30 | return { 31 | messages: [ 32 | ...state.messages.slice(0, idx), 33 | ...state.messages.slice(idx + 1, state.messages.length) 34 | ] 35 | }; 36 | default: 37 | return state; 38 | } 39 | }; 40 | 41 | // create a new store 42 | let store = new Store(reducer, { messages: [] }); 43 | console.log(store.getState()); // -> { messages: [] } 44 | 45 | store.dispatch({ 46 | type: 'ADD_MESSAGE', 47 | message: 'Would you say the fringe was made of silk?' 48 | } as AddMessageAction); 49 | 50 | store.dispatch({ 51 | type: 'ADD_MESSAGE', 52 | message: 'Wouldnt have no other kind but silk' 53 | } as AddMessageAction); 54 | 55 | store.dispatch({ 56 | type: 'ADD_MESSAGE', 57 | message: 'Has it really got a team of snow white horses?' 58 | } as AddMessageAction); 59 | 60 | console.log(store.getState()); 61 | // -> 62 | // { messages: 63 | // [ 'Would you say the fringe was made of silk?', 64 | // 'Wouldnt have no other kind but silk', 65 | // 'Has it really got a team of snow white horses?' ] } 66 | 67 | store.dispatch({ 68 | type: 'DELETE_MESSAGE', 69 | index: 1 70 | } as DeleteMessageAction); 71 | 72 | console.log(store.getState()); 73 | // -> 74 | // { messages: 75 | // [ 'Would you say the fringe was made of silk?', 76 | // 'Has it really got a team of snow white horses?' ] } 77 | 78 | store.dispatch({ 79 | type: 'DELETE_MESSAGE', 80 | index: 0 81 | } as DeleteMessageAction); 82 | 83 | console.log(store.getState()); 84 | // -> 85 | // { messages: [ 'Has it really got a team of snow white horses?' ] } 86 | -------------------------------------------------------------------------------- /tutorial/08-action-creators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Reducer, 4 | Store 5 | } from './lib/miniRedux'; 6 | 7 | interface AppState { 8 | messages: string[]; 9 | } 10 | 11 | interface AddMessageAction extends Action { 12 | message: string; 13 | } 14 | 15 | interface DeleteMessageAction extends Action { 16 | index: number; 17 | } 18 | 19 | class MessageActions { 20 | static addMessage(message: string): AddMessageAction { 21 | return { 22 | type: 'ADD_MESSAGE', 23 | message: message 24 | }; 25 | } 26 | static deleteMessage(index: number): DeleteMessageAction { 27 | return { 28 | type: 'DELETE_MESSAGE', 29 | index: index 30 | }; 31 | } 32 | } 33 | 34 | let reducer: Reducer = 35 | (state: AppState, action: Action) => { 36 | switch (action.type) { 37 | case 'ADD_MESSAGE': 38 | return { 39 | messages: state.messages.concat((action).message), 40 | }; 41 | case 'DELETE_MESSAGE': 42 | let idx = (action).index; 43 | return { 44 | messages: [ 45 | ...state.messages.slice(0, idx), 46 | ...state.messages.slice(idx + 1, state.messages.length) 47 | ] 48 | }; 49 | default: 50 | return state; 51 | } 52 | }; 53 | 54 | // create a new store 55 | let store = new Store(reducer, { messages: [] }); 56 | console.log(store.getState()); // -> { messages: [] } 57 | 58 | store.dispatch( 59 | MessageActions.addMessage('Would you say the fringe was made of silk?')); 60 | 61 | store.dispatch( 62 | MessageActions.addMessage('Wouldnt have no other kind but silk')); 63 | 64 | store.dispatch( 65 | MessageActions.addMessage('Has it really got a team of snow white horses?')); 66 | 67 | console.log(store.getState()); 68 | // -> 69 | // { messages: 70 | // [ 'Would you say the fringe was made of silk?', 71 | // 'Wouldnt have no other kind but silk', 72 | // 'Has it really got a team of snow white horses?' ] } 73 | 74 | store.dispatch( MessageActions.deleteMessage(1) ); 75 | 76 | console.log(store.getState()); 77 | // -> 78 | // { messages: 79 | // [ 'Would you say the fringe was made of silk?', 80 | // 'Has it really got a team of snow white horses?' ] } 81 | 82 | store.dispatch( MessageActions.deleteMessage(0) ); 83 | 84 | console.log(store.getState()); 85 | // -> 86 | // { messages: [ 'Has it really got a team of snow white horses?' ] } 87 | -------------------------------------------------------------------------------- /tutorial/09-real-redux.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Reducer, 4 | Store, 5 | createStore 6 | } from 'redux'; 7 | 8 | interface AppState { 9 | messages: string[]; 10 | } 11 | 12 | interface AddMessageAction extends Action { 13 | message: string; 14 | } 15 | 16 | interface DeleteMessageAction extends Action { 17 | index: number; 18 | } 19 | 20 | class MessageActions { 21 | static addMessage(message: string): AddMessageAction { 22 | return { 23 | type: 'ADD_MESSAGE', 24 | message: message 25 | }; 26 | } 27 | static deleteMessage(index: number): DeleteMessageAction { 28 | return { 29 | type: 'DELETE_MESSAGE', 30 | index: index 31 | }; 32 | } 33 | } 34 | 35 | let initialState: AppState = { messages: [] }; 36 | 37 | let reducer: Reducer = 38 | (state: AppState = initialState, action: Action) => { 39 | switch (action.type) { 40 | case 'ADD_MESSAGE': 41 | return { 42 | messages: state.messages.concat((action).message), 43 | }; 44 | case 'DELETE_MESSAGE': 45 | let idx = (action).index; 46 | return { 47 | messages: [ 48 | ...state.messages.slice(0, idx), 49 | ...state.messages.slice(idx + 1, state.messages.length) 50 | ] 51 | }; 52 | default: 53 | return state; 54 | } 55 | }; 56 | 57 | // create a new store 58 | let store: Store = createStore(reducer); 59 | console.log(store.getState()); // -> { messages: [] } 60 | 61 | store.dispatch( 62 | MessageActions.addMessage('Would you say the fringe was made of silk?')); 63 | 64 | store.dispatch( 65 | MessageActions.addMessage('Wouldnt have no other kind but silk')); 66 | 67 | store.dispatch( 68 | MessageActions.addMessage('Has it really got a team of snow white horses?')); 69 | 70 | console.log(store.getState()); 71 | // -> 72 | // { messages: 73 | // [ 'Would you say the fringe was made of silk?', 74 | // 'Wouldnt have no other kind but silk', 75 | // 'Has it really got a team of snow white horses?' ] } 76 | 77 | store.dispatch( MessageActions.deleteMessage(1) ); 78 | 79 | console.log(store.getState()); 80 | // -> 81 | // { messages: 82 | // [ 'Would you say the fringe was made of silk?', 83 | // 'Has it really got a team of snow white horses?' ] } 84 | 85 | store.dispatch( MessageActions.deleteMessage(0) ); 86 | 87 | console.log(store.getState()); 88 | // -> 89 | // { messages: [ 'Has it really got a team of snow white horses?' ] } 90 | -------------------------------------------------------------------------------- /tutorial/lib/miniRedux.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | type: string; 3 | payload?: any; 4 | } 5 | 6 | export interface Reducer { 7 | (state: T, action: Action): T; 8 | } 9 | 10 | export interface ListenerCallback { 11 | (): void; 12 | } 13 | 14 | export interface UnsubscribeCallback { 15 | (): void; 16 | } 17 | 18 | export class Store { 19 | private _state: T; 20 | private _listeners: ListenerCallback[] = []; 21 | 22 | constructor( 23 | private reducer: Reducer, 24 | initialState: T 25 | ) { 26 | this._state = initialState; 27 | } 28 | 29 | getState(): T { 30 | return this._state; 31 | } 32 | 33 | dispatch(action: Action): void { 34 | this._state = this.reducer(this._state, action); 35 | this._listeners.forEach((listener: ListenerCallback) => listener()); 36 | } 37 | 38 | subscribe(listener: ListenerCallback): UnsubscribeCallback { 39 | this._listeners.push(listener); 40 | return () => { // returns an "unsubscribe" function 41 | this._listeners = this._listeners.filter(l => l !== listener); 42 | }; 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /tutorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-redux-tutorial", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "author": "Nate Murray ", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "ts-node": "2.1.0" 10 | }, 11 | "dependencies": { 12 | "redux": "3.5.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tutorial/redux2.ts: -------------------------------------------------------------------------------- 1 | const createStore = (reducer, initialState) => { 2 | let state = initialState; 3 | let listeners = []; 4 | 5 | const getState = () => state; 6 | 7 | const dispatch = (action) => { 8 | state = reducer(state, action); 9 | listeners.forEach(listener => listener()); 10 | }; 11 | 12 | const subscribe = (listener) => { 13 | listeners.push(listener); 14 | return () => { 15 | listeners = listeners.filter(l => l !== listener); 16 | } 17 | }; 18 | 19 | dispatch(state); // dummy dispatch 20 | 21 | return { getState, dispatch, subscribe }; 22 | }; 23 | 24 | let reducer = (state, action) => { 25 | switch (action.type) { 26 | case 'INCREMENT': 27 | return state + 1; 28 | case 'DECREMENT': 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | let store = createStore(reducer, 0); 35 | store.subscribe(() => console.log(store.getState())); 36 | store.dispatch({type: 'INCREMENT'}); 37 | store.dispatch({type: 'INCREMENT'}); 38 | store.dispatch({type: 'INCREMENT'}); 39 | 40 | console.log(store.getState() == 3); 41 | -------------------------------------------------------------------------------- /tutorial/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "noEmitHelpers": true, 9 | "sourceMap": true 10 | }, 11 | "compileOnSave": false, 12 | "buildOnSave": false 13 | } 14 | --------------------------------------------------------------------------------