├── .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 |
3 |
4 |
5 | # Angular 2 Redux Chat [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
16 | Increment
17 |
18 |
20 | Decrement
21 |
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 |
2 |
3 |
9 |
10 |
11 | Messages {{ unreadMessagesCount }}
12 |
13 |
14 |
15 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------