├── .gitignore ├── README.md ├── async ├── package.json ├── spec-bundle.js ├── src │ ├── app │ │ ├── async-app.ts │ │ ├── bootstrap.ts │ │ ├── components │ │ │ ├── reddit-list.ts │ │ │ ├── reddit-select.ts │ │ │ └── refresh-button.ts │ │ ├── effects │ │ │ └── reddit-effects.ts │ │ ├── reducers │ │ │ ├── reddit.spec.ts │ │ │ └── reddit.ts │ │ └── services │ │ │ ├── reddit-model.ts │ │ │ └── reddit.ts │ ├── index.html │ ├── polyfills.ts │ ├── styles │ │ └── styles.css │ └── vendor.ts ├── tsconfig.json ├── typings.json ├── wallaby.js └── webpack.config.js ├── counter ├── package.json ├── spec-bundle.js ├── src │ ├── app │ │ ├── app.ts │ │ ├── bootstrap.ts │ │ ├── components │ │ │ └── counter.ts │ │ └── reducers │ │ │ ├── counter.spec.ts │ │ │ └── counter.ts │ ├── index.html │ ├── polyfills.ts │ ├── styles │ │ └── styles.css │ └── vendor.ts ├── tsconfig.json ├── typings.json ├── wallaby.js └── webpack.config.js ├── finances ├── README.md ├── angular-cli.json ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── common │ │ │ ├── operation.model.ts │ │ │ └── operations.ts │ │ ├── index.ts │ │ ├── new-operation.component.ts │ │ ├── new-operation.template.html │ │ ├── operations-list.component.ts │ │ └── operations-list.template.html │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.json │ └── typings.d.ts └── tslint.json ├── shopping-cart ├── helpers.js ├── karma.conf.js ├── package.json ├── spec-bundle.js ├── src │ ├── api │ │ ├── productsJSON.ts │ │ └── shop.ts │ ├── app │ │ ├── actions │ │ │ ├── cart.ts │ │ │ └── products.ts │ │ ├── bootstrap.ts │ │ ├── components │ │ │ ├── cart-item.ts │ │ │ ├── cart-list.ts │ │ │ ├── product-item.ts │ │ │ └── product-list.ts │ │ ├── effects │ │ │ ├── index.ts │ │ │ ├── shop.spec.ts │ │ │ └── shop.ts │ │ ├── reducers │ │ │ ├── cart.spec.ts │ │ │ ├── cart.ts │ │ │ ├── index.ts │ │ │ ├── products.spec.ts │ │ │ └── products.ts │ │ └── shoppingCart-app.ts │ ├── index.html │ ├── polyfills.ts │ ├── styles │ │ └── styles.css │ ├── test_harness.ts │ └── vendor.ts ├── tsconfig.json ├── tslint.json ├── typings.json ├── wallaby.js ├── webpack.config.js ├── webpack.default.config.js └── webpack.test.config.js ├── todos-undo-redo ├── package.json ├── spec-bundle.js ├── src │ ├── app │ │ ├── bootstrap.ts │ │ ├── common │ │ │ ├── actions.ts │ │ │ └── interfaces.ts │ │ ├── components │ │ │ ├── filter-select.ts │ │ │ ├── todo-input.ts │ │ │ └── todo-list.ts │ │ ├── reducers │ │ │ ├── reducers.ts │ │ │ ├── todos.spec.ts │ │ │ ├── todos.ts │ │ │ ├── undoable.ts │ │ │ └── visibility-filter.ts │ │ └── todo-app.ts │ ├── index.html │ ├── polyfills.ts │ ├── styles │ │ └── styles.css │ └── vendor.ts ├── tsconfig.json ├── typings.json ├── wallaby.js └── webpack.config.js └── todos ├── package.json ├── spec-bundle.js ├── src ├── app │ ├── bootstrap.ts │ ├── common │ │ ├── actions.ts │ │ └── interfaces.ts │ ├── components │ │ ├── filter-select.ts │ │ ├── todo-input.ts │ │ └── todo-list.ts │ ├── reducers │ │ ├── reducers.ts │ │ ├── todos.spec.ts │ │ ├── todos.ts │ │ └── visibility-filter.ts │ └── todo-app.ts ├── index.html ├── polyfills.ts ├── styles │ └── styles.css └── vendor.ts ├── tsconfig.json ├── typings.json ├── wallaby.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /*/node_modules 3 | npm-debug.log 4 | .DS_Store 5 | /*/typings 6 | /*/coverage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ngrx examples 2 | 3 | [Angular 2](https://angular.io/) + [ngrx](https://github.com/ngrx) examples, inspired by official [redux examples](https://github.com/reactjs/redux/tree/master/examples). 4 | 5 | ## Goal 6 | 7 | These examples illustrate how to utilize ngrx within an Angular 2 application. This repository will be actively maintained and updated as new tools and functionality become available and best practices are established. 8 | 9 | ## Contributions 10 | 11 | As Angular 2 and ngrx are relatively new, patterns and best practices are still being established. Examples found in this repository demonstrate how I (or the project author) would structure the solution but discussion and refinement is always encouraged! Please open an issue, submit a pull request, or drop me a message on [twitter](https://twitter.com/btroncone) to present a different approach or idea. This repository will feature the most solid, agreed upon techniques as they evolve. 12 | 13 | Please add any additional Angular 2, ngrx, or reactive programming articles, repositories, or code samples you find useful. I will keep this list as up-to-date as possible! 14 | 15 | 16 | ## Additional Resources 17 | Additional Angular 2, ngrx, and reactive programming articles, repositories, and code samples: 18 | 19 | ### Introduction 20 | * [Comprehensive Introduction to ngrx/store](https://gist.github.com/btroncone/a6e4347326749f938510) - Brian Troncone 21 | * [Reactive Angular 2 with ngrx](https://www.youtube.com/watch?v=mhA7zZ23Odw) - Rob Wormald 22 | * [@ngrx/store in 10 minutes - egghead.io](https://egghead.io/lessons/angular-2-ngrx-store-in-10-minutes) - Brian Troncone 23 | 24 | ### Articles 25 | * [Build a Better Angular 2 Application with Redux and ngrx](http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/) - Lukas Ruebbelke 26 | * [Reactive Data Flow in Angular 2](http://blog.lambda-it.ch/reactive-data-flow-in-angular-2/) - Wayne Maurer 27 | * [Understand and Utilize the Async Pipe in Angular 2](http://briantroncone.com/?p=623) - Brian Troncone 28 | * [Communication Between Components & Components Design](http://orizens.com/wp/topics/angular-2-communication-between-components-components-design/) - Oren Farhi 29 | 30 | ### Presentations and Slides 31 | * [Reactive Angular 2 - NG-NL 2016](https://www.youtube.com/watch?v=xAEFTSMEgIQ) - Rob Wormald 32 | * [Introduction to RxJS 5 - NG-NL 2016](http://slides.com/gerardsans/ng-nl-rxjs5) - Gerard Sans 33 | * [Angular 2 Change Detection Explained](http://pascalprecht.github.io/slides/angular-2-change-detection-explained/#/) - Pascal Precht 34 | * [Angular 2 and the Single Immutable State Tree](https://speakerdeck.com/cironunes/angular-2-and-the-single-immutable-state-tree) - Ciro Nunes 35 | 36 | ### Videos and Lessons 37 | * [Build Redux Style Applications with Angular2, RxJS, and ngrx/store](https://egghead.io/series/building-a-time-machine-with-angular-2-and-rxjs) - John Linquist 38 | * [Step-by-Step Async JavaScript with RxJS](https://egghead.io/series/step-by-step-async-javascript-with-rxjs) - John Lindquist 39 | * [RxJS Lessons from Ben Lesh](https://egghead.io/instructors/ben-lesh) - Ben Lesh 40 | * [RxJS Beyond The Basics - Creating Observables From Scratch](https://egghead.io/series/rxjs-beyond-the-basics-creating-observables-from-scratch) - André Staltz 41 | * [Introduction to Reactive Programming](https://egghead.io/series/introduction-to-reactive-programming) - André Staltz 42 | * [Getting Started With Redux](https://egghead.io/series/getting-started-with-redux) - Dan Abramov 43 | * [Asynchronous Programming - The End of the Loop](https://egghead.io/series/mastering-asynchronous-programming-the-end-of-the-loop) - Jafar Husain 44 | 45 | ### Repositories and Code Samples 46 | * [Official ngrx example application](https://github.com/ngrx/example-app) - Mike Ryan 47 | * [Echo Media Player - Media Player for YouTube](http://github.com/orizens/echoes-ng2) - Oren Fahri 48 | * [Staffer ngrx/store example](https://github.com/sapientglobalmarkets/staffer/tree/master/staffer-ng2-ngrxstore) -Pavan Podila 49 | * [Angular 2 Time Machine](https://gist.run/?id=da0af799da468b7ca70e) - John Lindquist 50 | * [NgRx Example](https://github.com/fxck/ngrx-example) - Aleš 51 | * [Todo with Undo/Redo](http://plnkr.co/edit/UnU1wnFcausVFfEP2RGD?p=preview) - Rob Wormald 52 | * [Instagram Filters - Instagram-like photo filter playground](https://github.com/JayKan/angular2-instagram) - Jay Kan 53 | 54 | ### Utilities 55 | * [ngrx-store-localstorage - Sync local storage with ngrx state slices](https://github.com/btroncone/ngrx-store-localstorage) - Brian Troncone 56 | * [ngrx-store-logger - Advanced action/state logging for @ngrx/store applications](https://github.com/btroncone/ngrx-store-logger) - Brian Troncone 57 | * [ngrx-store-freeze - Prevent state from being mutated in @ngrx/store](https://github.com/codewareio/ngrx-store-freeze) - Attila Egyed 58 | 59 | 60 | ## Getting Started 61 | ```bash 62 | # clone the repo 63 | git clone https://github.com/btroncone/ngrx-examples.git 64 | 65 | # cd into repo 66 | cd ngrx-examples 67 | 68 | # cd into project of your choice 69 | cd counter 70 | 71 | # install dependencies 72 | npm install 73 | 74 | # start the server 75 | npm start 76 | ``` 77 | 78 | ## Build 79 | 80 | Project builds are a stripped down version of [Angular Class Webpack Starter](https://github.com/AngularClass/angular2-webpack-starter), an exceptional Angular 2 seed project. Tests can be executed with either [WallabyJS](http://wallabyjs.com/) or Karma (soon!). 81 | 82 | ## Examples 83 | 84 | ### Counter 85 | ([source](https://github.com/btroncone/ngrx-examples/tree/master/counter)) 86 | ##### Summary 87 | A counter which can be incremented, decremented, with the option to increment or decrement async. 88 | ##### Demonstrates 89 | 1. Creating a basic reducer 90 | 2. Selecting a slice of state 91 | 3. Using the async pipe 92 | 4. Dispatching actions from a component 93 | 94 | ### Todos 95 | ([source](https://github.com/btroncone/ngrx-examples/tree/master/todos)) 96 | ##### Summary 97 | Basic todo application with add, remove, and toggle complete functionality. 98 | ##### Demonstrates 99 | 1. Initial reducer state 100 | 2. Managing arrays in reducers 101 | 3. Multiple reducers 102 | 4. Combining data from two reducers to project state for view 103 | 104 | ### Todos with Undo/Redo 105 | ([source](https://github.com/btroncone/ngrx-examples/tree/master/todos-undo-redo) | [plunker](http://plnkr.co/edit/UnU1wnFcausVFfEP2RGD?p=preview)) 106 | ##### Summary 107 | Same as todos example but with undo/redo functionality. 108 | ##### Demonstrates 109 | 1. Creating a meta-reducer to add undo/redo capability. 110 | 111 | ### Async 112 | ([source](https://github.com/btroncone/ngrx-examples/tree/master/async)) 113 | ##### Summary 114 | Request and display the latest Angular or React reddit posts, utilizing the reddit API. 115 | ##### Demonstrates 116 | 1. Handling async actions with @ngrx/effects 117 | 2. Conditionally making requests based on current state 118 | 119 | ### Shopping Cart 120 | ([source](https://github.com/btroncone/ngrx-examples/tree/master/shopping-cart)) 121 | ##### Summary 122 | Request sample inventory, add and remove items from shopping cart, checkout. 123 | ##### Demonstrates 124 | 1. Multiple Reducers 125 | 2. Handling effects with @ngrx/effects 126 | 3. Creating and applying selectors for state projection with `let` 127 | 128 | 129 | ### Finances 130 | ([source](https://github.com/btroncone/ngrx-examples/tree/master/finances)) 131 | ([tutorial](https://www.pluralsight.com/guides/front-end-javascript/building-a-redux-application-with-angular-2-part-1)) 132 | ##### Summary 133 | Add and remove items financial operations, change currency rates 134 | ##### Demonstrates 135 | 1. Multiple Reducers 136 | 2. Handling effects with @ngrx/effects 137 | 3. Modifying state projection 138 | 4. Using state in pipes 139 | 140 | ### Real World - In Progress! 141 | 142 | ### More to Come! 143 | -------------------------------------------------------------------------------- /async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-ngrx-async", 3 | "version": "0.0.2", 4 | "description": "angular 2 ngrx async example", 5 | "main": "", 6 | "scripts": { 7 | "build": "npm run webpack --colors --display-error-details --display-cached", 8 | "webpack": "webpack", 9 | "clean": "rimraf node_modules", 10 | "clean-install": "npm run clean && npm install", 11 | "clean-start": "npm run clean && npm start", 12 | "typings-install": "typings install", 13 | "postinstall": "npm run typings-install", 14 | "watch": "webpack --watch", 15 | "server": "npm run server:dev", 16 | "server:dev": "webpack-dev-server --progress --profile --colors --display-error-details --display-cached --content-base src/", 17 | "start": "npm run server:dev" 18 | }, 19 | "author": "btroncone@gmail.com", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@ngrx/store": "^2.0.0", 23 | "@ngrx/core": "^1.0.0", 24 | "@ngrx/effects": "^1.0.0", 25 | "@angular/common": "^2.0.0-rc.1", 26 | "@angular/compiler": "^2.0.0-rc.1", 27 | "@angular/upgrade": "^2.0.0-rc.1", 28 | "@angular/core": "^2.0.0-rc.1", 29 | "@angular/http": "^2.0.0-rc.1", 30 | "@angular/platform-browser": "^2.0.0-rc.1", 31 | "@angular/router": "^2.0.0-rc.1", 32 | "@angular/platform-browser-dynamic": "^2.0.0-rc.1", 33 | "core-js": "^2.1.5", 34 | "rxjs": "5.0.0-beta.6", 35 | "zone.js": "0.6.12" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/btroncone/ngrx-examples" 40 | }, 41 | "devDependencies": { 42 | "ngrx-store-logger": "0.1.2", 43 | "es6-promise": "^3.1.2", 44 | "es6-shim": "^0.35.0", 45 | "es7-reflect-metadata": "^1.6.0", 46 | "compression-webpack-plugin": "^0.3.0", 47 | "copy-webpack-plugin": "^1.1.1", 48 | "css-loader": "^0.23.1", 49 | "es6-promise-loader": "^1.0.1", 50 | "exports-loader": "^0.6.2", 51 | "expose-loader": "^0.7.1", 52 | "file-loader": "^0.8.5", 53 | "html-webpack-plugin": "^1.7.0", 54 | "http-server": "^0.8.5", 55 | "imports-loader": "^0.6.5", 56 | "istanbul-instrumenter-loader": "^0.1.3", 57 | "json-loader": "^0.5.4", 58 | "ncp": "^2.0.0", 59 | "phantomjs-polyfill": "0.0.1", 60 | "phantomjs-prebuilt": "^2.1.3", 61 | "raw-loader": "0.5.1", 62 | "reflect-metadata": "0.1.2", 63 | "remap-istanbul": "^0.5.1", 64 | "rimraf": "^2.5.1", 65 | "source-map-loader": "^0.1.5", 66 | "style-loader": "^0.13.0", 67 | "ts-helpers": "1.1.1", 68 | "ts-loader": "0.8.1", 69 | "ts-node": "^0.5.5", 70 | "tsconfig-lint": "^0.5.0", 71 | "tsd": "^0.6.5", 72 | "typedoc": "^0.3.12", 73 | "typescript": "~1.8.9", 74 | "url-loader": "^0.5.7", 75 | "wallaby-webpack": "0.0.11", 76 | "webpack": "^1.12.12", 77 | "webpack-dev-server": "^1.14.1", 78 | "webpack-md5-hash": "0.0.4" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /async/spec-bundle.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | /* 3 | * When testing with webpack and ES6, we have to do some extra 4 | * things get testing to work right. Because we are gonna write test 5 | * in ES6 to, we have to compile those as well. That's handled in 6 | * karma.conf.js with the karma-webpack plugin. This is the entry 7 | * file for webpack test. Just like webpack will create a bundle.js 8 | * file for our client, when we run test, it well compile and bundle them 9 | * all here! Crazy huh. So we need to do some setup 10 | */ 11 | Error.stackTraceLimit = Infinity; 12 | require('phantomjs-polyfill'); 13 | require('es6-promise'); 14 | require('es6-shim'); 15 | require('es7-reflect-metadata/dist/browser'); 16 | 17 | require('zone.js/dist/zone-microtask.js'); 18 | require('zone.js/dist/long-stack-trace-zone.js'); 19 | require('zone.js/dist/jasmine-patch.js'); 20 | 21 | 22 | var testing = require('angular2/testing'); 23 | var browser = require('angular2/platform/testing/browser'); 24 | testing.setBaseTestProviders( 25 | browser.TEST_BROWSER_PLATFORM_PROVIDERS, 26 | browser.TEST_BROWSER_APPLICATION_PROVIDERS); 27 | 28 | /* 29 | Ok, this is kinda crazy. We can use the the context method on 30 | require that webpack created in order to tell webpack 31 | what files we actually want to require or import. 32 | Below, context will be an function/object with file names as keys. 33 | using that regex we are saying look in ./src/app and ./test then find 34 | any file that ends with spec.js and get its path. By passing in true 35 | we say do this recursively 36 | */ 37 | var testContext = require.context('./src', true, /\.spec\.ts/); 38 | 39 | // get all the files, for each file, call the context function 40 | // that will require the file and load it up here. Context will 41 | // loop and require those spec files here 42 | testContext.keys().forEach(testContext); -------------------------------------------------------------------------------- /async/src/app/async-app.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import {RedditModel} from "./services/reddit-model"; 4 | import {RedditSelect} from "./components/reddit-select"; 5 | import {RedditList} from "./components/reddit-list"; 6 | import {DatePipe} from "@angular/common"; 7 | import {RefreshButton} from "./components/refresh-button"; 8 | import {SELECT_REDDIT, INVALIDATE_REDDIT} from "./reducers/reddit"; 9 | 10 | @Component({ 11 | selector: `async-app`, 12 | template: ` 13 |
14 | 20 |
21 |

Currently Displaying: {{redditModel.selectedReddit$ | async}}

22 |
Last Updated: {{(redditModel.lastUpdated$ | async) | date:'mediumTime'}}
23 | 26 | 27 | 30 | 31 | 34 | 35 |
36 |
37 | `, 38 | directives: [RedditList, RedditSelect, RefreshButton], 39 | providers: [RedditModel], 40 | changeDetection: ChangeDetectionStrategy.OnPush 41 | }) 42 | export class AsyncApp { 43 | constructor( 44 | private redditModel: RedditModel, 45 | private _store : Store 46 | ){} 47 | 48 | selectReddit(reddit: string){ 49 | this._store.dispatch({type: SELECT_REDDIT, payload: reddit}); 50 | } 51 | 52 | invalidateReddit(reddit : string){ 53 | this._store.dispatch({type: INVALIDATE_REDDIT, payload: {reddit}}); 54 | this._store.dispatch({type: SELECT_REDDIT, payload: reddit}); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /async/src/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from '@angular/platform-browser-dynamic'; 2 | import {HTTP_PROVIDERS} from '@angular/http'; 3 | import {AsyncApp} from './async-app'; 4 | import {RedditEffects} from "./effects/reddit-effects"; 5 | import {provideStore, combineReducers} from "@ngrx/store"; 6 | import {runEffects} from "@ngrx/effects"; 7 | import {selectedReddit, postsByReddit} from "./reducers/reddit"; 8 | import {Reddit} from "./services/reddit"; 9 | import {storeLogger} from "ngrx-store-logger"; 10 | 11 | export function main() { 12 | return bootstrap(AsyncApp, [ 13 | HTTP_PROVIDERS, 14 | provideStore( 15 | storeLogger()(combineReducers({selectedReddit, postsByReddit})) 16 | ), 17 | runEffects(RedditEffects), 18 | Reddit 19 | ]) 20 | .catch(err => console.error(err)); 21 | } 22 | 23 | document.addEventListener('DOMContentLoaded', main); -------------------------------------------------------------------------------- /async/src/app/components/reddit-list.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input} from "@angular/core"; 2 | import {RedditPosts} from "../reducers/reddit"; 3 | 4 | @Component({ 5 | selector: 'reddit-list', 6 | template: ` 7 |
8 | Loading... 9 | 14 |
15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class RedditList{ 19 | @Input() posts : RedditPosts[]; 20 | @Input() isFetching: boolean; 21 | } -------------------------------------------------------------------------------- /async/src/app/components/reddit-select.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, EventEmitter} from "@angular/core"; 2 | 3 | @Component({ 4 | selector: 'reddit-select', 5 | template: ` 6 |
7 | 12 |
13 | ` 14 | }) 15 | export class RedditSelect{ 16 | @Output() redditSelect : EventEmitter = new EventEmitter(); 17 | availableReddits : [string] = ["Angular 2", "ReactJS"]; 18 | 19 | ngOnInit(){ 20 | this.redditSelect.emit(this.availableReddits[0]); 21 | } 22 | } -------------------------------------------------------------------------------- /async/src/app/components/refresh-button.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | ChangeDetectionStrategy 7 | } from "@angular/core"; 8 | 9 | @Component({ 10 | selector: 'refresh-button', 11 | template: ` 12 | 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class RefreshButton { 19 | @Input() selectedReddit : string; 20 | @Output() invalidateReddit: EventEmitter = new EventEmitter(); 21 | } -------------------------------------------------------------------------------- /async/src/app/effects/reddit-effects.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {Store, Action} from "@ngrx/store"; 3 | import {StateUpdates, Effect} from "@ngrx/effects"; 4 | import {Observable} from "rxjs/Observable"; 5 | import {Subject} from "rxjs/Subject"; 6 | import {Reddit} from "../services/reddit"; 7 | import { 8 | REQUEST_POSTS, 9 | RECEIVE_POSTS, 10 | SELECT_REDDIT 11 | } from "../reducers/reddit"; 12 | 13 | 14 | @Injectable() 15 | export class RedditEffects{ 16 | constructor( 17 | private _updates$: StateUpdates, 18 | private _reddit : Reddit 19 | ){} 20 | 21 | @Effect() requestPosts$ = this._updates$ 22 | .whenAction(SELECT_REDDIT) 23 | .filter(({state, action}) => this.shouldFetchPosts(state.postsByReddit,action.payload)) 24 | .map(({action}) => ({type: REQUEST_POSTS, payload: {reddit: action.payload}})); 25 | 26 | @Effect() fetchPosts$ = this._updates$ 27 | .whenAction(REQUEST_POSTS) 28 | .switchMap(({action}) => ( 29 | this._reddit 30 | .fetchPosts(action.payload.reddit) 31 | .map(({data}) => ({ type: RECEIVE_POSTS, payload: {reddit: action.payload.reddit, data}}) 32 | ))); 33 | 34 | private shouldFetchPosts(postsByReddit, reddit){ 35 | const posts = postsByReddit[reddit]; 36 | if (!posts) { 37 | return true; 38 | } 39 | if (posts.isFetching) { 40 | return false; 41 | } 42 | return posts.didInvalidate; 43 | } 44 | } -------------------------------------------------------------------------------- /async/src/app/reducers/reddit.spec.ts: -------------------------------------------------------------------------------- 1 | import {selectedReddit, postsByReddit} from "./reddit"; 2 | //had issue with jasmine typing conflicts, this is temporary workaround 3 | declare var it, expect, describe, toBe; 4 | 5 | describe('The selectedReddit reducer', () => { 6 | it('should return current state when no valid actions have been made', () => { 7 | const state = "Angular 2"; 8 | const actual = selectedReddit(state, {type: 'INVALID_ACTION', payload: {}}); 9 | const expected = state; 10 | expect(actual).toBe(expected); 11 | }); 12 | 13 | it('should return currently selected reddit when SELECT_REDDIT is dispatched', () => { 14 | const state = "ReactJS"; 15 | const actual = selectedReddit(state, {type: 'SELECT_REDDIT', payload: 'ReactJS'}); 16 | const expected = state; 17 | expect(actual).toBe(expected); 18 | }); 19 | }); 20 | 21 | describe('The postsByReddit reducer', () => { 22 | 23 | it('should return current state when no valid actions have been made', () => { 24 | const state = {}; 25 | const actual = postsByReddit(state, {type: 'INVALID_ACTION', payload: {}}); 26 | const expected = state; 27 | expect(actual).toBe(expected); 28 | }); 29 | 30 | it('should set isFetching to true and didInvalidate to false when posts are requested', () => { 31 | const state = {}; 32 | const reddit = 'Angular 2'; 33 | const actual = postsByReddit(state, {type: 'REQUEST_POSTS', payload: {reddit}}); 34 | const expected = { 35 | [reddit]: { 36 | isFetching: true, 37 | didInvalidate: false, 38 | posts:[] 39 | } 40 | }; 41 | expect(actual).toEqual(expected); 42 | }); 43 | 44 | it('should invalidate a reddit when INVALIDATE_REDDIT is dispatched', () => { 45 | const reddit = 'Angular 2'; 46 | const state = { 47 | [reddit]: { 48 | isFetching: false, 49 | didInvalidate: false, 50 | posts:[] 51 | } 52 | }; 53 | const expected = { 54 | [reddit]: { 55 | isFetching: false, 56 | didInvalidate: true, 57 | posts:[] 58 | } 59 | }; 60 | const actual = postsByReddit(state, {type: 'INVALIDATE_REDDIT', payload: {reddit}}); 61 | expect(actual).toEqual(expected); 62 | }); 63 | 64 | it('should populate posts when RECEIEVE_POSTS is dispatched', () => { 65 | const reddit = 'Angular 2'; 66 | const state = { 67 | [reddit]: { 68 | isFetching: false, 69 | didInvalidate: false, 70 | posts:[] 71 | } 72 | }; 73 | const expected = { 74 | [reddit]: { 75 | isFetching: false, 76 | didInvalidate: true, 77 | posts:[{},{},{}] 78 | } 79 | }; 80 | const actual = postsByReddit(state, {type: 'RECEIVE_POSTS', payload: {reddit, data: {children: [{}, {}, {}]}}}); 81 | expect(actual[reddit].posts.length).toEqual(expected[reddit].posts.length); 82 | }); 83 | 84 | it('should mark lastUpdated when RECEIEVE_POSTS is dispatched', () => { 85 | const reddit = 'Angular 2'; 86 | const state = { 87 | [reddit]: { 88 | isFetching: false, 89 | didInvalidate: false, 90 | posts:[] 91 | } 92 | }; 93 | const actual = postsByReddit(state, {type: 'RECEIVE_POSTS', payload: {reddit, data: {children: [{}]}}}); 94 | expect(actual[reddit].lastUpdated).toBeDefined(); 95 | }); 96 | }); -------------------------------------------------------------------------------- /async/src/app/reducers/reddit.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | 3 | export interface RedditPosts { 4 | isFetching: boolean, 5 | didInvalidate?: boolean, 6 | posts: Array, 7 | lastUpdated?: Date, 8 | selectedReddit?: string 9 | } 10 | 11 | export const SELECT_REDDIT = 'SELECT_REDDIT'; 12 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'; 13 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 14 | export const RECEIVE_POSTS = 'RECEIVE_POSTS'; 15 | 16 | export const selectedReddit : ActionReducer = (state : string = 'Angular 2', action: Action) => { 17 | switch(action.type) { 18 | case SELECT_REDDIT: 19 | return action.payload; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | const posts : ActionReducer = (state : RedditPosts = { 26 | isFetching: false, 27 | didInvalidate: false, 28 | posts: [] 29 | }, action: Action) => { 30 | switch(action.type) { 31 | case INVALIDATE_REDDIT: 32 | return Object.assign({}, state, { 33 | didInvalidate: true 34 | }); 35 | case REQUEST_POSTS: 36 | return Object.assign({}, state, { 37 | isFetching: true, 38 | didInvalidate: false 39 | }); 40 | case RECEIVE_POSTS: 41 | return Object.assign({}, state, { 42 | isFetching: false, 43 | didInvalidate: false, 44 | posts: action.payload.data.children.map(child => child.data), 45 | lastUpdated: Date.now() 46 | }); 47 | default: 48 | return state; 49 | } 50 | }; 51 | 52 | export const postsByReddit : ActionReducer = (state: {} = {}, action : Action) => { 53 | switch (action.type) { 54 | case INVALIDATE_REDDIT: 55 | case RECEIVE_POSTS: 56 | case REQUEST_POSTS: 57 | return Object.assign({}, state, { 58 | [action.payload.reddit]: posts(state[action.payload.reddit], action) 59 | }); 60 | default: 61 | return state; 62 | } 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /async/src/app/services/reddit-model.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {Store} from "@ngrx/store"; 3 | import {Observable} from "rxjs/Observable"; 4 | import {RedditPosts} from "../reducers/reddit"; 5 | 6 | @Injectable() 7 | export class RedditModel{ 8 | public selectedReddit$ : Observable; 9 | public posts$ : Observable>; 10 | public isFetching$: Observable; 11 | public lastUpdated$: Observable; 12 | 13 | constructor( 14 | private store: Store 15 | ){ 16 | const model$ = Observable.combineLatest( 17 | store.select('postsByReddit'), 18 | store.select('selectedReddit'), 19 | (postsByReddit : Array, selectedReddit : string) => { 20 | const { 21 | isFetching, 22 | lastUpdated, 23 | posts 24 | } : RedditPosts = postsByReddit[selectedReddit] || { 25 | isFetching: true, 26 | posts: [] 27 | }; 28 | 29 | return { 30 | selectedReddit, 31 | posts, 32 | isFetching, 33 | lastUpdated 34 | } 35 | } 36 | ).share(); 37 | //expose to view 38 | this.selectedReddit$ = model$.map(vm => vm.selectedReddit); 39 | this.posts$ = model$.map(vm => vm.posts); 40 | this.isFetching$ = model$.map(vm => vm.isFetching); 41 | this.lastUpdated$ = model$.map(vm => vm.lastUpdated); 42 | } 43 | } -------------------------------------------------------------------------------- /async/src/app/services/reddit.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {Http} from "@angular/http"; 3 | 4 | @Injectable() 5 | export class Reddit{ 6 | constructor(private http : Http){} 7 | 8 | fetchPosts(reddit : string){ 9 | return this.http 10 | .get(`https://www.reddit.com/r/${reddit.replace(' ', '')}.json`) 11 | .map(response => response.json()); 12 | } 13 | } -------------------------------------------------------------------------------- /async/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%= o.webpackConfig.metadata.title %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 19 | 20 | {% } %} 21 | 22 | 23 | 24 | 25 | Loading... 26 | 27 | {% if (o.webpackConfig.metadata.ENV === 'development') { %} 28 | 29 | 30 | {% } %} 31 | 32 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 33 | 34 | {% } %} 35 | 36 | -------------------------------------------------------------------------------- /async/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6'; 2 | import 'core-js/es7/reflect'; 3 | import 'ts-helpers'; 4 | import 'rxjs'; 5 | require('zone.js/dist/zone'); 6 | require('zone.js/dist/long-stack-trace-zone'); -------------------------------------------------------------------------------- /async/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | a:hover, 12 | a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | h3 { 17 | font-weight: 100; 18 | } 19 | 20 | /* LAYOUT CSS */ 21 | .pure-img-responsive { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | #layout { 27 | padding: 0; 28 | } 29 | 30 | .header { 31 | text-align: center; 32 | top: auto; 33 | margin: 3em auto; 34 | } 35 | 36 | .sidebar { 37 | background: rgb(61, 79, 93); 38 | color: #fff; 39 | } 40 | 41 | .brand-title, 42 | .brand-tagline { 43 | margin: 0; 44 | } 45 | .brand-title { 46 | text-transform: uppercase; 47 | } 48 | .brand-tagline { 49 | font-weight: 300; 50 | color: rgb(176, 202, 219); 51 | } 52 | 53 | .nav-list { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | .nav-item { 59 | display: inline-block; 60 | *display: inline; 61 | zoom: 1; 62 | } 63 | .nav-item a { 64 | background: transparent; 65 | border: 2px solid rgb(176, 202, 219); 66 | color: #fff; 67 | margin-top: 1em; 68 | letter-spacing: 0.05em; 69 | text-transform: uppercase; 70 | font-size: 85%; 71 | } 72 | .nav-item a:hover, 73 | .nav-item a:focus { 74 | border: 2px solid rgb(61, 146, 201); 75 | text-decoration: none; 76 | } 77 | 78 | .content-subhead { 79 | text-transform: uppercase; 80 | color: #aaa; 81 | border-bottom: 1px solid #eee; 82 | padding: 0.4em 0; 83 | font-size: 80%; 84 | font-weight: 500; 85 | letter-spacing: 0.1em; 86 | } 87 | 88 | .content { 89 | padding: 2em 1em 0; 90 | } 91 | 92 | .post { 93 | padding-bottom: 2em; 94 | } 95 | .post-title { 96 | font-size: 2em; 97 | color: #222; 98 | margin-bottom: 0.2em; 99 | } 100 | .post-avatar { 101 | border-radius: 50px; 102 | float: right; 103 | margin-left: 1em; 104 | } 105 | .post-description { 106 | font-family: Georgia, "Cambria", serif; 107 | color: #444; 108 | line-height: 1.8em; 109 | } 110 | .post-meta { 111 | color: #999; 112 | font-size: 90%; 113 | margin: 0; 114 | } 115 | 116 | .post-category { 117 | margin: 0 0.1em; 118 | padding: 0.3em 1em; 119 | color: #fff; 120 | background: #999; 121 | font-size: 80%; 122 | } 123 | .post-category-design { 124 | background: #5aba59; 125 | } 126 | .post-category-pure { 127 | background: #4d85d1; 128 | } 129 | .post-category-yui { 130 | background: #8156a7; 131 | } 132 | .post-category-js { 133 | background: #df2d4f; 134 | } 135 | 136 | .post-images { 137 | margin: 1em 0; 138 | } 139 | .post-image-meta { 140 | margin-top: -3.5em; 141 | margin-left: 1em; 142 | color: #fff; 143 | text-shadow: 0 1px 1px #333; 144 | } 145 | 146 | .footer { 147 | text-align: center; 148 | padding: 1em 0; 149 | } 150 | .footer a { 151 | color: #ccc; 152 | font-size: 80%; 153 | } 154 | .footer .pure-menu a:hover, 155 | .footer .pure-menu a:focus { 156 | background: none; 157 | } 158 | 159 | @media (min-width: 48em) { 160 | .content { 161 | padding: 2em 3em 0; 162 | margin-left: 25%; 163 | } 164 | 165 | .header { 166 | margin: 80% 2em 0; 167 | text-align: right; 168 | } 169 | 170 | .sidebar { 171 | position: fixed; 172 | top: 0; 173 | bottom: 0; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /async/src/vendor.ts: -------------------------------------------------------------------------------- 1 | import '@angular/platform-browser'; 2 | import '@angular/platform-browser-dynamic'; 3 | import '@angular/core'; 4 | import '@angular/common'; 5 | import '@angular/http'; 6 | import '@angular/router-deprecated'; 7 | import 'rxjs'; -------------------------------------------------------------------------------- /async/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true 9 | }, 10 | "exclude":[ 11 | "node_modules", 12 | "typings/main.d.ts", 13 | "typings/main", 14 | "src/app/reducers/reddit.spec.ts" 15 | ], 16 | "filesGlob": [ 17 | "./src/**/*.ts", 18 | "!./node_modules/**/*.ts", 19 | "typings/browser.d.ts" 20 | ], 21 | "compileOnSave": false, 22 | "buildOnSave": false 23 | } -------------------------------------------------------------------------------- /async/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "angular-protractor": "registry:dt/angular-protractor#1.5.0+20160425143459", 4 | "core-js": "registry:dt/core-js#0.0.0+20160317120654", 5 | "hammerjs": "registry:dt/hammerjs#2.0.4+20160417130828", 6 | "jasmine": "registry:dt/jasmine#2.2.0+20160505161446", 7 | "node": "registry:dt/node#6.0.0+20160514165920", 8 | "selenium-webdriver": "registry:dt/selenium-webdriver#2.44.0+20160317120654", 9 | "source-map": "registry:dt/source-map#0.0.0+20160317120654", 10 | "uglify-js": "registry:dt/uglify-js#2.6.1+20160316155526", 11 | "webpack": "registry:dt/webpack#1.12.9+20160321060707" 12 | } 13 | } -------------------------------------------------------------------------------- /async/wallaby.js: -------------------------------------------------------------------------------- 1 | var wallabyWebpack = require('wallaby-webpack'); 2 | 3 | var webpackPostprocessor = wallabyWebpack({ 4 | entryPatterns: [ 5 | 'spec-bundle.js', 6 | 'src/**/*spec.js' 7 | ] 8 | }); 9 | 10 | module.exports = function (w) { 11 | 12 | return { 13 | files: [ 14 | {pattern: 'spec-bundle.js', load: false}, 15 | {pattern: 'src/**/*.ts', load: false}, 16 | {pattern: 'src/**/*spec.ts', ignore: true} 17 | ], 18 | 19 | tests: [ 20 | { pattern: 'src/**/*spec.ts', load: false } 21 | ], 22 | 23 | testFramework: "jasmine", 24 | 25 | compilers: { 26 | '**/*.ts': w.compilers.typeScript({ 27 | emitDecoratorMetadata: true, 28 | experimentalDecorators: true 29 | }) 30 | }, 31 | 32 | postprocessor: webpackPostprocessor, 33 | 34 | bootstrap: function () { 35 | window.__moduleBundler.loadTests(); 36 | } 37 | }; 38 | }; -------------------------------------------------------------------------------- /async/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | 3 | /* 4 | * Helper: root(), and rootDir() are defined at the bottom 5 | */ 6 | var path = require('path'); 7 | var webpack = require('webpack'); 8 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | var ENV = process.env.ENV = process.env.NODE_ENV = 'development'; 11 | 12 | var metadata = { 13 | title: 'NgRx Async Example', 14 | baseUrl: '/', 15 | host: 'localhost', 16 | port: 3000, 17 | ENV: ENV 18 | }; 19 | /* 20 | * Config 21 | */ 22 | module.exports = { 23 | // static data for index.html 24 | metadata: metadata, 25 | // for faster builds use 'eval' 26 | devtool: 'source-map', 27 | debug: true, 28 | // cache: false, 29 | 30 | // our angular app 31 | entry: { 'polyfills': './src/polyfills.ts', 'main': './src/app/bootstrap.ts' }, 32 | 33 | // Config for our build files 34 | output: { 35 | path: root('dist'), 36 | filename: '[name].bundle.js', 37 | sourceMapFilename: '[name].map', 38 | chunkFilename: '[id].chunk.js' 39 | }, 40 | 41 | 42 | resolve: { 43 | // ensure loader extensions match 44 | extensions: prepend(['.ts','.js','.json','.css','.html'], '.async') // ensure .async.ts etc also works 45 | }, 46 | 47 | module: { 48 | preLoaders: [ 49 | { test: /\.js$/, loader: "source-map-loader", exclude: [ root('node_modules/rxjs') ] } 50 | ], 51 | loaders: [ 52 | // Support Angular 2 async routes via .async.ts 53 | { test: /\.async\.ts$/, loaders: ['es6-promise-loader', 'ts-loader'], exclude: [ /\.(spec|e2e)\.ts$/ ] }, 54 | 55 | // Support for .ts files. 56 | { test: /\.ts$/, loader: 'ts-loader', exclude: [ /\.(spec|e2e|async)\.ts$/ ] }, 57 | 58 | // Support for *.json files. 59 | { test: /\.json$/, loader: 'json-loader' }, 60 | 61 | // Support for CSS as raw text 62 | { test: /\.css$/, loader: 'raw-loader' }, 63 | 64 | // support for .html as raw text 65 | { test: /\.html$/, loader: 'raw-loader' } 66 | 67 | // if you add a loader include the resolve file extension above 68 | ] 69 | }, 70 | 71 | plugins: [ 72 | new webpack.optimize.OccurenceOrderPlugin(true), 73 | new webpack.optimize.CommonsChunkPlugin({ name: 'polyfills', filename: 'polyfills.bundle.js', minChunks: Infinity }), 74 | // static assets 75 | new CopyWebpackPlugin([ { from: 'src/assets', to: 'assets' } ]), 76 | // generating html 77 | new HtmlWebpackPlugin({ template: 'src/index.html', inject: false }), 78 | // replace 79 | new webpack.DefinePlugin({ 80 | 'process.env': { 81 | 'ENV': JSON.stringify(metadata.ENV), 82 | 'NODE_ENV': JSON.stringify(metadata.ENV) 83 | } 84 | }) 85 | ], 86 | 87 | // Other module loader config 88 | tslint: { 89 | emitErrors: false, 90 | failOnHint: false, 91 | resourcePath: 'src' 92 | }, 93 | // our Webpack Development Server config 94 | devServer: { 95 | port: metadata.port, 96 | host: metadata.host, 97 | // contentBase: 'src/', 98 | historyApiFallback: true, 99 | watchOptions: { aggregateTimeout: 300, poll: 1000 } 100 | }, 101 | // we need this due to problems with es6-shim 102 | node: {global: 'window', progress: false, crypto: 'empty', module: false, clearImmediate: false, setImmediate: false} 103 | }; 104 | 105 | // Helper functions 106 | 107 | function root(args) { 108 | args = Array.prototype.slice.call(arguments, 0); 109 | return path.join.apply(path, [__dirname].concat(args)); 110 | } 111 | 112 | function prepend(extensions, args) { 113 | args = args || []; 114 | if (!Array.isArray(args)) { args = [args] } 115 | return extensions.reduce(function(memo, val) { 116 | return memo.concat(val, args.map(function(prefix) { 117 | return prefix + val 118 | })); 119 | }, ['']); 120 | } 121 | function rootNode(args) { 122 | args = Array.prototype.slice.call(arguments, 0); 123 | return root.apply(path, ['node_modules'].concat(args)); 124 | } -------------------------------------------------------------------------------- /counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-ngrx-counter", 3 | "version": "0.0.2", 4 | "description": "angular 2 ngrx counter example", 5 | "main": "", 6 | "scripts": { 7 | "build": "npm run webpack --colors --display-error-details --display-cached", 8 | "webpack": "webpack", 9 | "clean": "rimraf node_modules", 10 | "clean-install": "npm run clean && npm install", 11 | "clean-start": "npm run clean && npm start", 12 | "typings-install": "typings install", 13 | "postinstall": "npm run typings-install", 14 | "watch": "webpack --watch", 15 | "server": "npm run server:dev", 16 | "server:dev": "webpack-dev-server --progress --profile --colors --display-error-details --display-cached --content-base src/", 17 | "start": "npm run server:dev" 18 | }, 19 | "author": "btroncone@gmail.com", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@ngrx/store": "^2.0.0", 23 | "@ngrx/core": "^1.0.0", 24 | "@angular/common": "^2.0.0-rc.1", 25 | "@angular/compiler": "^2.0.0-rc.1", 26 | "@angular/upgrade": "^2.0.0-rc.1", 27 | "@angular/core": "^2.0.0-rc.1", 28 | "@angular/http": "^2.0.0-rc.1", 29 | "@angular/platform-browser": "^2.0.0-rc.1", 30 | "@angular/router": "^2.0.0-rc.1", 31 | "@angular/platform-browser-dynamic": "^2.0.0-rc.1", 32 | "core-js": "^2.1.5", 33 | "rxjs": "5.0.0-beta.6", 34 | "zone.js": "0.6.12" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/btroncone/ngrx-examples" 39 | }, 40 | "devDependencies": { 41 | "es6-promise": "^3.1.2", 42 | "es6-shim": "^0.35.0", 43 | "es7-reflect-metadata": "^1.6.0", 44 | "compression-webpack-plugin": "^0.3.0", 45 | "copy-webpack-plugin": "^1.1.1", 46 | "css-loader": "^0.23.1", 47 | "es6-promise-loader": "^1.0.1", 48 | "exports-loader": "^0.6.2", 49 | "expose-loader": "^0.7.1", 50 | "file-loader": "^0.8.5", 51 | "html-webpack-plugin": "^1.7.0", 52 | "http-server": "^0.8.5", 53 | "imports-loader": "^0.6.5", 54 | "istanbul-instrumenter-loader": "^0.1.3", 55 | "json-loader": "^0.5.4", 56 | "ncp": "^2.0.0", 57 | "phantomjs-polyfill": "0.0.1", 58 | "phantomjs-prebuilt": "^2.1.3", 59 | "raw-loader": "0.5.1", 60 | "reflect-metadata": "0.1.2", 61 | "remap-istanbul": "^0.5.1", 62 | "rimraf": "^2.5.1", 63 | "source-map-loader": "^0.1.5", 64 | "style-loader": "^0.13.0", 65 | "ts-helpers": "1.1.1", 66 | "ts-loader": "0.8.1", 67 | "ts-node": "^0.5.5", 68 | "tsconfig-lint": "^0.5.0", 69 | "tsd": "^0.6.5", 70 | "typedoc": "^0.3.12", 71 | "typescript": "~1.8.9", 72 | "url-loader": "^0.5.7", 73 | "wallaby-webpack": "0.0.11", 74 | "webpack": "^1.12.12", 75 | "webpack-dev-server": "^1.14.1", 76 | "webpack-md5-hash": "0.0.4" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /counter/spec-bundle.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | /* 3 | * When testing with webpack and ES6, we have to do some extra 4 | * things get testing to work right. Because we are gonna write test 5 | * in ES6 to, we have to compile those as well. That's handled in 6 | * karma.conf.js with the karma-webpack plugin. This is the entry 7 | * file for webpack test. Just like webpack will create a bundle.js 8 | * file for our client, when we run test, it well compile and bundle them 9 | * all here! Crazy huh. So we need to do some setup 10 | */ 11 | Error.stackTraceLimit = Infinity; 12 | require('phantomjs-polyfill'); 13 | require('es6-promise'); 14 | require('es6-shim'); 15 | require('es7-reflect-metadata/dist/browser'); 16 | 17 | require('zone.js/dist/zone-microtask.js'); 18 | require('zone.js/dist/long-stack-trace-zone.js'); 19 | require('zone.js/dist/jasmine-patch.js'); 20 | 21 | 22 | var testing = require('angular2/testing'); 23 | var browser = require('angular2/platform/testing/browser'); 24 | testing.setBaseTestProviders( 25 | browser.TEST_BROWSER_PLATFORM_PROVIDERS, 26 | browser.TEST_BROWSER_APPLICATION_PROVIDERS); 27 | 28 | /* 29 | Ok, this is kinda crazy. We can use the the context method on 30 | require that webpack created in order to tell webpack 31 | what files we actually want to require or import. 32 | Below, context will be an function/object with file names as keys. 33 | using that regex we are saying look in ./src/app and ./test then find 34 | any file that ends with spec.js and get its path. By passing in true 35 | we say do this recursively 36 | */ 37 | var testContext = require.context('./src', true, /\.spec\.ts/); 38 | 39 | // get all the files, for each file, call the context function 40 | // that will require the file and load it up here. Context will 41 | // loop and require those spec files here 42 | testContext.keys().forEach(testContext); -------------------------------------------------------------------------------- /counter/src/app/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Counter} from './components/counter'; 3 | 4 | @Component({ 5 | selector: `app`, 6 | template: ` 7 |
8 | 14 |
15 | 16 |
17 |
18 | `, 19 | directives: [Counter] 20 | }) 21 | export class App {} -------------------------------------------------------------------------------- /counter/src/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from '@angular/platform-browser-dynamic'; 2 | import {App} from './app'; 3 | import {provideStore} from "@ngrx/store"; 4 | import {counter} from "./reducers/counter"; 5 | 6 | export function main() { 7 | return bootstrap(App, [ 8 | provideStore({counter}) 9 | ]) 10 | .catch(err => console.error(err)); 11 | } 12 | 13 | document.addEventListener('DOMContentLoaded', main); -------------------------------------------------------------------------------- /counter/src/app/components/counter.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from "@angular/core"; 2 | import {Store} from "@ngrx/store"; 3 | import {Observable} from "rxjs/Observable"; 4 | 5 | @Component({ 6 | selector: 'counter', 7 | template: ` 8 |
9 | 10 | 11 | 12 | 13 |

{{counter$ | async}}

14 |
15 | `, 16 | //Unless a reference changes, ignore change detection on this component. 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class Counter{ 20 | counter$: Observable; 21 | 22 | constructor( 23 | private store : Store 24 | ){ 25 | /* 26 | Select returns an observable of the appropriate slice of state (reducer) from store. 27 | This is equivalent to store.map(state => state['counter']).distinctUntilChanged() 28 | */ 29 | this.counter$ = this.store.select('counter') 30 | } 31 | /* 32 | The only way to modify state in store is through dispatched actions. 33 | Actions require a type (string) and optional payload. 34 | This type will match up to a case in one of your application reducers, 35 | specifying how this action will create a new representation 36 | of that particular section of state. 37 | */ 38 | increment(){ 39 | this.store.dispatch({type: 'INCREMENT'}); 40 | } 41 | 42 | decrement(){ 43 | this.store.dispatch({type: 'DECREMENT'}); 44 | } 45 | 46 | incrementAsync(){ 47 | setTimeout(() => { 48 | this.store.dispatch({type: 'INCREMENT'}); 49 | }, 1000); 50 | } 51 | 52 | decrementAsync(){ 53 | setTimeout(() => { 54 | this.store.dispatch({type: 'DECREMENT'}); 55 | }, 1000); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /counter/src/app/reducers/counter.spec.ts: -------------------------------------------------------------------------------- 1 | import {counter} from "./counter"; 2 | //had issue with jasmine typing conflicts, this is temporary workaround 3 | declare var it, expect, describe, toBe; 4 | 5 | describe('The counter reducer', () => { 6 | it('should return current state when an invalid action is dispatched', () => { 7 | const actual = counter(0, {type: 'INVALID_ACTION'}); 8 | const expected = 0; 9 | expect(actual).toBe(expected); 10 | }); 11 | 12 | it('should increment the counter when INCREMENT action is dispatched', () => { 13 | const actual = counter(0, {type: 'INCREMENT'}); 14 | const expected = 1; 15 | expect(actual).toBe(expected); 16 | }); 17 | 18 | it('should decrement the counter when DECREMENT action is dispatched', () => { 19 | const actual = counter(0, {type: 'DECREMENT'}); 20 | const expected = -1; 21 | expect(actual).toBe(expected); 22 | }); 23 | }); -------------------------------------------------------------------------------- /counter/src/app/reducers/counter.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | 3 | /* 4 | Default parameter will be used for initial state unless initial 5 | state is provided for this reducer in 'provideStore' method. 6 | */ 7 | export const counter: ActionReducer = (state: number = 0, action: Action) => { 8 | switch(action.type){ 9 | case 'INCREMENT': 10 | return state + 1; 11 | case 'DECREMENT': 12 | return state - 1; 13 | default: 14 | return state; 15 | } 16 | }; -------------------------------------------------------------------------------- /counter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%= o.webpackConfig.metadata.title %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 19 | 20 | {% } %} 21 | 22 | 23 | 24 | 25 | Loading... 26 | 27 | {% if (o.webpackConfig.metadata.ENV === 'development') { %} 28 | 29 | 30 | {% } %} 31 | 32 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 33 | 34 | {% } %} 35 | 36 | -------------------------------------------------------------------------------- /counter/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6'; 2 | import 'core-js/es7/reflect'; 3 | import 'ts-helpers'; 4 | import 'rxjs'; 5 | require('zone.js/dist/zone'); 6 | require('zone.js/dist/long-stack-trace-zone'); -------------------------------------------------------------------------------- /counter/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | a:hover, 12 | a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | h3 { 17 | font-weight: 100; 18 | } 19 | 20 | /* LAYOUT CSS */ 21 | .pure-img-responsive { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | #layout { 27 | padding: 0; 28 | } 29 | 30 | .header { 31 | text-align: center; 32 | top: auto; 33 | margin: 3em auto; 34 | } 35 | 36 | .sidebar { 37 | background: rgb(61, 79, 93); 38 | color: #fff; 39 | } 40 | 41 | .brand-title, 42 | .brand-tagline { 43 | margin: 0; 44 | } 45 | .brand-title { 46 | text-transform: uppercase; 47 | } 48 | .brand-tagline { 49 | font-weight: 300; 50 | color: rgb(176, 202, 219); 51 | } 52 | 53 | .nav-list { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | .nav-item { 59 | display: inline-block; 60 | *display: inline; 61 | zoom: 1; 62 | } 63 | .nav-item a { 64 | background: transparent; 65 | border: 2px solid rgb(176, 202, 219); 66 | color: #fff; 67 | margin-top: 1em; 68 | letter-spacing: 0.05em; 69 | text-transform: uppercase; 70 | font-size: 85%; 71 | } 72 | .nav-item a:hover, 73 | .nav-item a:focus { 74 | border: 2px solid rgb(61, 146, 201); 75 | text-decoration: none; 76 | } 77 | 78 | .content-subhead { 79 | text-transform: uppercase; 80 | color: #aaa; 81 | border-bottom: 1px solid #eee; 82 | padding: 0.4em 0; 83 | font-size: 80%; 84 | font-weight: 500; 85 | letter-spacing: 0.1em; 86 | } 87 | 88 | .content { 89 | padding: 2em 1em 0; 90 | } 91 | 92 | .post { 93 | padding-bottom: 2em; 94 | } 95 | .post-title { 96 | font-size: 2em; 97 | color: #222; 98 | margin-bottom: 0.2em; 99 | } 100 | .post-avatar { 101 | border-radius: 50px; 102 | float: right; 103 | margin-left: 1em; 104 | } 105 | .post-description { 106 | font-family: Georgia, "Cambria", serif; 107 | color: #444; 108 | line-height: 1.8em; 109 | } 110 | .post-meta { 111 | color: #999; 112 | font-size: 90%; 113 | margin: 0; 114 | } 115 | 116 | .post-category { 117 | margin: 0 0.1em; 118 | padding: 0.3em 1em; 119 | color: #fff; 120 | background: #999; 121 | font-size: 80%; 122 | } 123 | .post-category-design { 124 | background: #5aba59; 125 | } 126 | .post-category-pure { 127 | background: #4d85d1; 128 | } 129 | .post-category-yui { 130 | background: #8156a7; 131 | } 132 | .post-category-js { 133 | background: #df2d4f; 134 | } 135 | 136 | .post-images { 137 | margin: 1em 0; 138 | } 139 | .post-image-meta { 140 | margin-top: -3.5em; 141 | margin-left: 1em; 142 | color: #fff; 143 | text-shadow: 0 1px 1px #333; 144 | } 145 | 146 | .footer { 147 | text-align: center; 148 | padding: 1em 0; 149 | } 150 | .footer a { 151 | color: #ccc; 152 | font-size: 80%; 153 | } 154 | .footer .pure-menu a:hover, 155 | .footer .pure-menu a:focus { 156 | background: none; 157 | } 158 | 159 | @media (min-width: 48em) { 160 | .content { 161 | padding: 2em 3em 0; 162 | margin-left: 25%; 163 | } 164 | 165 | .header { 166 | margin: 80% 2em 0; 167 | text-align: right; 168 | } 169 | 170 | .sidebar { 171 | position: fixed; 172 | top: 0; 173 | bottom: 0; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /counter/src/vendor.ts: -------------------------------------------------------------------------------- 1 | import '@angular/platform-browser'; 2 | import '@angular/platform-browser-dynamic'; 3 | import '@angular/core'; 4 | import '@angular/common'; 5 | import '@angular/http'; 6 | import '@angular/router-deprecated'; 7 | import 'rxjs'; -------------------------------------------------------------------------------- /counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true 9 | }, 10 | "exclude":[ 11 | "node_modules", 12 | "typings/main.d.ts", 13 | "typings/main" 14 | ], 15 | "filesGlob": [ 16 | "./src/**/*.ts", 17 | "!./node_modules/**/*.ts", 18 | "typings/browser.d.ts" 19 | ], 20 | "compileOnSave": false, 21 | "buildOnSave": false 22 | } -------------------------------------------------------------------------------- /counter/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "es6-promise": "github:typed-typings/npm-es6-promise#fb04188767acfec1defd054fc8024fafa5cd4de7" 4 | }, 5 | "devDependencies": {}, 6 | "ambientDependencies": { 7 | "angular-protractor": "github:DefinitelyTyped/DefinitelyTyped/angular-protractor/angular-protractor.d.ts#64b25f63f0ec821040a5d3e049a976865062ed9d", 8 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2", 9 | "hammerjs": "github:DefinitelyTyped/DefinitelyTyped/hammerjs/hammerjs.d.ts#74a4dfc1bc2dfadec47b8aae953b28546cb9c6b7", 10 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c", 11 | "ng2": "github:gdi2290/typings-ng2/ng2.d.ts#32998ff5584c0eab0cd9dc7704abb1c5c450701c", 12 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#8cf8164641be73e8f1e652c2a5b967c7210b6729", 13 | "selenium-webdriver": "github:DefinitelyTyped/DefinitelyTyped/selenium-webdriver/selenium-webdriver.d.ts#a83677ed13add14c2ab06c7325d182d0ba2784ea", 14 | "webpack": "github:DefinitelyTyped/DefinitelyTyped/webpack/webpack.d.ts#95c02169ba8fa58ac1092422efbd2e3174a206f4", 15 | "zone.js": "github:DefinitelyTyped/DefinitelyTyped/zone.js/zone.js.d.ts#c393f8974d44840a6c9cc6d5b5c0188a8f05143d" 16 | } 17 | } -------------------------------------------------------------------------------- /counter/wallaby.js: -------------------------------------------------------------------------------- 1 | var wallabyWebpack = require('wallaby-webpack'); 2 | 3 | var webpackPostprocessor = wallabyWebpack({ 4 | entryPatterns: [ 5 | 'spec-bundle.js', 6 | 'src/**/*spec.js' 7 | ] 8 | }); 9 | 10 | module.exports = function (w) { 11 | 12 | return { 13 | files: [ 14 | {pattern: 'spec-bundle.js', load: false}, 15 | {pattern: 'src/**/*.ts', load: false}, 16 | {pattern: 'src/**/*spec.ts', ignore: true} 17 | ], 18 | 19 | tests: [ 20 | { pattern: 'src/**/*spec.ts', load: false } 21 | ], 22 | 23 | testFramework: "jasmine", 24 | 25 | compilers: { 26 | '**/*.ts': w.compilers.typeScript({ 27 | emitDecoratorMetadata: true, 28 | experimentalDecorators: true 29 | }) 30 | }, 31 | 32 | postprocessor: webpackPostprocessor, 33 | 34 | bootstrap: function () { 35 | window.__moduleBundler.loadTests(); 36 | } 37 | }; 38 | }; -------------------------------------------------------------------------------- /counter/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | 3 | /* 4 | * Helper: root(), and rootDir() are defined at the bottom 5 | */ 6 | var path = require('path'); 7 | var webpack = require('webpack'); 8 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | var ENV = process.env.ENV = process.env.NODE_ENV = 'development'; 11 | 12 | var metadata = { 13 | title: 'NgRx Counter Example', 14 | baseUrl: '/', 15 | host: 'localhost', 16 | port: 3000, 17 | ENV: ENV 18 | }; 19 | /* 20 | * Config 21 | */ 22 | module.exports = { 23 | // static data for index.html 24 | metadata: metadata, 25 | // for faster builds use 'eval' 26 | devtool: 'source-map', 27 | debug: true, 28 | // cache: false, 29 | 30 | // our angular app 31 | entry: { 'polyfills': './src/polyfills.ts', 'main': './src/app/bootstrap.ts' }, 32 | 33 | // Config for our build files 34 | output: { 35 | path: root('dist'), 36 | filename: '[name].bundle.js', 37 | sourceMapFilename: '[name].map', 38 | chunkFilename: '[id].chunk.js' 39 | }, 40 | 41 | resolve: { 42 | // ensure loader extensions match 43 | extensions: prepend(['.ts','.js','.json','.css','.html'], '.async') // ensure .async.ts etc also works 44 | }, 45 | 46 | module: { 47 | preLoaders: [ 48 | { test: /\.js$/, loader: "source-map-loader", exclude: [ root('node_modules/rxjs') ] } 49 | ], 50 | loaders: [ 51 | // Support Angular 2 async routes via .async.ts 52 | { test: /\.async\.ts$/, loaders: ['es6-promise-loader', 'ts-loader'], exclude: [ /\.(spec|e2e)\.ts$/ ] }, 53 | 54 | // Support for .ts files. 55 | { test: /\.ts$/, loader: 'ts-loader', exclude: [ /\.(spec|e2e|async)\.ts$/ ] }, 56 | 57 | // Support for *.json files. 58 | { test: /\.json$/, loader: 'json-loader' }, 59 | 60 | // Support for CSS as raw text 61 | { test: /\.css$/, loader: 'raw-loader' }, 62 | 63 | // support for .html as raw text 64 | { test: /\.html$/, loader: 'raw-loader', exclude: [ root('src/index.html') ] } 65 | 66 | // if you add a loader include the resolve file extension above 67 | ] 68 | }, 69 | 70 | plugins: [ 71 | new webpack.optimize.OccurenceOrderPlugin(true), 72 | new webpack.optimize.CommonsChunkPlugin({ name: 'polyfills', filename: 'polyfills.bundle.js', minChunks: Infinity }), 73 | // static assets 74 | new CopyWebpackPlugin([ { from: 'src/assets', to: 'assets' } ]), 75 | // generating html 76 | new HtmlWebpackPlugin({ template: 'src/index.html' }), 77 | // replace 78 | new webpack.DefinePlugin({ 79 | 'process.env': { 80 | 'ENV': JSON.stringify(metadata.ENV), 81 | 'NODE_ENV': JSON.stringify(metadata.ENV) 82 | } 83 | }) 84 | ], 85 | 86 | // Other module loader config 87 | tslint: { 88 | emitErrors: false, 89 | failOnHint: false, 90 | resourcePath: 'src' 91 | }, 92 | // our Webpack Development Server config 93 | devServer: { 94 | port: metadata.port, 95 | host: metadata.host, 96 | // contentBase: 'src/', 97 | historyApiFallback: true, 98 | watchOptions: { aggregateTimeout: 300, poll: 1000 } 99 | }, 100 | // we need this due to problems with es6-shim 101 | node: {global: 'window', progress: false, crypto: 'empty', module: false, clearImmediate: false, setImmediate: false} 102 | }; 103 | 104 | // Helper functions 105 | 106 | function root(args) { 107 | args = Array.prototype.slice.call(arguments, 0); 108 | return path.join.apply(path, [__dirname].concat(args)); 109 | } 110 | 111 | function prepend(extensions, args) { 112 | args = args || []; 113 | if (!Array.isArray(args)) { args = [args] } 114 | return extensions.reduce(function(memo, val) { 115 | return memo.concat(val, args.map(function(prefix) { 116 | return prefix + val 117 | })); 118 | }, ['']); 119 | } 120 | function rootNode(args) { 121 | args = Array.prototype.slice.call(arguments, 0); 122 | return root.apply(path, ['node_modules'].concat(args)); 123 | } -------------------------------------------------------------------------------- /finances/README.md: -------------------------------------------------------------------------------- 1 | # FinancesRedux 2 | 3 | This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.19-3. 4 | 5 | ## Development server 6 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 7 | 8 | ## Code scaffolding 9 | 10 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class`. 11 | 12 | ## Build 13 | 14 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 15 | 16 | ## Running unit tests 17 | 18 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 19 | 20 | ## Running end-to-end tests 21 | 22 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 23 | Before running the tests make sure you are serving the app via `ng serve`. 24 | 25 | ## Deploying to Github Pages 26 | 27 | Run `ng github-pages:deploy` to deploy to Github Pages. 28 | 29 | ## Further help 30 | 31 | To get more help on the `angular-cli` use `ng --help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 32 | -------------------------------------------------------------------------------- /finances/angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.19-3", 4 | "name": "finances-redux" 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 | "test": "test.ts", 17 | "tsconfig": "tsconfig.json", 18 | "prefix": "app", 19 | "mobile": false, 20 | "styles": [ 21 | "styles.css", 22 | "../node_modules/bootstrap/dist/css/bootstrap.css" 23 | ], 24 | "scripts": [], 25 | "environments": { 26 | "source": "environments/environment.ts", 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts", 29 | "scripts": [ 30 | "../node_modules/jquery/dist/jquery.js", 31 | "../node_modules/tether/dist/js/tether.js", 32 | "../node_modules/bootstrap/dist/js/bootstrap.js" 33 | ] 34 | } 35 | } 36 | ], 37 | "addons": [], 38 | "packages": [], 39 | "e2e": { 40 | "protractor": { 41 | "config": "./protractor.conf.js" 42 | } 43 | }, 44 | "test": { 45 | "karma": { 46 | "config": "./karma.conf.js" 47 | } 48 | }, 49 | "defaults": { 50 | "styleExt": "css", 51 | "prefixInterfaces": false, 52 | "inline": { 53 | "style": false, 54 | "template": false 55 | }, 56 | "spec": { 57 | "class": false, 58 | "component": true, 59 | "directive": true, 60 | "module": false, 61 | "pipe": true, 62 | "service": true 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /finances/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { FinancesReduxPage } from './app.po'; 2 | 3 | describe('finances-redux App', function() { 4 | let page: FinancesReduxPage; 5 | 6 | beforeEach(() => { 7 | page = new FinancesReduxPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /finances/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class FinancesReduxPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /finances/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /finances/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | remapIstanbulReporter: { 21 | reports: { 22 | html: 'coverage', 23 | lcovonly: './coverage/coverage.lcov' 24 | } 25 | }, 26 | angularCli: { 27 | config: './angular-cli.json', 28 | environment: 'dev' 29 | }, 30 | reporters: config.angularCli && config.angularCli.codeCoverage 31 | ? ['progress', 'karma-remap-istanbul'] 32 | : ['progress'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /finances/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "finances-redux", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "start": "ng serve", 8 | "lint": "tslint \"src/**/*.ts\"", 9 | "test": "ng test", 10 | "pree2e": "webdriver-manager update", 11 | "e2e": "protractor" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "~2.1.0", 16 | "@angular/compiler": "~2.1.0", 17 | "@angular/core": "~2.1.0", 18 | "@angular/forms": "~2.1.0", 19 | "@angular/http": "~2.1.0", 20 | "@angular/platform-browser": "~2.1.0", 21 | "@angular/platform-browser-dynamic": "~2.1.0", 22 | "@angular/router": "~3.1.0", 23 | "@ngrx/core": "^1.2.0", 24 | "@ngrx/store": "^2.2.1", 25 | "core-js": "^2.4.1", 26 | "rxjs": "5.0.0-beta.12", 27 | "ts-helpers": "^1.1.1", 28 | "zone.js": "^0.6.23" 29 | }, 30 | "devDependencies": { 31 | "@types/jasmine": "^2.2.30", 32 | "@types/node": "^6.0.42", 33 | "angular-cli": "1.0.0-beta.19-3", 34 | "codelyzer": "1.0.0-beta.1", 35 | "jasmine-core": "2.4.1", 36 | "jasmine-spec-reporter": "2.5.0", 37 | "karma": "1.2.0", 38 | "karma-chrome-launcher": "^2.0.0", 39 | "karma-cli": "^1.0.1", 40 | "karma-jasmine": "^1.0.2", 41 | "karma-remap-istanbul": "^0.2.1", 42 | "protractor": "4.0.9", 43 | "ts-node": "1.2.1", 44 | "tslint": "3.13.0", 45 | "typescript": "~2.0.3", 46 | "webdriver-manager": "10.2.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /finances/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/docs/referenceConf.js 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /finances/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btroncone/ngrx-examples/b91f62d1c7f8f053420ee1dd61839332fe4e989e/finances/src/app/app.component.css -------------------------------------------------------------------------------- /finances/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('App: FinancesRedux', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | }); 13 | }); 14 | 15 | it('should create the app', async(() => { 16 | let fixture = TestBed.createComponent(AppComponent); 17 | let app = fixture.debugElement.componentInstance; 18 | expect(app).toBeTruthy(); 19 | })); 20 | 21 | it(`should have as title 'app works!'`, async(() => { 22 | let fixture = TestBed.createComponent(AppComponent); 23 | let app = fixture.debugElement.componentInstance; 24 | expect(app.title).toEqual('app works!'); 25 | })); 26 | 27 | it('should render title in a h1 tag', async(() => { 28 | let fixture = TestBed.createComponent(AppComponent); 29 | fixture.detectChanges(); 30 | let compiled = fixture.debugElement.nativeElement; 31 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 32 | })); 33 | }); 34 | -------------------------------------------------------------------------------- /finances/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Angular 2 decorators and services 3 | */ 4 | import { Component } from '@angular/core'; 5 | import {Operation} from "./common/operation.model"; 6 | import {State, Store} from "@ngrx/store"; 7 | import {ADD_OPERATION, REMOVE_OPERATION, INCREMENT_OPERATION, DECREMENT_OPERATION} from "./common/operations"; 8 | 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | template: `
13 | 14 | 18 |
19 | 20 | ` 21 | }) 22 | export class AppComponent { 23 | 24 | public id:number = 0 ; //simulating IDs 25 | public operations:Array; 26 | 27 | 28 | constructor(private _store: Store) { 29 | this.operations = _store.select('operations') 30 | 31 | } 32 | 33 | 34 | addOperation(operation) { 35 | this._store.dispatch({type: ADD_OPERATION , payload: { 36 | id: ++ this.id,//simulating ID increments 37 | reason: operation.reason, 38 | amount: operation.amount 39 | }}); 40 | } 41 | 42 | incrementOperation(operation){ 43 | this._store.dispatch({type: INCREMENT_OPERATION, payload: operation}) 44 | } 45 | 46 | decrementOperation(operation) { 47 | this._store.dispatch({type: DECREMENT_OPERATION, payload: operation}) 48 | } 49 | 50 | 51 | deleteOperation(operation) { 52 | this._store.dispatch({type: REMOVE_OPERATION, payload: operation}) 53 | } 54 | 55 | 56 | 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /finances/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ApplicationRef } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import {StoreModule} from "@ngrx/store"; 9 | import {operationsReducer} from "./common/operations"; 10 | import {CommonModule} from "@angular/common"; 11 | import {NewOperation} from "./new-operation.component"; 12 | import {OperationsList} from "./operations-list.component"; 13 | 14 | 15 | @NgModule({ 16 | bootstrap: [ AppComponent ], 17 | declarations: [ 18 | AppComponent, 19 | NewOperation, 20 | OperationsList, 21 | ], 22 | imports: [ // import Angular's modules 23 | BrowserModule, 24 | CommonModule, 25 | FormsModule, 26 | HttpModule, 27 | StoreModule.provideStore({ operations: operationsReducer }), 28 | ], 29 | }) 30 | export class AppModule { 31 | constructor() {} 32 | 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /finances/src/app/common/operation.model.ts: -------------------------------------------------------------------------------- 1 | export class Operation { 2 | id:number; 3 | amount:number; 4 | reason:string; 5 | 6 | constructor() { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /finances/src/app/common/operations.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action, State} from '@ngrx/store'; 2 | import {Operation} from "./operation.model"; 3 | 4 | export const ADD_OPERATION = 'Add an operation'; 5 | export const REMOVE_OPERATION = 'Remove an operation'; 6 | export const INCREMENT_OPERATION = 'Increment an operation'; 7 | export const DECREMENT_OPERATION = 'Decrement an operation'; 8 | 9 | 10 | 11 | const initialState:State = []; 12 | 13 | 14 | export const operationsReducer: ActionReducer = (state = initialState, action: Action) => { 15 | switch (action.type) { 16 | case ADD_OPERATION: 17 | const operation:Operation = action.payload; 18 | return [ ...state, operation ]; 19 | 20 | case INCREMENT_OPERATION: 21 | const operation = ++action.payload.amount; 22 | return state.map(item => { 23 | return item.id === action.payload.id ? Object.assign({}, item, operation) : item; 24 | }); 25 | 26 | case DECREMENT_OPERATION: 27 | const operation = --action.payload.amount; 28 | return state.map(item => { 29 | return item.id === action.payload.id ? Object.assign({}, item, operation) : item; 30 | }); 31 | 32 | case REMOVE_OPERATION: 33 | return state.filter(operation => { 34 | return operation.id !== action.payload.id; 35 | }); 36 | 37 | 38 | default: 39 | return state; 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /finances/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /finances/src/app/new-operation.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, EventEmitter, ChangeDetectionStrategy} from '@angular/core'; 2 | import {Operation} from "./common/operation.model"; 3 | 4 | 5 | 6 | 7 | @Component({ 8 | selector: 'new-operation', 9 | templateUrl: './new-operation.template.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | 12 | }) 13 | 14 | export class NewOperation { 15 | 16 | public operation:Operation; 17 | constructor() { 18 | this.operation = new Operation(); 19 | } 20 | 21 | @Output() addOperation = new EventEmitter(); 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /finances/src/app/new-operation.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
$
7 | 8 |
.00
9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /finances/src/app/operations-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter, ChangeDetectionStrategy} from '@angular/core'; 2 | import {Operation} from "./common/operation.model"; 3 | 4 | 5 | 6 | 7 | @Component({ 8 | selector: 'operations-list', 9 | templateUrl: './operations-list.template.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | 12 | }) 13 | 14 | export class OperationsList { 15 | @Input() operations:Array; 16 | 17 | constructor() {} 18 | 19 | @Output() deleteOperation = new EventEmitter(); 20 | @Output() incrementOperation = new EventEmitter(); 21 | @Output() decrementOperation = new EventEmitter(); 22 | } 23 | -------------------------------------------------------------------------------- /finances/src/app/operations-list.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 |
  • 5 |

    $ {{operation.amount}}

    6 |

    Reason: {{operation.reason}}

    7 |
    8 | 9 | 10 | 11 |
    12 | 13 |
  • 14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /finances/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btroncone/ngrx-examples/b91f62d1c7f8f053420ee1dd61839332fe4e989e/finances/src/assets/.gitkeep -------------------------------------------------------------------------------- /finances/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /finances/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 | -------------------------------------------------------------------------------- /finances/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btroncone/ngrx-examples/b91f62d1c7f8f053420ee1dd61839332fe4e989e/finances/src/favicon.ico -------------------------------------------------------------------------------- /finances/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FinancesRedux 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /finances/src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './app/'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /finances/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /finances/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /finances/src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | 10 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 11 | declare var __karma__: any; 12 | declare var require: any; 13 | 14 | // Prevent Karma from running prematurely. 15 | __karma__.loaded = function () {}; 16 | 17 | 18 | Promise.all([ 19 | System.import('@angular/core/testing'), 20 | System.import('@angular/platform-browser-dynamic/testing') 21 | ]) 22 | // First, initialize the Angular testing environment. 23 | .then(([testing, testingBrowser]) => { 24 | testing.getTestBed().initTestEnvironment( 25 | testingBrowser.BrowserDynamicTestingModule, 26 | testingBrowser.platformBrowserDynamicTesting() 27 | ); 28 | }) 29 | // Then we find all the tests. 30 | .then(() => require.context('./', true, /\.spec\.ts/)) 31 | // And load the modules. 32 | .then(context => context.keys().map(context)) 33 | // Finally, start Karma to run the tests. 34 | .then(__karma__.start, __karma__.error); 35 | -------------------------------------------------------------------------------- /finances/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "dom"], 7 | "mapRoot": "./", 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "outDir": "../dist/out-tsc", 11 | "sourceMap": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "../node_modules/@types" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /finances/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, you can add your own global typings here 2 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 3 | 4 | declare var System: any; 5 | -------------------------------------------------------------------------------- /finances/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "label-undefined": true, 20 | "max-line-length": [ 21 | true, 22 | 140 23 | ], 24 | "member-access": false, 25 | "member-ordering": [ 26 | true, 27 | "static-before-instance", 28 | "variables-before-functions" 29 | ], 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | true, 34 | "debug", 35 | "info", 36 | "time", 37 | "timeEnd", 38 | "trace" 39 | ], 40 | "no-construct": true, 41 | "no-debugger": true, 42 | "no-duplicate-key": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-eval": true, 46 | "no-inferrable-types": true, 47 | "no-shadowed-variable": true, 48 | "no-string-literal": false, 49 | "no-switch-case-fall-through": true, 50 | "no-trailing-whitespace": true, 51 | "no-unused-expression": true, 52 | "no-unused-variable": true, 53 | "no-unreachable": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "radix": true, 69 | "semicolon": [ 70 | "always" 71 | ], 72 | "triple-equals": [ 73 | true, 74 | "allow-null-check" 75 | ], 76 | "typedef-whitespace": [ 77 | true, 78 | { 79 | "call-signature": "nospace", 80 | "index-signature": "nospace", 81 | "parameter": "nospace", 82 | "property-declaration": "nospace", 83 | "variable-declaration": "nospace" 84 | } 85 | ], 86 | "variable-name": false, 87 | "whitespace": [ 88 | true, 89 | "check-branch", 90 | "check-decl", 91 | "check-operator", 92 | "check-separator", 93 | "check-type" 94 | ], 95 | 96 | "directive-selector-prefix": [true, "app"], 97 | "component-selector-prefix": [true, "app"], 98 | "directive-selector-name": [true, "camelCase"], 99 | "component-selector-name": [true, "kebab-case"], 100 | "directive-selector-type": [true, "attribute"], 101 | "component-selector-type": [true, "element"], 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 | "templates-use-public": true, 112 | "invoke-injectable": true 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /shopping-cart/helpers.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var zlib = require('zlib'); 3 | var webpackMerge = require('webpack-merge'); 4 | var webpackDefaults = require('./webpack.default.config.js'); 5 | 6 | 7 | // Helper functions 8 | 9 | function defaults(config) { 10 | return webpackMerge(webpackDefaults, config); 11 | } 12 | 13 | function hasProcessFlag(flag) { 14 | return process.argv.join('').indexOf(flag) > -1; 15 | } 16 | 17 | function gzipMaxLevel(buffer, callback) { 18 | return zlib['gzip'](buffer, {level: 9}, callback) 19 | } 20 | 21 | function root(args) { 22 | args = Array.prototype.slice.call(arguments, 0); 23 | return path.join.apply(path, [__dirname].concat(args)); 24 | } 25 | 26 | function rootNode(args) { 27 | args = Array.prototype.slice.call(arguments, 0); 28 | return root.apply(path, ['node_modules'].concat(args)); 29 | } 30 | 31 | function prependExt(extensions, args) { 32 | args = args || []; 33 | if (!Array.isArray(args)) { args = [args] } 34 | return extensions.reduce(function(memo, val) { 35 | return memo.concat(val, args.map(function(prefix) { 36 | return prefix + val 37 | })); 38 | }, ['']); 39 | } 40 | 41 | exports.defaults = defaults; 42 | exports.hasProcessFlag = hasProcessFlag; 43 | exports.gzipMaxLevel = gzipMaxLevel; 44 | exports.root = root; 45 | exports.rootNode = rootNode; 46 | exports.prependExt = prependExt; 47 | exports.prepend = prependExt; 48 | -------------------------------------------------------------------------------- /shopping-cart/karma.conf.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | 3 | module.exports = function(config) { 4 | var testWebpackConfig = require('./webpack.test.config.js'); 5 | 6 | config.set({ 7 | 8 | // base path that will be used to resolve all patterns (e.g. files, exclude) 9 | basePath: '', 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | // list of files to exclude 16 | exclude: [ ], 17 | 18 | // list of files / patterns to load in the browser 19 | // we are building the test environment in ./spec-bundle.js 20 | // files: [ { pattern: './spec-bundle.js', watched: false } ], 21 | files: [ { pattern: './spec-bundle.js', watched: false } ], 22 | 23 | // preprocess matching files before serving them to the browser 24 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 25 | preprocessors: { './spec-bundle.js': ['webpack', 'sourcemap'] }, 26 | 27 | // Webpack Config at ./webpack.test.js 28 | webpack: testWebpackConfig, 29 | 30 | // Webpack please don't spam the console when running in karma! 31 | webpackServer: { noInfo: true }, 32 | 33 | reporters: [ 'mocha' ], 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | // enable / disable colors in the output (reporters and logs) 44 | colors: true, 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | // enable / disable watching file and executing tests whenever any file changes 51 | autoWatch: true, 52 | 53 | // start these browsers 54 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 55 | browsers: [ 56 | 'Chrome', 57 | // 'PhantomJS' 58 | ], 59 | 60 | // Continuous Integration mode 61 | // if true, Karma captures browsers, runs the tests and exits 62 | singleRun: false 63 | }); 64 | 65 | }; 66 | -------------------------------------------------------------------------------- /shopping-cart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-ngrx-shopping-cart", 3 | "version": "0.0.1", 4 | "description": "angular 2 ngrx shopping-cart example", 5 | "main": "", 6 | "scripts": { 7 | "build": "npm run webpack --colors --display-error-details --display-cached", 8 | "webpack": "webpack", 9 | "clean": "rimraf node_modules", 10 | "clean-install": "npm run clean && npm install", 11 | "clean-start": "npm run clean && npm start", 12 | "typings-install": "typings install", 13 | "postinstall": "npm run typings-install", 14 | "watch": "webpack --watch", 15 | "server": "npm run server:dev", 16 | "server:dev": "webpack-dev-server --progress --profile --colors --display-error-details --display-cached --content-base src/", 17 | "start": "npm run server:dev", 18 | "test": "karma start" 19 | }, 20 | "author": "thomas.sattlecker@gmail.com", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@ngrx/store": "^2.0.0", 24 | "@ngrx/core": "^1.0.0", 25 | "@ngrx/effects":"^1.0.1", 26 | "ngrx-store-logger": "^0.1.4", 27 | "@angular/common": "^2.0.0-rc.1", 28 | "@angular/compiler": "^2.0.0-rc.1", 29 | "@angular/upgrade": "^2.0.0-rc.1", 30 | "@angular/core": "^2.0.0-rc.1", 31 | "@angular/http": "^2.0.0-rc.1", 32 | "@angular/platform-browser": "^2.0.0-rc.1", 33 | "@angular/router": "^2.0.0-rc.1", 34 | "@angular/platform-browser-dynamic": "^2.0.0-rc.1", 35 | "core-js": "^2.1.5", 36 | "rxjs": "5.0.0-beta.6", 37 | "zone.js": "0.6.12" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/btroncone/ngrx-examples" 42 | }, 43 | "devDependencies": { 44 | "compression-webpack-plugin": "^0.3.0", 45 | "copy-webpack-plugin": "^1.1.1", 46 | "css-loader": "^0.23.1", 47 | "es6-promise": "^3.1.2", 48 | "es6-promise-loader": "^1.0.1", 49 | "es6-shim": "^0.35.0", 50 | "es7-reflect-metadata": "^1.6.0", 51 | "exports-loader": "^0.6.2", 52 | "expose-loader": "^0.7.1", 53 | "file-loader": "^0.8.5", 54 | "html-webpack-plugin": "^1.7.0", 55 | "http-server": "^0.8.5", 56 | "imports-loader": "^0.6.5", 57 | "istanbul": "^0.4.2", 58 | "istanbul-instrumenter-loader": "^0.1.3", 59 | "jasmine-core": "^2.4.1", 60 | "json-loader": "^0.5.4", 61 | "karma": "^0.13.22", 62 | "karma-chrome-launcher": "^0.2.2", 63 | "karma-coverage": "^0.5.5", 64 | "karma-jasmine": "^0.3.8", 65 | "karma-mocha-reporter": "^2.0.0", 66 | "karma-phantomjs-launcher": "^1.0.0", 67 | "karma-sourcemap-loader": "^0.3.7", 68 | "karma-webpack": "^1.7.0", 69 | "ncp": "^2.0.0", 70 | "phantomjs-polyfill": "0.0.1", 71 | "phantomjs-prebuilt": "^2.1.3", 72 | "raw-loader": "0.5.1", 73 | "reflect-metadata": "0.1.2", 74 | "remap-istanbul": "^0.5.1", 75 | "rimraf": "^2.5.1", 76 | "source-map-loader": "^0.1.5", 77 | "style-loader": "^0.13.0", 78 | "ts-helper": "0.0.1", 79 | "ts-loader": "0.8.1", 80 | "ts-node": "^0.5.5", 81 | "tsconfig-lint": "^0.5.0", 82 | "tsd": "^0.6.5", 83 | "typedoc": "^0.3.12", 84 | "typescript": "~1.8.9", 85 | "url-loader": "^0.5.7", 86 | "wallaby-webpack": "0.0.11", 87 | "webpack": "^1.12.12", 88 | "webpack-dev-server": "^1.14.1", 89 | "webpack-md5-hash": "0.0.4", 90 | "webpack-merge": "^0.8.4" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /shopping-cart/spec-bundle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * When testing with webpack and ES6, we have to do some extra 3 | * things get testing to work right. Because we are gonna write test 4 | * in ES6 to, we have to compile those as well. That's handled in 5 | * karma.conf.js with the karma-webpack plugin. This is the entry 6 | * file for webpack test. Just like webpack will create a bundle.js 7 | * file for our client, when we run test, it well compile and bundle them 8 | * all here! Crazy huh. So we need to do some setup 9 | */ 10 | Error.stackTraceLimit = Infinity; 11 | require('phantomjs-polyfill'); 12 | require('es6-promise'); 13 | require('es6-shim'); 14 | require('es7-reflect-metadata/dist/browser'); 15 | require('core-js'); 16 | 17 | require('zone.js/dist/zone'); 18 | require('zone.js/dist/long-stack-trace-zone'); 19 | require('zone.js/dist/jasmine-patch'); 20 | 21 | globalPolyfills() 22 | 23 | 24 | var testing = require('@angular/core/testing'); 25 | var browser = require('@angular/platform-browser-dynamic/testing'); 26 | testing.setBaseTestProviders( 27 | browser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, 28 | browser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); 29 | /* 30 | Ok, this is kinda crazy. We can use the the context method on 31 | require that webpack created in order to tell webpack 32 | what files we actually want to require or import. 33 | Below, context will be an function/object with file names as keys. 34 | using that regex we are saying look in client/app and find 35 | any file that ends with spec.js and get its path. By passing in true 36 | we say do this recursively 37 | */ 38 | var appContext = require.context('./src', true, /\.spec\.ts/); 39 | 40 | // get all the files, for each file, call the context function 41 | // that will require the file and load it up here. Context will 42 | // loop and require those spec files here 43 | appContext.keys().forEach(appContext); 44 | 45 | 46 | 47 | 48 | // these are helpers that typescript uses 49 | // I manually added them by opting out of EmitHelpers by noEmitHelpers: false 50 | function globalPolyfills(){ 51 | global.__extends = (this && this.__extends) || function (d, b) { 52 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 53 | var __ = function() { this.constructor = d; }; 54 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 55 | }; 56 | 57 | global.__decorate = global.Reflect.decorate; 58 | global.__metadata = global.Reflect.metadata; 59 | 60 | global.__param = (this && this.__param) || function (paramIndex, decorator) { 61 | return function (target, key) { decorator(target, key, paramIndex); }; 62 | }; 63 | 64 | global.__awaiter = (this && this.__awaiter) || 65 | function (thisArg, _arguments, Promise, generator) { 66 | return new Promise(function (resolve, reject) { 67 | generator = generator.call(thisArg, _arguments); 68 | function cast(value) { 69 | return value instanceof Promise && value.constructor === Promise ? 70 | value : new Promise(function (resolve) { resolve(value); }); } 71 | function onfulfill(value) { try { step('next', value); } catch (e) { reject(e); } } 72 | function onreject(value) { try { step('throw', value); } catch (e) { reject(e); } } 73 | function step(verb, value) { 74 | var result = generator[verb](value); 75 | result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject); 76 | } 77 | step('next', void 0); 78 | }); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /shopping-cart/src/api/productsJSON.ts: -------------------------------------------------------------------------------- 1 | export const jsonProducts = [ 2 | {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2}, 3 | {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10}, 4 | {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5} 5 | ] 6 | -------------------------------------------------------------------------------- /shopping-cart/src/api/shop.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | import {jsonProducts} from './productsJSON'; 5 | import { Observable } from 'rxjs/Observable'; 6 | 7 | const TIMEOUT = 100; 8 | 9 | export default { 10 | getProducts(timeout) { 11 | return Observable.of(jsonProducts) 12 | .delay(timeout || TIMEOUT); 13 | }, 14 | 15 | buyProducts(payload, timeout) { 16 | return Observable.timer(timeout || TIMEOUT); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shopping-cart/src/app/actions/cart.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import {CHECKOUT_REQUEST} from '../reducers/cart'; 3 | 4 | export const checkout = (products: [number]) => { 5 | return { type: CHECKOUT_REQUEST, payload: products }; 6 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/actions/products.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import {ADD_TO_CART, REQUEST_PRODUCTS, IProduct} from '../reducers/products'; 3 | 4 | 5 | export const getProducts = () => { 6 | return { type: REQUEST_PRODUCTS }; 7 | } 8 | 9 | export const addToCart = (product: IProduct) => { 10 | return { type: ADD_TO_CART, payload: product.id }; 11 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '@angular/platform-browser-dynamic'; 2 | import { ELEMENT_PROBE_PROVIDERS } from '@angular/platform-browser/index'; 3 | import { ShoppingCartApp } from './shoppingCart-app'; 4 | import { provideStore } from '@ngrx/store'; 5 | import reducer from './reducers'; 6 | import effects from './effects'; 7 | import { runEffects } from '@ngrx/effects'; 8 | 9 | 10 | export function main() { 11 | return bootstrap(ShoppingCartApp, [ 12 | ELEMENT_PROBE_PROVIDERS, 13 | provideStore(reducer), 14 | runEffects(effects), 15 | ]) 16 | .catch(err => console.error(err)); 17 | } 18 | 19 | document.addEventListener('DOMContentLoaded', main); -------------------------------------------------------------------------------- /shopping-cart/src/app/components/cart-item.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'cart-item', 5 | template: ` 6 |
  • 7 | {{cartItem.title}} - \${{cartItem.price}} x {{cartItem.quantity}} 8 |
  • 9 | `, 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class CartItem { 13 | @Input() cartItem: any; 14 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/components/cart-list.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input, Output, EventEmitter} from '@angular/core'; 2 | import {CartItem} from './cart-item'; 3 | 4 | @Component({ 5 | selector: 'cart-list', 6 | template: ` 7 | Cart 8 |
      9 | 12 | 13 |
    14 | 18 | `, 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | directives: [CartItem] 21 | }) 22 | export class CartList { 23 | @Input() cartList: any; 24 | @Output() checkout = new EventEmitter(); 25 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/components/product-item.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Output, Input, EventEmitter} from '@angular/core'; 2 | import {IProduct} from '../reducers/products'; 3 | 4 | @Component({ 5 | selector: 'product-item', 6 | template: ` 7 |
  • 8 |
    {{product.title}} - {{product.price}}
    9 | 14 |
  • 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class ProductItem { 19 | @Input() product: IProduct; 20 | @Output() addToCart: EventEmitter = new EventEmitter(); 21 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/components/product-list.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input, Output, EventEmitter} from '@angular/core'; 2 | 3 | import {ProductItem} from './product-item'; 4 | import {IProduct} from '../reducers/products'; 5 | 6 | @Component({ 7 | selector: 'product-list', 8 | template: ` 9 | Products 10 |
      11 | 15 | 16 |
    17 | 18 | `, 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | directives: [ProductItem] 21 | }) 22 | export class ProductList { 23 | @Input() products: IProduct[]; 24 | @Output() addToCart = new EventEmitter(); 25 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/effects/index.ts: -------------------------------------------------------------------------------- 1 | import { ShopEffects } from './shop'; 2 | 3 | export default [ 4 | ShopEffects 5 | ]; 6 | -------------------------------------------------------------------------------- /shopping-cart/src/app/effects/shop.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../test_harness'; 2 | import {Injector, Provider, ReflectiveInjector} from '@angular/core'; 3 | import {Observable} from 'rxjs/Observable'; 4 | import {provideStore, Store, Dispatcher} from '@ngrx/store'; 5 | import { 6 | MOCK_EFFECTS_PROVIDERS, 7 | MockStateUpdates 8 | } from '@ngrx/effects/testing'; 9 | 10 | import productsReducer, * as fromProducts from '../reducers/products'; 11 | import {CHECKOUT_REQUEST, CHECKOUT_SUCCESS} from '../reducers/cart'; 12 | import {jsonProducts} from '../../api/productsJSON'; 13 | 14 | import {ShopEffects} from './shop'; 15 | 16 | 17 | describe('Shop Effect LOAD', () => { 18 | let shop: ShopEffects; 19 | let updates$: MockStateUpdates; 20 | 21 | beforeEach(function () { 22 | const injector = ReflectiveInjector.resolveAndCreate([ 23 | ShopEffects, 24 | MOCK_EFFECTS_PROVIDERS, 25 | // Mock out other dependencies (like Http) here 26 | ]); 27 | 28 | shop = injector.get(ShopEffects); 29 | updates$ = injector.get(MockStateUpdates); 30 | }); 31 | 32 | it('should dispatch products list', (done) => { 33 | 34 | updates$.sendAction({ type: fromProducts.REQUEST_PRODUCTS }); 35 | 36 | shop.load$ 37 | .filter(Boolean) 38 | .subscribe(last => { 39 | expect(last).toEqual({ type: fromProducts.RECEIVED_PRODUCTS, payload: jsonProducts }); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should checkout products', (done) => { 45 | 46 | updates$.sendAction({ type: CHECKOUT_REQUEST, payload: [0, 1] }); 47 | 48 | shop.checkout$ 49 | .filter(Boolean) 50 | .subscribe(last => { 51 | expect(last).toEqual({ type: CHECKOUT_SUCCESS, payload: 0 }); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /shopping-cart/src/app/effects/shop.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { StateUpdates, Effect } from '@ngrx/effects' 3 | import 'rxjs'; 4 | 5 | import {REQUEST_PRODUCTS, RECEIVED_PRODUCTS} from '../reducers/products'; 6 | import {CHECKOUT_REQUEST, CHECKOUT_SUCCESS} from '../reducers/cart'; 7 | import * as shop from '../../api/shop'; 8 | 9 | @Injectable() 10 | export class ShopEffects { 11 | constructor(private updates$: StateUpdates) { } 12 | 13 | @Effect() load$ = this.updates$ 14 | .whenAction(REQUEST_PRODUCTS) 15 | .map(update => JSON.stringify(update.action.payload)) 16 | .switchMap(() => shop.default.getProducts(300)) 17 | .map(res => { 18 | return { 19 | type: RECEIVED_PRODUCTS, 20 | payload: res 21 | }; 22 | }); 23 | 24 | @Effect() checkout$ = this.updates$ 25 | .whenAction(CHECKOUT_REQUEST) 26 | .map(update => JSON.stringify(update.action.payload)) 27 | .switchMap(payload => shop.default.buyProducts(payload, 300)) 28 | .map(res => { 29 | return { 30 | type: CHECKOUT_SUCCESS, 31 | payload: res 32 | }; 33 | }); 34 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/reducers/cart.spec.ts: -------------------------------------------------------------------------------- 1 | import {ADD_TO_CART} from './products'; 2 | import cartReducer, * as fromCart from './cart'; 3 | 4 | declare var it, expect, describe, toBe; 5 | 6 | describe('The cart reducer', () => { 7 | it('should return current state when no valid actions have been made', () => { 8 | const state = { productIds: [], quantityById: [] }; 9 | const actual = cartReducer(state, { type: 'INVALID_ACTION', payload: {} }); 10 | const expected = state; 11 | expect(actual).toBe(expected); 12 | }); 13 | 14 | it('should initialize quantity in cart when ADD_TO_CART is dispatched', () => { 15 | const state = { productIds: [], quantityById: [] } 16 | const actual = cartReducer(state, { type: ADD_TO_CART, payload: 1 }); 17 | const expected = state; 18 | expect(1).toBe(actual.quantityById[1]); 19 | expect(1).toBe(actual.productIds[0]); 20 | }); 21 | 22 | it('should increase quantity in cart when ADD_TO_CART is dispatched', () => { 23 | const state = { productIds: [2], quantityById: { 2: 1 } } 24 | const actual = cartReducer(state, { type: ADD_TO_CART, payload: 2 }); 25 | const expected = state; 26 | expect(state.quantityById[2] + 1).toBe(actual.quantityById[2]); 27 | expect(2).toBe(actual.productIds[0]); 28 | }); 29 | 30 | it('should return initial cart when CHECKOUT_SUCCESS is dispatched', () => { 31 | const state = { productIds: [2], quantityById: { 2: 1 } } 32 | const actual = cartReducer(state, { type: fromCart.CHECKOUT_SUCCESS }); 33 | const expected = { productIds: [], quantityById: {} }; 34 | expect(actual).toEqual(expected); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /shopping-cart/src/app/reducers/cart.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import {ADD_TO_CART} from './products'; 3 | 4 | export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST' 5 | export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS' 6 | export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE' 7 | 8 | export interface CartState { 9 | productIds: any[]; 10 | quantityById: any; 11 | } 12 | 13 | const initialState: CartState = { 14 | productIds: [], quantityById: {} 15 | } 16 | 17 | export default function (state = initialState, action: Action): CartState { 18 | switch (action.type) { 19 | case ADD_TO_CART: 20 | if (state.productIds.indexOf(action.payload) !== -1) { 21 | return Object.assign({}, 22 | state, 23 | { 24 | quantityById: 25 | Object.assign({}, state.quantityById, 26 | { [action.payload]: (state.quantityById[action.payload] || 0) + 1 } 27 | ) 28 | } 29 | ); 30 | } 31 | return Object.assign({}, 32 | state, 33 | { 34 | productIds: [...state.productIds, action.payload], 35 | quantityById: 36 | Object.assign({}, state.quantityById, 37 | { [action.payload]: (state.quantityById[action.payload] || 0) + 1 } 38 | ) 39 | } 40 | ); 41 | case CHECKOUT_SUCCESS: 42 | return initialState; 43 | default: 44 | return state; 45 | } 46 | }; -------------------------------------------------------------------------------- /shopping-cart/src/app/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import '@ngrx/core/add/operator/select'; 2 | import 'rxjs/add/operator/switchMap'; 3 | import 'rxjs/add/operator/let'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { compose } from '@ngrx/core/compose'; 7 | import { storeLogger } from 'ngrx-store-logger'; 8 | import { combineReducers } from '@ngrx/store'; 9 | 10 | import cartReducer, * as fromCart from './cart'; 11 | import productsReducer, * as fromProducts from './products'; 12 | 13 | export interface AppState { 14 | cart: fromCart.CartState; 15 | products: fromProducts.ProductsState; 16 | } 17 | 18 | export default compose(storeLogger(), combineReducers)({ 19 | cart: cartReducer, 20 | products: productsReducer, 21 | }); 22 | 23 | 24 | export function getCartState() { 25 | return (state$: Observable) => state$ 26 | .select(s => s.cart); 27 | } 28 | 29 | export function getProductState() { 30 | return (state$: Observable) => state$ 31 | .select(s => s.products); 32 | } 33 | 34 | 35 | export function getProductEntities() { 36 | return compose(fromProducts.getProductEntities(), getProductState()); 37 | } 38 | 39 | export function getProductsAsArry() { 40 | return compose(fromProducts.getProductsAsArry(), getProductState()); 41 | } 42 | 43 | export function getCalculatedCartList() { 44 | return (state$: Observable) => { 45 | return Observable 46 | .combineLatest(state$.let(getCartState()), state$.let(getProductEntities())) 47 | .map(([cart, products]: any[]) => { 48 | return cart.productIds.map(productId => { 49 | return { 50 | title: products[productId].title, 51 | price: products[productId].price, 52 | quantity: cart.quantityById[productId] 53 | }; 54 | }); 55 | }); 56 | }; 57 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/reducers/products.spec.ts: -------------------------------------------------------------------------------- 1 | import productsReducer, * as fromProducts from './products'; 2 | import {jsonProducts} from '../../api/productsJSON'; 3 | 4 | declare var it, expect, describe, toBe; 5 | 6 | describe('The products reducer', () => { 7 | it('should return current state when no valid actions have been made', () => { 8 | const state = null; 9 | const actual = productsReducer(state, { type: 'INVALID_ACTION', payload: {} }); 10 | const expected = state; 11 | expect(actual).toBe(expected); 12 | }); 13 | 14 | it('should return received products when RECEIVED_PRODUCTS is dispatched', () => { 15 | const state = null; 16 | const actual = productsReducer(state, { type: fromProducts.RECEIVED_PRODUCTS, payload: jsonProducts }); 17 | const expected = { 18 | entities: jsonProducts.reduce((obj, product: fromProducts.IProduct) => { 19 | obj[product.id] = product; 20 | return obj; 21 | }, {}) 22 | }; 23 | expect(actual).toEqual(expected); 24 | }); 25 | 26 | it('should decrease inventory when ADD_TO_CART is dispatched', () => { 27 | const state = { 28 | entities: jsonProducts.reduce((obj, product: fromProducts.IProduct) => { 29 | obj[product.id] = product; 30 | return obj; 31 | }, {}) 32 | }; 33 | const actual = productsReducer(state, { type: fromProducts.ADD_TO_CART, payload: 1 }); 34 | const expected = state; 35 | expect(state.entities[1].inventory - 1).toBe(actual.entities[1].inventory); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /shopping-cart/src/app/reducers/products.ts: -------------------------------------------------------------------------------- 1 | import '@ngrx/core/add/operator/select'; 2 | import { Action } from '@ngrx/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | export const ADD_TO_CART = 'ADD_TO_CART'; 6 | export const REQUEST_PRODUCTS = 'REQUEST_PRODUCTS'; 7 | export const RECEIVED_PRODUCTS = 'RECEIVED_PRODUCTS'; 8 | 9 | 10 | export interface IProduct { 11 | id: number; 12 | title: string; 13 | price: number; 14 | inventory: number; 15 | } 16 | 17 | export interface ProductsState { 18 | entities: { [id: string]: IProduct }; 19 | } 20 | 21 | const initialState: ProductsState = { 22 | entities: {} 23 | }; 24 | 25 | export default function (state = initialState, action: Action): ProductsState { 26 | switch (action.type) { 27 | case RECEIVED_PRODUCTS: 28 | return { 29 | entities: Object.assign({}, 30 | state.entities, 31 | action.payload.reduce((obj, product) => { 32 | obj[product.id] = product; 33 | return obj; 34 | }, {}) 35 | ) 36 | }; 37 | case ADD_TO_CART: 38 | return { 39 | entities: Object.assign({}, state.entities, { 40 | [action.payload]: Object.assign({}, state.entities[action.payload], { 41 | inventory: state.entities[action.payload].inventory - 1 42 | }) 43 | }) 44 | }; 45 | default: 46 | return state; 47 | } 48 | }; 49 | 50 | export function getProductEntities() { 51 | return (state$: Observable) => state$ 52 | .select(s => s.entities); 53 | } 54 | 55 | export function getProductsAsArry() { 56 | return (state$: Observable) => state$ 57 | .let(getProductEntities()) 58 | .map(res => Object.keys(res).map(key => res[key])); 59 | } -------------------------------------------------------------------------------- /shopping-cart/src/app/shoppingCart-app.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {ProductList} from './components/product-list'; 3 | import {CartList} from './components/cart-list'; 4 | 5 | import {getProducts, addToCart} from './actions/products'; 6 | import {checkout} from './actions/cart'; 7 | 8 | import {getProductsAsArry, getCalculatedCartList} from './reducers'; 9 | import { Subject } from 'rxjs'; 10 | import {Store, Action} from '@ngrx/store'; 11 | 12 | @Component({ 13 | selector: `shopping-cart-app`, 14 | template: ` 15 |
    16 | 22 |
    23 | 26 | 27 | 30 | 31 |
    32 |
    33 | `, 34 | directives: [ProductList, CartList], 35 | changeDetection: ChangeDetectionStrategy.OnPush 36 | }) 37 | export class ShoppingCartApp { 38 | 39 | cartList: any; 40 | products: any; 41 | actions$ = new Subject(); 42 | 43 | addToCartAction = addToCart; 44 | checkoutAction = checkout; 45 | 46 | constructor(public store: Store) { 47 | this.products = store.let(getProductsAsArry()); 48 | this.cartList = store.let(getCalculatedCartList()); 49 | 50 | this.actions$.subscribe(store); 51 | this.actions$.next(getProducts()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /shopping-cart/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%= o.webpackConfig.metadata.title %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 19 | 20 | {% } %} 21 | 22 | 23 | 24 | 25 | Loading... 26 | 27 | {% if (o.webpackConfig.metadata.ENV === 'development') { %} 28 | 29 | 30 | {% } %} 31 | 32 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 33 | 34 | {% } %} 35 | 36 | -------------------------------------------------------------------------------- /shopping-cart/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js'; 2 | import 'rxjs'; 3 | require('zone.js/dist/zone'); 4 | require('zone.js/dist/long-stack-trace-zone'); -------------------------------------------------------------------------------- /shopping-cart/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | a:hover, 12 | a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | h3 { 17 | font-weight: 100; 18 | } 19 | 20 | /* LAYOUT CSS */ 21 | .pure-img-responsive { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | #layout { 27 | padding: 0; 28 | } 29 | 30 | .header { 31 | text-align: center; 32 | top: auto; 33 | margin: 3em auto; 34 | } 35 | 36 | .sidebar { 37 | background: rgb(61, 79, 93); 38 | color: #fff; 39 | } 40 | 41 | .brand-title, 42 | .brand-tagline { 43 | margin: 0; 44 | } 45 | .brand-title { 46 | text-transform: uppercase; 47 | } 48 | .brand-tagline { 49 | font-weight: 300; 50 | color: rgb(176, 202, 219); 51 | } 52 | 53 | .nav-list { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | .nav-item { 59 | display: inline-block; 60 | *display: inline; 61 | zoom: 1; 62 | } 63 | .nav-item a { 64 | background: transparent; 65 | border: 2px solid rgb(176, 202, 219); 66 | color: #fff; 67 | margin-top: 1em; 68 | letter-spacing: 0.05em; 69 | text-transform: uppercase; 70 | font-size: 85%; 71 | } 72 | .nav-item a:hover, 73 | .nav-item a:focus { 74 | border: 2px solid rgb(61, 146, 201); 75 | text-decoration: none; 76 | } 77 | 78 | .content-subhead { 79 | text-transform: uppercase; 80 | color: #aaa; 81 | border-bottom: 1px solid #eee; 82 | padding: 0.4em 0; 83 | font-size: 80%; 84 | font-weight: 500; 85 | letter-spacing: 0.1em; 86 | } 87 | 88 | .content { 89 | padding: 2em 1em 0; 90 | } 91 | 92 | .post { 93 | padding-bottom: 2em; 94 | } 95 | .post-title { 96 | font-size: 2em; 97 | color: #222; 98 | margin-bottom: 0.2em; 99 | } 100 | .post-avatar { 101 | border-radius: 50px; 102 | float: right; 103 | margin-left: 1em; 104 | } 105 | .post-description { 106 | font-family: Georgia, "Cambria", serif; 107 | color: #444; 108 | line-height: 1.8em; 109 | } 110 | .post-meta { 111 | color: #999; 112 | font-size: 90%; 113 | margin: 0; 114 | } 115 | 116 | .post-category { 117 | margin: 0 0.1em; 118 | padding: 0.3em 1em; 119 | color: #fff; 120 | background: #999; 121 | font-size: 80%; 122 | } 123 | .post-category-design { 124 | background: #5aba59; 125 | } 126 | .post-category-pure { 127 | background: #4d85d1; 128 | } 129 | .post-category-yui { 130 | background: #8156a7; 131 | } 132 | .post-category-js { 133 | background: #df2d4f; 134 | } 135 | 136 | .post-images { 137 | margin: 1em 0; 138 | } 139 | .post-image-meta { 140 | margin-top: -3.5em; 141 | margin-left: 1em; 142 | color: #fff; 143 | text-shadow: 0 1px 1px #333; 144 | } 145 | 146 | .footer { 147 | text-align: center; 148 | padding: 1em 0; 149 | } 150 | .footer a { 151 | color: #ccc; 152 | font-size: 80%; 153 | } 154 | .footer .pure-menu a:hover, 155 | .footer .pure-menu a:focus { 156 | background: none; 157 | } 158 | 159 | @media (min-width: 48em) { 160 | .content { 161 | padding: 2em 3em 0; 162 | margin-left: 25%; 163 | } 164 | 165 | .header { 166 | margin: 80% 2em 0; 167 | text-align: right; 168 | } 169 | 170 | .sidebar { 171 | position: fixed; 172 | top: 0; 173 | bottom: 0; 174 | } 175 | } 176 | 177 | .complete{ 178 | text-decoration: line-through; 179 | } 180 | 181 | .margin-t-20{ 182 | margin-top:20px; 183 | } 184 | -------------------------------------------------------------------------------- /shopping-cart/src/test_harness.ts: -------------------------------------------------------------------------------- 1 | require('core-js'); 2 | require('reflect-metadata'); 3 | import 'rxjs/add/operator/do'; 4 | import 'rxjs/add/observable/empty'; 5 | import 'rxjs/add/operator/filter'; 6 | import 'rxjs/add/operator/take'; 7 | import 'rxjs/add/operator/mergeMap'; 8 | import 'rxjs/add/operator/delay'; 9 | import 'rxjs/add/observable/timer'; 10 | import 'rxjs/add/operator/toArray'; 11 | import 'rxjs/add/operator/select'; 12 | import '@ngrx/core/add/operator/select'; -------------------------------------------------------------------------------- /shopping-cart/src/vendor.ts: -------------------------------------------------------------------------------- 1 | import '@angular/platform-browser'; 2 | import '@angular/platform-browser-dynamic'; 3 | import '@angular/core'; 4 | import '@angular/common'; 5 | import '@angular/http'; 6 | import '@angular/router-deprecated'; 7 | import 'rxjs'; -------------------------------------------------------------------------------- /shopping-cart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true 9 | }, 10 | "exclude":[ 11 | "node_modules", 12 | "typings/main.d.ts", 13 | "typings/main" 14 | ], 15 | "filesGlob": [ 16 | "./src/**/*.ts", 17 | "!./node_modules/**/*.ts", 18 | "typings/browser.d.ts" 19 | ], 20 | "compileOnSave": false, 21 | "buildOnSave": false 22 | } -------------------------------------------------------------------------------- /shopping-cart/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-var-keyword": true, 17 | "no-unused-variable": true, 18 | "one-line": [ 19 | true, 20 | "check-open-brace", 21 | "check-whitespace" 22 | ], 23 | "quotemark": [ 24 | true, 25 | "single" 26 | ], 27 | "semicolon": true, 28 | "triple-equals": [ 29 | true, 30 | "allow-null-check" 31 | ], 32 | "typedef-whitespace": [ 33 | true, 34 | { 35 | "call-signature": "nospace", 36 | "index-signature": "nospace", 37 | "parameter": "nospace", 38 | "property-declaration": "nospace", 39 | "variable-declaration": "nospace" 40 | } 41 | ], 42 | "variable-name": [ 43 | true, 44 | "ban-keywords", 45 | "allow-leading-underscore" 46 | ], 47 | "whitespace": [ 48 | true, 49 | "check-branch", 50 | "check-decl", 51 | "check-operator", 52 | "check-separator", 53 | "check-type" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /shopping-cart/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "es6-promise": "github:typed-typings/npm-es6-promise#fb04188767acfec1defd054fc8024fafa5cd4de7" 4 | }, 5 | "devDependencies": {}, 6 | "ambientDependencies": { 7 | "angular-protractor": "github:DefinitelyTyped/DefinitelyTyped/angular-protractor/angular-protractor.d.ts#64b25f63f0ec821040a5d3e049a976865062ed9d", 8 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2", 9 | "hammerjs": "github:DefinitelyTyped/DefinitelyTyped/hammerjs/hammerjs.d.ts#74a4dfc1bc2dfadec47b8aae953b28546cb9c6b7", 10 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c", 11 | "ng2": "github:gdi2290/typings-ng2/ng2.d.ts#32998ff5584c0eab0cd9dc7704abb1c5c450701c", 12 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#8cf8164641be73e8f1e652c2a5b967c7210b6729", 13 | "selenium-webdriver": "github:DefinitelyTyped/DefinitelyTyped/selenium-webdriver/selenium-webdriver.d.ts#a83677ed13add14c2ab06c7325d182d0ba2784ea", 14 | "webpack": "github:DefinitelyTyped/DefinitelyTyped/webpack/webpack.d.ts#95c02169ba8fa58ac1092422efbd2e3174a206f4", 15 | "zone.js": "github:DefinitelyTyped/DefinitelyTyped/zone.js/zone.js.d.ts#c393f8974d44840a6c9cc6d5b5c0188a8f05143d" 16 | } 17 | } -------------------------------------------------------------------------------- /shopping-cart/wallaby.js: -------------------------------------------------------------------------------- 1 | var wallabyWebpack = require('wallaby-webpack'); 2 | 3 | var webpackPostprocessor = wallabyWebpack({ 4 | entryPatterns: [ 5 | 'spec-bundle.js', 6 | 'src/**/*spec.js' 7 | ] 8 | }); 9 | 10 | module.exports = function(w) { 11 | 12 | return { 13 | files: [ 14 | { pattern: 'spec-bundle.js', load: false }, 15 | { pattern: 'src/**/*.ts', load: false }, 16 | { pattern: 'src/**/*spec.ts', ignore: true } 17 | ], 18 | 19 | tests: [ 20 | { pattern: 'src/**/*spec.ts', load: false } 21 | ], 22 | 23 | testFramework: "jasmine", 24 | 25 | compilers: { 26 | '**/*.ts': w.compilers.typeScript({ 27 | emitDecoratorMetadata: true, 28 | experimentalDecorators: true 29 | }) 30 | }, 31 | 32 | postprocessor: webpackPostprocessor, 33 | 34 | bootstrap: function() { 35 | window.__moduleBundler.loadTests(); 36 | } 37 | }; 38 | }; -------------------------------------------------------------------------------- /shopping-cart/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack'); 3 | var helpers = require('./helpers'); 4 | 5 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | var ProvidePlugin = require('webpack/lib/ProvidePlugin'); 8 | 9 | var ENV = process.env.ENV = process.env.NODE_ENV = 'development'; 10 | var HMR = helpers.hasProcessFlag('hot'); 11 | const autoprefixer = require('autoprefixer'); 12 | 13 | var metadata = { 14 | title: 'NgRx Shopping Cart Example', 15 | baseUrl: '/', 16 | host: 'localhost', 17 | port: 3000, 18 | ENV: ENV, 19 | HMR: HMR 20 | }; 21 | /* 22 | * Config 23 | * with default values at webpack.default.conf 24 | */ 25 | module.exports = helpers.defaults({ 26 | // static data for index.html 27 | metadata: metadata, 28 | // devtool: 'eval' // for faster builds use 'eval' 29 | 30 | // our angular app 31 | entry: { 'polyfills': './src/polyfills.ts', 'main': './src/app/bootstrap.ts' }, 32 | 33 | // Config for our build files 34 | output: { 35 | path: helpers.root('dist') 36 | }, 37 | 38 | module: { 39 | preLoaders: [ 40 | { test: /\.js$/, loader: "source-map-loader", exclude: [ helpers.root('node_modules/rxjs') ] } 41 | ], 42 | loaders: [ 43 | // Support for .ts files. 44 | { test: /\.ts$/, loader: 'ts-loader', exclude: [ /\.(spec|e2e)\.ts$/ ] }, 45 | 46 | // Support for *.json files. 47 | { test: /\.json$/, loader: 'json-loader' }, 48 | 49 | // Support for CSS as raw text 50 | { test: /\.css$/, loader: 'raw-loader' }, 51 | 52 | // support for .html as raw text 53 | { test: /\.html$/, loader: 'raw-loader', exclude: [ helpers.root('src/index.html') ] } 54 | 55 | ] 56 | }, 57 | 58 | plugins: [ 59 | new webpack.optimize.OccurenceOrderPlugin(true), 60 | new webpack.optimize.CommonsChunkPlugin({ name: 'polyfills', filename: 'polyfills.bundle.js', minChunks: Infinity }), 61 | // static assets 62 | new CopyWebpackPlugin([ { from: 'src/assets', to: 'assets' } ]), 63 | // generating html 64 | new HtmlWebpackPlugin({ template: 'src/index.html' }), 65 | // replace 66 | new webpack.DefinePlugin({ 67 | 'process.env': { 68 | 'ENV': JSON.stringify(metadata.ENV), 69 | 'NODE_ENV': JSON.stringify(metadata.ENV), 70 | 'HMR': HMR 71 | } 72 | }), 73 | new ProvidePlugin({ 74 | jQuery: 'jquery', 75 | $: 'jquery', 76 | jquery: 'jquery' 77 | }) 78 | ], 79 | 80 | // Other module loader config 81 | 82 | // our Webpack Development Server config 83 | devServer: { 84 | port: metadata.port, 85 | host: metadata.host 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /shopping-cart/webpack.default.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | stats: { colors: true, reasons: true }, 6 | resolve: { 7 | extensions: ['', '.ts', '.js'] 8 | }, 9 | debug: true, 10 | output: { 11 | filename: '[name].bundle.js', 12 | sourceMapFilename: '[name].map', 13 | chunkFilename: '[id].chunk.js' 14 | }, 15 | module: { 16 | noParse: [ 17 | path.join(__dirname, 'zone.js', 'dist'), 18 | path.join(__dirname, 'angular2', 'bundles') 19 | ] 20 | }, 21 | node: { 22 | global: 'window', 23 | progress: false, 24 | crypto: 'empty', 25 | module: false, 26 | clearImmediate: false, 27 | setImmediate: false 28 | }, 29 | tslint: { 30 | emitErrors: false, 31 | failOnHint: false, 32 | resourcePath: 'src', 33 | }, 34 | devServer: { 35 | historyApiFallback: true, 36 | watchOptions: { 37 | aggregateTimeout: 300, 38 | poll: 1000 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /shopping-cart/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | var helpers = require('./helpers'); 2 | 3 | var ProvidePlugin = require('webpack/lib/ProvidePlugin'); 4 | var DefinePlugin = require('webpack/lib/DefinePlugin'); 5 | var ENV = process.env.ENV = process.env.NODE_ENV = 'test'; 6 | 7 | 8 | module.exports = helpers.defaults({ 9 | devtool: 'inline-source-map', 10 | module: { 11 | preLoaders: [ 12 | { 13 | test: /\.js$/, 14 | loader: "source-map-loader", 15 | exclude: [ 16 | helpers.root('node_modules/rxjs') 17 | ] 18 | } 19 | ], 20 | loaders: [ 21 | { 22 | test: /\.ts$/, 23 | loader: 'ts-loader', 24 | query: { 25 | "compilerOptions": { 26 | "noEmitHelpers": true, 27 | "removeComments": true, 28 | } 29 | }, 30 | exclude: [ /\.e2e\.ts$/ ] 31 | }, 32 | { test: /\.json$/, loader: 'json-loader' }, 33 | { test: /\.html$/, loader: 'raw-loader' }, 34 | { test: /\.css$/, loader: 'raw-loader' } 35 | ], 36 | postLoaders: [ 37 | 38 | { 39 | test: /\.(js|ts)$/, 40 | include: helpers.root('src'), 41 | loader: 'istanbul-instrumenter-loader', 42 | exclude: [ 43 | /\.(e2e|spec)\.ts$/, 44 | /node_modules/ 45 | ] 46 | } 47 | ] 48 | }, 49 | plugins: [ 50 | new DefinePlugin({ 51 | 52 | 'process.env': { 53 | 'ENV': JSON.stringify(ENV), 54 | 'NODE_ENV': JSON.stringify(ENV) 55 | } 56 | }), 57 | new ProvidePlugin({ 58 | 59 | '__metadata': 'ts-helper/metadata', 60 | '__decorate': 'ts-helper/decorate', 61 | '__awaiter': 'ts-helper/awaiter', 62 | '__extends': 'ts-helper/extends', 63 | '__param': 'ts-helper/param', 64 | }) 65 | ], 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /todos-undo-redo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-ngrx-todos-undo-redo", 3 | "version": "0.0.2", 4 | "description": "angular 2 ngrx todos-undo-redo example", 5 | "main": "", 6 | "scripts": { 7 | "build": "npm run webpack --colors --display-error-details --display-cached", 8 | "webpack": "webpack", 9 | "clean": "rimraf node_modules", 10 | "clean-install": "npm run clean && npm install", 11 | "clean-start": "npm run clean && npm start", 12 | "typings-install": "typings install", 13 | "postinstall": "npm run typings-install", 14 | "watch": "webpack --watch", 15 | "server": "npm run server:dev", 16 | "server:dev": "webpack-dev-server --progress --profile --colors --display-error-details --display-cached --content-base src/", 17 | "start": "npm run server:dev" 18 | }, 19 | "author": "btroncone@gmail.com", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@ngrx/store": "^2.0.0", 23 | "@ngrx/core": "^1.0.0", 24 | "@angular/common": "^2.0.0-rc.1", 25 | "@angular/compiler": "^2.0.0-rc.1", 26 | "@angular/upgrade": "^2.0.0-rc.1", 27 | "@angular/core": "^2.0.0-rc.1", 28 | "@angular/http": "^2.0.0-rc.1", 29 | "@angular/platform-browser": "^2.0.0-rc.1", 30 | "@angular/router": "^2.0.0-rc.1", 31 | "@angular/platform-browser-dynamic": "^2.0.0-rc.1", 32 | "core-js": "^2.1.5", 33 | "rxjs": "5.0.0-beta.6", 34 | "zone.js": "0.6.12" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/btroncone/ngrx-examples" 39 | }, 40 | "devDependencies": { 41 | "es6-promise": "^3.1.2", 42 | "es6-shim": "^0.35.0", 43 | "es7-reflect-metadata": "^1.6.0", 44 | "compression-webpack-plugin": "^0.3.0", 45 | "copy-webpack-plugin": "^1.1.1", 46 | "css-loader": "^0.23.1", 47 | "es6-promise-loader": "^1.0.1", 48 | "exports-loader": "^0.6.2", 49 | "expose-loader": "^0.7.1", 50 | "file-loader": "^0.8.5", 51 | "html-webpack-plugin": "^1.7.0", 52 | "http-server": "^0.8.5", 53 | "imports-loader": "^0.6.5", 54 | "istanbul-instrumenter-loader": "^0.1.3", 55 | "json-loader": "^0.5.4", 56 | "ncp": "^2.0.0", 57 | "phantomjs-polyfill": "0.0.1", 58 | "phantomjs-prebuilt": "^2.1.3", 59 | "raw-loader": "0.5.1", 60 | "reflect-metadata": "0.1.2", 61 | "remap-istanbul": "^0.5.1", 62 | "rimraf": "^2.5.1", 63 | "source-map-loader": "^0.1.5", 64 | "style-loader": "^0.13.0", 65 | "ts-helpers": "1.1.1", 66 | "ts-loader": "0.8.1", 67 | "ts-node": "^0.5.5", 68 | "tsconfig-lint": "^0.5.0", 69 | "tsd": "^0.6.5", 70 | "typedoc": "^0.3.12", 71 | "typescript": "~1.8.9", 72 | "url-loader": "^0.5.7", 73 | "wallaby-webpack": "0.0.11", 74 | "webpack": "^1.12.12", 75 | "webpack-dev-server": "^1.14.1", 76 | "webpack-md5-hash": "0.0.4" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /todos-undo-redo/spec-bundle.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | /* 3 | * When testing with webpack and ES6, we have to do some extra 4 | * things get testing to work right. Because we are gonna write test 5 | * in ES6 to, we have to compile those as well. That's handled in 6 | * karma.conf.js with the karma-webpack plugin. This is the entry 7 | * file for webpack test. Just like webpack will create a bundle.js 8 | * file for our client, when we run test, it well compile and bundle them 9 | * all here! Crazy huh. So we need to do some setup 10 | */ 11 | Error.stackTraceLimit = Infinity; 12 | require('phantomjs-polyfill'); 13 | require('es6-promise'); 14 | require('es6-shim'); 15 | require('es7-reflect-metadata/dist/browser'); 16 | 17 | require('zone.js/dist/zone-microtask.js'); 18 | require('zone.js/dist/long-stack-trace-zone.js'); 19 | require('zone.js/dist/jasmine-patch.js'); 20 | 21 | 22 | var testing = require('angular2/testing'); 23 | var browser = require('angular2/platform/testing/browser'); 24 | testing.setBaseTestProviders( 25 | browser.TEST_BROWSER_PLATFORM_PROVIDERS, 26 | browser.TEST_BROWSER_APPLICATION_PROVIDERS); 27 | 28 | /* 29 | Ok, this is kinda crazy. We can use the the context method on 30 | require that webpack created in order to tell webpack 31 | what files we actually want to require or import. 32 | Below, context will be an function/object with file names as keys. 33 | using that regex we are saying look in ./src/app and ./test then find 34 | any file that ends with spec.js and get its path. By passing in true 35 | we say do this recursively 36 | */ 37 | var testContext = require.context('./src', true, /\.spec\.ts/); 38 | 39 | // get all the files, for each file, call the context function 40 | // that will require the file and load it up here. Context will 41 | // loop and require those spec files here 42 | testContext.keys().forEach(testContext); -------------------------------------------------------------------------------- /todos-undo-redo/src/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from '@angular/platform-browser-dynamic'; 2 | import {TodoApp} from './todo-app'; 3 | import {provideStore} from "@ngrx/store"; 4 | import {APP_REDUCERS} from "./reducers/reducers"; 5 | 6 | 7 | export function main() { 8 | return bootstrap(TodoApp, [ 9 | provideStore(APP_REDUCERS) 10 | ]) 11 | .catch(err => console.error(err)); 12 | } 13 | 14 | document.addEventListener('DOMContentLoaded', main); -------------------------------------------------------------------------------- /todos-undo-redo/src/app/common/actions.ts: -------------------------------------------------------------------------------- 1 | //todo actions 2 | export const ADD_TODO = 'ADD_TODO'; 3 | export const REMOVE_TODO = 'REMOVE_TODO'; 4 | export const TOGGLE_TODO = 'TOGGLE_TODO'; 5 | 6 | //filter actions 7 | export const SHOW_ALL = 'SHOW_ALL'; 8 | export const SHOW_COMPLETED = 'SHOW_COMPLETED'; 9 | export const SHOW_ACTIVE = 'SHOW_ACTIVE'; 10 | 11 | //undo/redo 12 | export const UNDO = 'UNDO'; 13 | export const REDO = 'REDO'; -------------------------------------------------------------------------------- /todos-undo-redo/src/app/common/interfaces.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer} from "@ngrx/store"; 2 | 3 | export interface AppState { 4 | Todos: Todo[], 5 | VisibilityFilter: any 6 | } 7 | 8 | export interface Todo { 9 | id: number, 10 | text: string, 11 | complete: boolean 12 | } 13 | 14 | export interface TodoModel { 15 | filteredTodos: Todo[], 16 | totalTodos: number, 17 | completedTodos: number 18 | } 19 | 20 | export interface UndoableState { 21 | past: any[], 22 | present: any, 23 | future: any[] 24 | } 25 | -------------------------------------------------------------------------------- /todos-undo-redo/src/app/components/filter-select.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, Input, EventEmitter} from "@angular/core"; 2 | import {Todo} from "../common/interfaces"; 3 | 4 | @Component({ 5 | selector: 'filter-select', 6 | template: ` 7 |
    8 | 13 |
    14 | ` 15 | }) 16 | export class FilterSelect{ 17 | public filters = [ 18 | {friendly: "All", action: 'SHOW_ALL'}, 19 | {friendly: "Completed", action: 'SHOW_COMPLETED'}, 20 | {friendly: "Active", action: 'SHOW_ACTIVE'} 21 | ]; 22 | @Output() filterSelect: EventEmitter = new EventEmitter(); 23 | } -------------------------------------------------------------------------------- /todos-undo-redo/src/app/components/todo-input.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, EventEmitter} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'todo-input', 5 | template: ` 6 |
    7 | 8 | 9 |
    10 | 15 | ` 16 | }) 17 | export class TodoInput { 18 | @Output() addTodo : EventEmitter = new EventEmitter(); 19 | 20 | add(todoInput){ 21 | this.addTodo.emit(todoInput.value); 22 | todoInput.value = ''; 23 | } 24 | } -------------------------------------------------------------------------------- /todos-undo-redo/src/app/components/todo-list.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input, Output, EventEmitter} from "@angular/core"; 2 | import {Todo, TodoModel} from "../common/interfaces"; 3 | 4 | @Component({ 5 | selector: 'todo-list', 6 | template: ` 7 | Completed: {{todosModel.completedTodos}}/{{todosModel.totalTodos}} 8 |
      9 |
    • 10 | {{todo.description}} 11 | 15 | 19 |
    • 20 |
    21 | `, 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class TodoList{ 25 | @Input() todosModel : TodoModel[]; 26 | @Output() removeTodo: EventEmitter = new EventEmitter(); 27 | @Output() toggleTodo: EventEmitter = new EventEmitter(); 28 | } -------------------------------------------------------------------------------- /todos-undo-redo/src/app/reducers/reducers.ts: -------------------------------------------------------------------------------- 1 | import {todos} from "./todos"; 2 | import {visibilityFilter} from "./visibility-filter"; 3 | import {undoable} from "./undoable"; 4 | //wrap todos reducer for undo/redo functionality 5 | export const APP_REDUCERS = { 6 | todos: undoable(todos), 7 | visibilityFilter 8 | }; -------------------------------------------------------------------------------- /todos-undo-redo/src/app/reducers/todos.spec.ts: -------------------------------------------------------------------------------- 1 | import {todos} from "./todos"; 2 | import {Todo} from "../common/interfaces"; 3 | //had issue with jasmine typing conflicts, this is temporary workaround 4 | declare var it, expect, describe, toBe; 5 | 6 | describe('The counter reducer', () => { 7 | it('should return current state when no valid actions have been made', () => { 8 | const state : Todo[] = [ 9 | { 10 | id: 1, 11 | text: 'Test', 12 | complete: false 13 | } 14 | ]; 15 | const actual = todos(state, {type: 'INVALID_ACTION', payload: {}}); 16 | const expected = state; 17 | expect(actual).toBe(expected); 18 | }); 19 | 20 | it('should add a todo when ADD_TODO action is dispatched', () => { 21 | const initialState : Todo[] = [ 22 | { 23 | id: 1, 24 | text: 'Test', 25 | complete: false 26 | } 27 | ]; 28 | const expectedState : Todo[] = [ 29 | { 30 | id: 1, 31 | text: 'Test', 32 | complete: false 33 | }, 34 | { 35 | id: 2, 36 | text: 'New Todo', 37 | complete: false 38 | } 39 | ]; 40 | const newTodo : Todo = { 41 | id: 2, 42 | text: 'New Todo', 43 | complete: false 44 | }; 45 | const actual = todos(initialState, {type: 'ADD_TODO', payload: newTodo}); 46 | const expected = expectedState; 47 | expect(actual).toEqual(expected); 48 | }); 49 | 50 | it('should toggle a todos completed status when TOGGLE_TODO is dispatched', () => { 51 | const state : Todo[] = [ 52 | { 53 | id: 1, 54 | text: 'Test', 55 | complete: false 56 | } 57 | ]; 58 | const todoToToggle = { 59 | id: 1, 60 | text: 'Test', 61 | completed: false 62 | }; 63 | const [actual] = todos(state, {type: 'TOGGLE_TODO', payload: todoToToggle}); 64 | const expected = true; 65 | expect(actual.complete).toBe(expected); 66 | }); 67 | 68 | }); -------------------------------------------------------------------------------- /todos-undo-redo/src/app/reducers/todos.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | import {Todo} from "../common/interfaces"; 3 | import {ADD_TODO, REMOVE_TODO, TOGGLE_TODO} from "../common/actions"; 4 | 5 | export const todos : ActionReducer = (state : Todo[] = [], action: Action) => { 6 | switch(action.type) { 7 | case ADD_TODO: 8 | return [ 9 | ...state, 10 | action.payload 11 | ]; 12 | 13 | case REMOVE_TODO: 14 | return state.filter(todo => todo.id !== action.payload); 15 | 16 | case TOGGLE_TODO: 17 | return state.map(todo => { 18 | if(todo.id !== action.payload){ 19 | return todo; 20 | } 21 | return Object.assign({}, todo, { 22 | complete: !todo.complete 23 | }); 24 | }); 25 | 26 | default: 27 | return state; 28 | } 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /todos-undo-redo/src/app/reducers/undoable.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | import {UndoableState} from "../common/interfaces"; 3 | 4 | //based on Rob Wormald's example http://plnkr.co/edit/UnU1wnFcausVFfEP2RGD?p=preview 5 | /* 6 | This is a 'meta-reducer', meant to wrap (accept a reducer, return reducer) any reducer to quickly provide undo/redo capability. 7 | With the removal of middleware in Store v2 meta-reducers will replace the majority of that functionality. 8 | More on meta-reducers: https://gist.github.com/btroncone/a6e4347326749f938510#implementing-a-meta-reducer 9 | Example local-storage: https://github.com/btroncone/ngrx-store-localstorage/tree/storev2 10 | Example logger: https://github.com/btroncone/ngrx-store-logger/tree/loggerv2 11 | */ 12 | export function undoable(reducer : ActionReducer) { 13 | // Call the reducer with empty action to populate the initial state 14 | const initialState : UndoableState = { 15 | past: [], 16 | present: reducer(undefined, {type: '__INIT__'}), 17 | future: [] 18 | }; 19 | 20 | // Return a reducer that handles undo and redo 21 | return function (state = initialState, action : Action) { 22 | const { past, present, future } = state; 23 | switch (action.type) { 24 | 25 | case 'UNDO': 26 | const previous = past[past.length - 1]; 27 | const newPast = past.slice(0, past.length - 1); 28 | return { 29 | past: newPast, 30 | present: previous, 31 | future: [ present, ...future ] 32 | }; 33 | case 'REDO': 34 | const next = future[0]; 35 | const newFuture = future.slice(1); 36 | return { 37 | past: [ ...past, present ], 38 | present: next, 39 | future: newFuture 40 | }; 41 | default: 42 | // Delegate handling the action to the passed reducer 43 | const newPresent = reducer(present, action); 44 | if (present === newPresent) { 45 | return state 46 | } 47 | return { 48 | past: [ ...past, present ], 49 | present: newPresent, 50 | future: [] 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /todos-undo-redo/src/app/reducers/visibility-filter.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | import {SHOW_COMPLETED, SHOW_ACTIVE, SHOW_ALL} from "../common/actions"; 3 | 4 | export const visibilityFilter : ActionReducer = (state : any = t => t, action : Action) => { 5 | switch(action.type){ 6 | case SHOW_COMPLETED: 7 | return todo => todo.complete; 8 | 9 | case SHOW_ACTIVE: 10 | return todo => !todo.complete; 11 | 12 | case SHOW_ALL: 13 | return todo => todo; 14 | 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /todos-undo-redo/src/app/todo-app.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {TodoList} from "./components/todo-list"; 3 | import {TodoInput} from "./components/todo-input"; 4 | import {FilterSelect} from "./components/filter-select"; 5 | import {Store} from '@ngrx/store'; 6 | import {AppState, Todo, TodoModel} from "./common/interfaces"; 7 | import {Observable} from "rxjs/Observable"; 8 | import {ADD_TODO, REMOVE_TODO, TOGGLE_TODO, UNDO, REDO} from './common/actions'; 9 | 10 | @Component({ 11 | selector: `todo-app`, 12 | template: ` 13 |
    14 | 20 |
    21 | 23 | 24 | 28 | 32 | 34 | 35 | 39 | 40 |
    41 |
    42 | `, 43 | directives: [TodoList, TodoInput, FilterSelect], 44 | changeDetection: ChangeDetectionStrategy.OnPush 45 | }) 46 | export class TodoApp { 47 | public todosModel$ : Observable; 48 | private id: number = 0; 49 | 50 | constructor( 51 | private _store : Store 52 | ){ 53 | const todos$ = _store.select>('todos'); 54 | const visibilityFilter$ = _store.select('visibilityFilter'); 55 | 56 | this.todosModel$ = Observable 57 | .combineLatest( 58 | todos$, 59 | visibilityFilter$, 60 | ({present = []}, visibilityFilter : any) => { 61 | return { 62 | filteredTodos: present.filter(visibilityFilter), 63 | totalTodos: present.length, 64 | completedTodos: present.filter((todo : Todo) => todo.complete).length 65 | } 66 | } 67 | ); 68 | } 69 | 70 | addTodo(description : string){ 71 | this._store.dispatch({type: ADD_TODO, payload: { 72 | id: ++this.id, 73 | description, 74 | complete: false 75 | }}); 76 | } 77 | 78 | removeTodo(id : number){ 79 | this._store.dispatch({type: REMOVE_TODO, payload: id}); 80 | } 81 | 82 | toggleTodo(id : number){ 83 | this._store.dispatch({type: TOGGLE_TODO, payload: id}); 84 | } 85 | 86 | updateFilter(filter){ 87 | this._store.dispatch({type: filter}); 88 | } 89 | 90 | undo(){ 91 | this._store.dispatch({type: UNDO}); 92 | } 93 | 94 | redo(){ 95 | this._store.dispatch({type: REDO}); 96 | } 97 | } -------------------------------------------------------------------------------- /todos-undo-redo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%= o.webpackConfig.metadata.title %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 19 | 20 | {% } %} 21 | 22 | 23 | 24 | 25 | Loading... 26 | 27 | {% if (o.webpackConfig.metadata.ENV === 'development') { %} 28 | 29 | 30 | {% } %} 31 | 32 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 33 | 34 | {% } %} 35 | 36 | -------------------------------------------------------------------------------- /todos-undo-redo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6'; 2 | import 'core-js/es7/reflect'; 3 | import 'ts-helpers'; 4 | import 'rxjs'; 5 | require('zone.js/dist/zone'); 6 | require('zone.js/dist/long-stack-trace-zone'); -------------------------------------------------------------------------------- /todos-undo-redo/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | a:hover, 12 | a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | h3 { 17 | font-weight: 100; 18 | } 19 | 20 | /* LAYOUT CSS */ 21 | .pure-img-responsive { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | #layout { 27 | padding: 0; 28 | } 29 | 30 | .header { 31 | text-align: center; 32 | top: auto; 33 | margin: 3em auto; 34 | } 35 | 36 | .sidebar { 37 | background: rgb(61, 79, 93); 38 | color: #fff; 39 | } 40 | 41 | .brand-title, 42 | .brand-tagline { 43 | margin: 0; 44 | } 45 | .brand-title { 46 | text-transform: uppercase; 47 | } 48 | .brand-tagline { 49 | font-weight: 300; 50 | color: rgb(176, 202, 219); 51 | } 52 | 53 | .nav-list { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | .nav-item { 59 | display: inline-block; 60 | *display: inline; 61 | zoom: 1; 62 | } 63 | .nav-item a { 64 | background: transparent; 65 | border: 2px solid rgb(176, 202, 219); 66 | color: #fff; 67 | margin-top: 1em; 68 | letter-spacing: 0.05em; 69 | text-transform: uppercase; 70 | font-size: 85%; 71 | } 72 | .nav-item a:hover, 73 | .nav-item a:focus { 74 | border: 2px solid rgb(61, 146, 201); 75 | text-decoration: none; 76 | } 77 | 78 | .content-subhead { 79 | text-transform: uppercase; 80 | color: #aaa; 81 | border-bottom: 1px solid #eee; 82 | padding: 0.4em 0; 83 | font-size: 80%; 84 | font-weight: 500; 85 | letter-spacing: 0.1em; 86 | } 87 | 88 | .content { 89 | padding: 2em 1em 0; 90 | } 91 | 92 | .post { 93 | padding-bottom: 2em; 94 | } 95 | .post-title { 96 | font-size: 2em; 97 | color: #222; 98 | margin-bottom: 0.2em; 99 | } 100 | .post-avatar { 101 | border-radius: 50px; 102 | float: right; 103 | margin-left: 1em; 104 | } 105 | .post-description { 106 | font-family: Georgia, "Cambria", serif; 107 | color: #444; 108 | line-height: 1.8em; 109 | } 110 | .post-meta { 111 | color: #999; 112 | font-size: 90%; 113 | margin: 0; 114 | } 115 | 116 | .post-category { 117 | margin: 0 0.1em; 118 | padding: 0.3em 1em; 119 | color: #fff; 120 | background: #999; 121 | font-size: 80%; 122 | } 123 | .post-category-design { 124 | background: #5aba59; 125 | } 126 | .post-category-pure { 127 | background: #4d85d1; 128 | } 129 | .post-category-yui { 130 | background: #8156a7; 131 | } 132 | .post-category-js { 133 | background: #df2d4f; 134 | } 135 | 136 | .post-images { 137 | margin: 1em 0; 138 | } 139 | .post-image-meta { 140 | margin-top: -3.5em; 141 | margin-left: 1em; 142 | color: #fff; 143 | text-shadow: 0 1px 1px #333; 144 | } 145 | 146 | .footer { 147 | text-align: center; 148 | padding: 1em 0; 149 | } 150 | .footer a { 151 | color: #ccc; 152 | font-size: 80%; 153 | } 154 | .footer .pure-menu a:hover, 155 | .footer .pure-menu a:focus { 156 | background: none; 157 | } 158 | 159 | @media (min-width: 48em) { 160 | .content { 161 | padding: 2em 3em 0; 162 | margin-left: 25%; 163 | } 164 | 165 | .header { 166 | margin: 80% 2em 0; 167 | text-align: right; 168 | } 169 | 170 | .sidebar { 171 | position: fixed; 172 | top: 0; 173 | bottom: 0; 174 | } 175 | } 176 | 177 | .complete{ 178 | text-decoration: line-through; 179 | } 180 | 181 | .margin-t-20{ 182 | margin-top:20px; 183 | } 184 | -------------------------------------------------------------------------------- /todos-undo-redo/src/vendor.ts: -------------------------------------------------------------------------------- 1 | import '@angular/platform-browser'; 2 | import '@angular/platform-browser-dynamic'; 3 | import '@angular/core'; 4 | import '@angular/common'; 5 | import '@angular/http'; 6 | import '@angular/router-deprecated'; 7 | import 'rxjs'; -------------------------------------------------------------------------------- /todos-undo-redo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true 9 | }, 10 | "exclude":[ 11 | "node_modules", 12 | "typings/main.d.ts", 13 | "typings/main" 14 | ], 15 | "filesGlob": [ 16 | "./src/**/*.ts", 17 | "!./node_modules/**/*.ts", 18 | "typings/browser.d.ts" 19 | ], 20 | "compileOnSave": false, 21 | "buildOnSave": false 22 | } -------------------------------------------------------------------------------- /todos-undo-redo/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "es6-promise": "github:typed-typings/npm-es6-promise#fb04188767acfec1defd054fc8024fafa5cd4de7" 4 | }, 5 | "devDependencies": {}, 6 | "ambientDependencies": { 7 | "angular-protractor": "github:DefinitelyTyped/DefinitelyTyped/angular-protractor/angular-protractor.d.ts#64b25f63f0ec821040a5d3e049a976865062ed9d", 8 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2", 9 | "hammerjs": "github:DefinitelyTyped/DefinitelyTyped/hammerjs/hammerjs.d.ts#74a4dfc1bc2dfadec47b8aae953b28546cb9c6b7", 10 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c", 11 | "ng2": "github:gdi2290/typings-ng2/ng2.d.ts#32998ff5584c0eab0cd9dc7704abb1c5c450701c", 12 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#8cf8164641be73e8f1e652c2a5b967c7210b6729", 13 | "selenium-webdriver": "github:DefinitelyTyped/DefinitelyTyped/selenium-webdriver/selenium-webdriver.d.ts#a83677ed13add14c2ab06c7325d182d0ba2784ea", 14 | "webpack": "github:DefinitelyTyped/DefinitelyTyped/webpack/webpack.d.ts#95c02169ba8fa58ac1092422efbd2e3174a206f4", 15 | "zone.js": "github:DefinitelyTyped/DefinitelyTyped/zone.js/zone.js.d.ts#c393f8974d44840a6c9cc6d5b5c0188a8f05143d" 16 | } 17 | } -------------------------------------------------------------------------------- /todos-undo-redo/wallaby.js: -------------------------------------------------------------------------------- 1 | var wallabyWebpack = require('wallaby-webpack'); 2 | 3 | var webpackPostprocessor = wallabyWebpack({ 4 | entryPatterns: [ 5 | 'spec-bundle.js', 6 | 'src/**/*spec.js' 7 | ] 8 | }); 9 | 10 | module.exports = function (w) { 11 | 12 | return { 13 | files: [ 14 | {pattern: 'spec-bundle.js', load: false}, 15 | {pattern: 'src/**/*.ts', load: false}, 16 | {pattern: 'src/**/*spec.ts', ignore: true} 17 | ], 18 | 19 | tests: [ 20 | { pattern: 'src/**/*spec.ts', load: false } 21 | ], 22 | 23 | testFramework: "jasmine", 24 | 25 | compilers: { 26 | '**/*.ts': w.compilers.typeScript({ 27 | emitDecoratorMetadata: true, 28 | experimentalDecorators: true 29 | }) 30 | }, 31 | 32 | postprocessor: webpackPostprocessor, 33 | 34 | bootstrap: function () { 35 | window.__moduleBundler.loadTests(); 36 | } 37 | }; 38 | }; -------------------------------------------------------------------------------- /todos-undo-redo/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | 3 | /* 4 | * Helper: root(), and rootDir() are defined at the bottom 5 | */ 6 | var path = require('path'); 7 | var webpack = require('webpack'); 8 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | var ENV = process.env.ENV = process.env.NODE_ENV = 'development'; 11 | 12 | var metadata = { 13 | title: 'NgRx Example #4 - Todos Undo/Redo', 14 | baseUrl: '/', 15 | host: 'localhost', 16 | port: 3000, 17 | ENV: ENV 18 | }; 19 | /* 20 | * Config 21 | */ 22 | module.exports = { 23 | // static data for index.html 24 | metadata: metadata, 25 | // for faster builds use 'eval' 26 | devtool: 'source-map', 27 | debug: true, 28 | // cache: false, 29 | 30 | // our angular app 31 | entry: { 'polyfills': './src/polyfills.ts', 'main': './src/app/bootstrap.ts' }, 32 | 33 | // Config for our build files 34 | output: { 35 | path: root('dist'), 36 | filename: '[name].bundle.js', 37 | sourceMapFilename: '[name].map', 38 | chunkFilename: '[id].chunk.js' 39 | }, 40 | 41 | 42 | resolve: { 43 | // ensure loader extensions match 44 | extensions: prepend(['.ts','.js','.json','.css','.html'], '.async') // ensure .async.ts etc also works 45 | }, 46 | 47 | module: { 48 | preLoaders: [ 49 | { test: /\.js$/, loader: "source-map-loader", exclude: [ root('node_modules/rxjs') ] } 50 | ], 51 | loaders: [ 52 | // Support Angular 2 async routes via .async.ts 53 | { test: /\.async\.ts$/, loaders: ['es6-promise-loader', 'ts-loader'], exclude: [ /\.(spec|e2e)\.ts$/ ] }, 54 | 55 | // Support for .ts files. 56 | { test: /\.ts$/, loader: 'ts-loader', exclude: [ /\.(spec|e2e|async)\.ts$/ ] }, 57 | 58 | // Support for *.json files. 59 | { test: /\.json$/, loader: 'json-loader' }, 60 | 61 | // Support for CSS as raw text 62 | { test: /\.css$/, loader: 'raw-loader' }, 63 | 64 | // support for .html as raw text 65 | { test: /\.html$/, loader: 'raw-loader' } 66 | 67 | // if you add a loader include the resolve file extension above 68 | ] 69 | }, 70 | 71 | plugins: [ 72 | new webpack.optimize.OccurenceOrderPlugin(true), 73 | new webpack.optimize.CommonsChunkPlugin({ name: 'polyfills', filename: 'polyfills.bundle.js', minChunks: Infinity }), 74 | // static assets 75 | new CopyWebpackPlugin([ { from: 'src/assets', to: 'assets' } ]), 76 | // generating html 77 | new HtmlWebpackPlugin({ template: 'src/index.html', inject: false }), 78 | // replace 79 | new webpack.DefinePlugin({ 80 | 'process.env': { 81 | 'ENV': JSON.stringify(metadata.ENV), 82 | 'NODE_ENV': JSON.stringify(metadata.ENV) 83 | } 84 | }) 85 | ], 86 | 87 | // Other module loader config 88 | tslint: { 89 | emitErrors: false, 90 | failOnHint: false, 91 | resourcePath: 'src' 92 | }, 93 | // our Webpack Development Server config 94 | devServer: { 95 | port: metadata.port, 96 | host: metadata.host, 97 | // contentBase: 'src/', 98 | historyApiFallback: true, 99 | watchOptions: { aggregateTimeout: 300, poll: 1000 } 100 | }, 101 | // we need this due to problems with es6-shim 102 | node: {global: 'window', progress: false, crypto: 'empty', module: false, clearImmediate: false, setImmediate: false} 103 | }; 104 | 105 | // Helper functions 106 | 107 | function root(args) { 108 | args = Array.prototype.slice.call(arguments, 0); 109 | return path.join.apply(path, [__dirname].concat(args)); 110 | } 111 | 112 | function prepend(extensions, args) { 113 | args = args || []; 114 | if (!Array.isArray(args)) { args = [args] } 115 | return extensions.reduce(function(memo, val) { 116 | return memo.concat(val, args.map(function(prefix) { 117 | return prefix + val 118 | })); 119 | }, ['']); 120 | } 121 | function rootNode(args) { 122 | args = Array.prototype.slice.call(arguments, 0); 123 | return root.apply(path, ['node_modules'].concat(args)); 124 | } -------------------------------------------------------------------------------- /todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-ngrx-todos", 3 | "version": "0.0.2", 4 | "description": "angular 2 ngrx todos example", 5 | "main": "", 6 | "scripts": { 7 | "build": "npm run webpack --colors --display-error-details --display-cached", 8 | "webpack": "webpack", 9 | "clean": "rimraf node_modules", 10 | "clean-install": "npm run clean && npm install", 11 | "clean-start": "npm run clean && npm start", 12 | "typings-install": "typings install", 13 | "postinstall": "npm run typings-install", 14 | "watch": "webpack --watch", 15 | "server": "npm run server:dev", 16 | "server:dev": "webpack-dev-server --progress --profile --colors --display-error-details --display-cached --content-base src/", 17 | "start": "npm run server:dev" 18 | }, 19 | "author": "btroncone@gmail.com", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@ngrx/store": "^2.0.0", 23 | "@ngrx/core": "^1.0.0", 24 | "@angular/common": "^2.0.0-rc.1", 25 | "@angular/compiler": "^2.0.0-rc.1", 26 | "@angular/upgrade": "^2.0.0-rc.1", 27 | "@angular/core": "^2.0.0-rc.1", 28 | "@angular/http": "^2.0.0-rc.1", 29 | "@angular/platform-browser": "^2.0.0-rc.1", 30 | "@angular/router": "^2.0.0-rc.1", 31 | "@angular/platform-browser-dynamic": "^2.0.0-rc.1", 32 | "core-js": "^2.1.5", 33 | "rxjs": "5.0.0-beta.6", 34 | "zone.js": "0.6.12" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/btroncone/ngrx-examples" 39 | }, 40 | "devDependencies": { 41 | "es6-promise": "^3.1.2", 42 | "es6-shim": "^0.35.0", 43 | "es7-reflect-metadata": "^1.6.0", 44 | "compression-webpack-plugin": "^0.3.0", 45 | "copy-webpack-plugin": "^1.1.1", 46 | "css-loader": "^0.23.1", 47 | "es6-promise-loader": "^1.0.1", 48 | "exports-loader": "^0.6.2", 49 | "expose-loader": "^0.7.1", 50 | "file-loader": "^0.8.5", 51 | "html-webpack-plugin": "^1.7.0", 52 | "http-server": "^0.8.5", 53 | "imports-loader": "^0.6.5", 54 | "istanbul-instrumenter-loader": "^0.1.3", 55 | "json-loader": "^0.5.4", 56 | "ncp": "^2.0.0", 57 | "phantomjs-polyfill": "0.0.1", 58 | "phantomjs-prebuilt": "^2.1.3", 59 | "raw-loader": "0.5.1", 60 | "reflect-metadata": "0.1.2", 61 | "remap-istanbul": "^0.5.1", 62 | "rimraf": "^2.5.1", 63 | "source-map-loader": "^0.1.5", 64 | "style-loader": "^0.13.0", 65 | "ts-helpers": "1.1.1", 66 | "ts-loader": "0.8.1", 67 | "ts-node": "^0.5.5", 68 | "tsconfig-lint": "^0.5.0", 69 | "tsd": "^0.6.5", 70 | "typedoc": "^0.3.12", 71 | "typescript": "~1.8.9", 72 | "url-loader": "^0.5.7", 73 | "wallaby-webpack": "0.0.11", 74 | "webpack": "^1.12.12", 75 | "webpack-dev-server": "^1.14.1", 76 | "webpack-md5-hash": "0.0.4" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /todos/spec-bundle.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | /* 3 | * When testing with webpack and ES6, we have to do some extra 4 | * things get testing to work right. Because we are gonna write test 5 | * in ES6 to, we have to compile those as well. That's handled in 6 | * karma.conf.js with the karma-webpack plugin. This is the entry 7 | * file for webpack test. Just like webpack will create a bundle.js 8 | * file for our client, when we run test, it well compile and bundle them 9 | * all here! Crazy huh. So we need to do some setup 10 | */ 11 | Error.stackTraceLimit = Infinity; 12 | require('phantomjs-polyfill'); 13 | require('es6-promise'); 14 | require('es6-shim'); 15 | require('es7-reflect-metadata/dist/browser'); 16 | 17 | require('zone.js/dist/zone-microtask.js'); 18 | require('zone.js/dist/long-stack-trace-zone.js'); 19 | require('zone.js/dist/jasmine-patch.js'); 20 | 21 | 22 | var testing = require('angular2/testing'); 23 | var browser = require('angular2/platform/testing/browser'); 24 | testing.setBaseTestProviders( 25 | browser.TEST_BROWSER_PLATFORM_PROVIDERS, 26 | browser.TEST_BROWSER_APPLICATION_PROVIDERS); 27 | 28 | /* 29 | Ok, this is kinda crazy. We can use the the context method on 30 | require that webpack created in order to tell webpack 31 | what files we actually want to require or import. 32 | Below, context will be an function/object with file names as keys. 33 | using that regex we are saying look in ./src/app and ./test then find 34 | any file that ends with spec.js and get its path. By passing in true 35 | we say do this recursively 36 | */ 37 | var testContext = require.context('./src', true, /\.spec\.ts/); 38 | 39 | // get all the files, for each file, call the context function 40 | // that will require the file and load it up here. Context will 41 | // loop and require those spec files here 42 | testContext.keys().forEach(testContext); -------------------------------------------------------------------------------- /todos/src/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from '@angular/platform-browser-dynamic'; 2 | import {TodoApp} from './todo-app'; 3 | import {provideStore} from "@ngrx/store"; 4 | import * as APP_REDUCERS from "./reducers/reducers"; 5 | 6 | 7 | export function main() { 8 | return bootstrap(TodoApp, [ 9 | provideStore(APP_REDUCERS) 10 | ]) 11 | .catch(err => console.error(err)); 12 | } 13 | 14 | document.addEventListener('DOMContentLoaded', main); -------------------------------------------------------------------------------- /todos/src/app/common/actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | I prefer to keep action constants in a single file. 3 | This allows you to, at a quick glance, see all relevant user interaction within your application. 4 | You could also keep actions with the appropriate reducers or export each action group seperately. 5 | */ 6 | 7 | //todo actions 8 | export const ADD_TODO = 'ADD_TODO'; 9 | export const REMOVE_TODO = 'REMOVE_TODO'; 10 | export const TOGGLE_TODO = 'TOGGLE_TODO'; 11 | 12 | //filter actions 13 | export const SHOW_ALL = 'SHOW_ALL'; 14 | export const SHOW_COMPLETED = 'SHOW_COMPLETED'; 15 | export const SHOW_ACTIVE = 'SHOW_ACTIVE'; -------------------------------------------------------------------------------- /todos/src/app/common/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface AppState { 2 | Todos: Todo[], 3 | VisibilityFilter: any 4 | } 5 | 6 | export interface Todo { 7 | id: number, 8 | text: string, 9 | complete: boolean 10 | }; 11 | 12 | export interface TodoModel { 13 | filteredTodos: Todo[], 14 | totalTodos: number, 15 | completedTodos: number 16 | } -------------------------------------------------------------------------------- /todos/src/app/components/filter-select.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, Input, EventEmitter} from "@angular/core"; 2 | import {Todo} from "../common/interfaces"; 3 | 4 | @Component({ 5 | selector: 'filter-select', 6 | template: ` 7 |
    8 | 13 |
    14 | ` 15 | }) 16 | export class FilterSelect{ 17 | public filters = [ 18 | {friendly: "All", action: 'SHOW_ALL'}, 19 | {friendly: "Completed", action: 'SHOW_COMPLETED'}, 20 | {friendly: "Active", action: 'SHOW_ACTIVE'} 21 | ]; 22 | @Output() filterSelect: EventEmitter = new EventEmitter(); 23 | } -------------------------------------------------------------------------------- /todos/src/app/components/todo-input.ts: -------------------------------------------------------------------------------- 1 | import {Component, Output, EventEmitter} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'todo-input', 5 | template: ` 6 |
    7 | 8 | 9 |
    10 | 15 | ` 16 | }) 17 | export class TodoInput { 18 | @Output() addTodo : EventEmitter = new EventEmitter(); 19 | 20 | add(todoInput){ 21 | this.addTodo.emit(todoInput.value); 22 | todoInput.value = ''; 23 | } 24 | } -------------------------------------------------------------------------------- /todos/src/app/components/todo-list.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input, Output, EventEmitter} from "@angular/core"; 2 | import {Todo, TodoModel} from "../common/interfaces"; 3 | 4 | @Component({ 5 | selector: 'todo-list', 6 | template: ` 7 | Completed: {{todosModel.completedTodos}}/{{todosModel.totalTodos}} 8 |
      9 |
    • 10 | {{todo.description}} 11 | 15 | 19 |
    • 20 |
    21 | `, 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class TodoList{ 25 | @Input() todosModel : TodoModel[]; 26 | @Output() removeTodo: EventEmitter = new EventEmitter(); 27 | @Output() toggleTodo: EventEmitter = new EventEmitter(); 28 | } -------------------------------------------------------------------------------- /todos/src/app/reducers/reducers.ts: -------------------------------------------------------------------------------- 1 | export * from "./todos"; 2 | export * from "./visibility-filter"; -------------------------------------------------------------------------------- /todos/src/app/reducers/todos.spec.ts: -------------------------------------------------------------------------------- 1 | import {todos} from "./todos"; 2 | import {Todo} from "../common/interfaces"; 3 | //had issue with jasmine typing conflicts, this is temporary workaround 4 | declare var it, expect, describe, toBe; 5 | 6 | describe('The counter reducer', () => { 7 | it('should return current state when no valid actions have been made', () => { 8 | const state : Todo[] = [ 9 | { 10 | id: 1, 11 | text: 'Test', 12 | complete: false 13 | } 14 | ]; 15 | const actual = todos(state, {type: 'INVALID_ACTION', payload: {}}); 16 | const expected = state; 17 | expect(actual).toBe(expected); 18 | }); 19 | 20 | it('should add a todo when ADD_TODO action is dispatched', () => { 21 | const initialState : Todo[] = [ 22 | { 23 | id: 1, 24 | text: 'Test', 25 | complete: false 26 | } 27 | ]; 28 | const expectedState : Todo[] = [ 29 | { 30 | id: 1, 31 | text: 'Test', 32 | complete: false 33 | }, 34 | { 35 | id: 2, 36 | text: 'New Todo', 37 | complete: false 38 | } 39 | ]; 40 | const newTodo : Todo = { 41 | id: 2, 42 | text: 'New Todo', 43 | complete: false 44 | }; 45 | const actual = todos(initialState, {type: 'ADD_TODO', payload: newTodo}); 46 | const expected = expectedState; 47 | expect(actual).toEqual(expected); 48 | }); 49 | 50 | it('should toggle a todos completed status when TOGGLE_TODO is dispatched', () => { 51 | const state : Todo[] = [ 52 | { 53 | id: 1, 54 | text: 'Test', 55 | complete: false 56 | } 57 | ]; 58 | const todoToToggle = { 59 | id: 1, 60 | text: 'Test', 61 | completed: false 62 | }; 63 | const [actual] = todos(state, {type: 'TOGGLE_TODO', payload: todoToToggle}); 64 | const expected = true; 65 | expect(actual.complete).toBe(expected); 66 | }); 67 | 68 | }); -------------------------------------------------------------------------------- /todos/src/app/reducers/todos.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | import {Todo} from "../common/interfaces"; 3 | import {ADD_TODO, REMOVE_TODO, TOGGLE_TODO} from "../common/actions"; 4 | 5 | export const todos : ActionReducer = (state : Todo[] = [], action: Action) => { 6 | switch(action.type) { 7 | case ADD_TODO: 8 | return [ 9 | ...state, 10 | action.payload 11 | ]; 12 | 13 | case REMOVE_TODO: 14 | return state.filter(todo => todo.id !== action.payload); 15 | 16 | case TOGGLE_TODO: 17 | return state.map(todo => { 18 | if(todo.id !== action.payload){ 19 | return todo; 20 | } 21 | return Object.assign({}, todo, { 22 | complete: !todo.complete 23 | }); 24 | }); 25 | 26 | default: 27 | return state; 28 | } 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /todos/src/app/reducers/visibility-filter.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducer, Action} from "@ngrx/store"; 2 | import {SHOW_COMPLETED, SHOW_ACTIVE, SHOW_ALL} from "../common/actions"; 3 | 4 | export const visibilityFilter : ActionReducer = (state : any = t => t, action : Action) => { 5 | switch(action.type){ 6 | case SHOW_COMPLETED: 7 | return todo => todo.complete; 8 | 9 | case SHOW_ACTIVE: 10 | return todo => !todo.complete; 11 | 12 | case SHOW_ALL: 13 | return todo => todo; 14 | 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /todos/src/app/todo-app.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {TodoList} from "./components/todo-list"; 3 | import {TodoInput} from "./components/todo-input"; 4 | import {FilterSelect} from "./components/filter-select"; 5 | import {Store} from '@ngrx/store'; 6 | import {AppState, Todo, TodoModel} from "./common/interfaces"; 7 | import {Observable} from "rxjs/Observable"; 8 | import {ADD_TODO, REMOVE_TODO, TOGGLE_TODO} from './common/actions'; 9 | 10 | @Component({ 11 | selector: `todo-app`, 12 | template: ` 13 |
    14 | 20 |
    21 | 23 | 24 | 26 | 27 | 31 | 32 |
    33 |
    34 | `, 35 | directives: [TodoList, TodoInput, FilterSelect], 36 | changeDetection: ChangeDetectionStrategy.OnPush 37 | }) 38 | export class TodoApp { 39 | public todosModel$ : Observable; 40 | //faking an id for demo purposes 41 | private id: number = 0; 42 | 43 | constructor( 44 | private _store : Store 45 | ){ 46 | const todos$ = _store.select>('todos'); 47 | const visibilityFilter$ = _store.select('visibilityFilter'); 48 | /* 49 | Each time todos or visibilityFilter emits a new value, get the last emitted value from the other observable. 50 | This projection could be moved into a service or exported independantly and applied with the 'let' operator. 51 | For more on projecting state: https://gist.github.com/btroncone/a6e4347326749f938510#projecting-state-for-view-with-combinelatest-and-withlatestfrom 52 | For more on selectors: https://gist.github.com/btroncone/a6e4347326749f938510#extracting-selectors-for-reuse 53 | For more on combineLatest: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35#combinelatest 54 | */ 55 | this.todosModel$ = Observable 56 | .combineLatest( 57 | todos$, 58 | visibilityFilter$, 59 | (todos : Array, visibilityFilter : any) => { 60 | return { 61 | filteredTodos: todos.filter(visibilityFilter), 62 | totalTodos: todos.length, 63 | completedTodos: todos.filter((todo : Todo) => todo.complete).length 64 | } 65 | } 66 | ); 67 | } 68 | /* 69 | All state updates occur through dispatched actions. 70 | For demo purpose we are dispatching actions from container component but this could just as easily be done in services, or handled with ngrx/effect. 71 | The store can also be subscribed directly to 'action streams' for the same result. 72 | ex: action$.subscribe(_store) 73 | */ 74 | addTodo(description : string){ 75 | this._store.dispatch({type: ADD_TODO, payload: { 76 | id: ++this.id, 77 | description, 78 | complete: false 79 | }}); 80 | } 81 | 82 | removeTodo(id : number){ 83 | this._store.dispatch({type: REMOVE_TODO, payload: id}); 84 | } 85 | 86 | toggleTodo(id : number){ 87 | this._store.dispatch({type: TOGGLE_TODO, payload: id}); 88 | } 89 | 90 | updateFilter(filter){ 91 | this._store.dispatch({type: filter}); 92 | } 93 | } -------------------------------------------------------------------------------- /todos/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%= o.webpackConfig.metadata.title %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 19 | 20 | {% } %} 21 | 22 | 23 | 24 | 25 | Loading... 26 | 27 | {% if (o.webpackConfig.metadata.ENV === 'development') { %} 28 | 29 | 30 | {% } %} 31 | 32 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 33 | 34 | {% } %} 35 | 36 | -------------------------------------------------------------------------------- /todos/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6'; 2 | import 'core-js/es7/reflect'; 3 | import 'ts-helpers'; 4 | import 'rxjs'; 5 | require('zone.js/dist/zone'); 6 | require('zone.js/dist/long-stack-trace-zone'); -------------------------------------------------------------------------------- /todos/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | a:hover, 12 | a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | h3 { 17 | font-weight: 100; 18 | } 19 | 20 | /* LAYOUT CSS */ 21 | .pure-img-responsive { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | #layout { 27 | padding: 0; 28 | } 29 | 30 | .header { 31 | text-align: center; 32 | top: auto; 33 | margin: 3em auto; 34 | } 35 | 36 | .sidebar { 37 | background: rgb(61, 79, 93); 38 | color: #fff; 39 | } 40 | 41 | .brand-title, 42 | .brand-tagline { 43 | margin: 0; 44 | } 45 | .brand-title { 46 | text-transform: uppercase; 47 | } 48 | .brand-tagline { 49 | font-weight: 300; 50 | color: rgb(176, 202, 219); 51 | } 52 | 53 | .nav-list { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | .nav-item { 59 | display: inline-block; 60 | *display: inline; 61 | zoom: 1; 62 | } 63 | .nav-item a { 64 | background: transparent; 65 | border: 2px solid rgb(176, 202, 219); 66 | color: #fff; 67 | margin-top: 1em; 68 | letter-spacing: 0.05em; 69 | text-transform: uppercase; 70 | font-size: 85%; 71 | } 72 | .nav-item a:hover, 73 | .nav-item a:focus { 74 | border: 2px solid rgb(61, 146, 201); 75 | text-decoration: none; 76 | } 77 | 78 | .content-subhead { 79 | text-transform: uppercase; 80 | color: #aaa; 81 | border-bottom: 1px solid #eee; 82 | padding: 0.4em 0; 83 | font-size: 80%; 84 | font-weight: 500; 85 | letter-spacing: 0.1em; 86 | } 87 | 88 | .content { 89 | padding: 2em 1em 0; 90 | } 91 | 92 | .post { 93 | padding-bottom: 2em; 94 | } 95 | .post-title { 96 | font-size: 2em; 97 | color: #222; 98 | margin-bottom: 0.2em; 99 | } 100 | .post-avatar { 101 | border-radius: 50px; 102 | float: right; 103 | margin-left: 1em; 104 | } 105 | .post-description { 106 | font-family: Georgia, "Cambria", serif; 107 | color: #444; 108 | line-height: 1.8em; 109 | } 110 | .post-meta { 111 | color: #999; 112 | font-size: 90%; 113 | margin: 0; 114 | } 115 | 116 | .post-category { 117 | margin: 0 0.1em; 118 | padding: 0.3em 1em; 119 | color: #fff; 120 | background: #999; 121 | font-size: 80%; 122 | } 123 | .post-category-design { 124 | background: #5aba59; 125 | } 126 | .post-category-pure { 127 | background: #4d85d1; 128 | } 129 | .post-category-yui { 130 | background: #8156a7; 131 | } 132 | .post-category-js { 133 | background: #df2d4f; 134 | } 135 | 136 | .post-images { 137 | margin: 1em 0; 138 | } 139 | .post-image-meta { 140 | margin-top: -3.5em; 141 | margin-left: 1em; 142 | color: #fff; 143 | text-shadow: 0 1px 1px #333; 144 | } 145 | 146 | .footer { 147 | text-align: center; 148 | padding: 1em 0; 149 | } 150 | .footer a { 151 | color: #ccc; 152 | font-size: 80%; 153 | } 154 | .footer .pure-menu a:hover, 155 | .footer .pure-menu a:focus { 156 | background: none; 157 | } 158 | 159 | @media (min-width: 48em) { 160 | .content { 161 | padding: 2em 3em 0; 162 | margin-left: 25%; 163 | } 164 | 165 | .header { 166 | margin: 80% 2em 0; 167 | text-align: right; 168 | } 169 | 170 | .sidebar { 171 | position: fixed; 172 | top: 0; 173 | bottom: 0; 174 | } 175 | } 176 | 177 | .complete{ 178 | text-decoration: line-through; 179 | } 180 | 181 | .margin-t-20{ 182 | margin-top:20px; 183 | } 184 | 185 | .button-error { 186 | background: rgb(202, 60, 60); /* this is a maroon */ 187 | } 188 | 189 | -------------------------------------------------------------------------------- /todos/src/vendor.ts: -------------------------------------------------------------------------------- 1 | import '@angular/platform-browser'; 2 | import '@angular/platform-browser-dynamic'; 3 | import '@angular/core'; 4 | import '@angular/common'; 5 | import '@angular/http'; 6 | import '@angular/router-deprecated'; 7 | import 'rxjs'; -------------------------------------------------------------------------------- /todos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "sourceMap": true 9 | }, 10 | "exclude":[ 11 | "node_modules", 12 | "typings/main.d.ts", 13 | "typings/main" 14 | ], 15 | "filesGlob": [ 16 | "./src/**/*.ts", 17 | "!./node_modules/**/*.ts", 18 | "typings/browser.d.ts" 19 | ], 20 | "compileOnSave": false, 21 | "buildOnSave": false 22 | } -------------------------------------------------------------------------------- /todos/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "es6-promise": "github:typed-typings/npm-es6-promise#fb04188767acfec1defd054fc8024fafa5cd4de7" 4 | }, 5 | "devDependencies": {}, 6 | "ambientDependencies": { 7 | "angular-protractor": "github:DefinitelyTyped/DefinitelyTyped/angular-protractor/angular-protractor.d.ts#64b25f63f0ec821040a5d3e049a976865062ed9d", 8 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2", 9 | "hammerjs": "github:DefinitelyTyped/DefinitelyTyped/hammerjs/hammerjs.d.ts#74a4dfc1bc2dfadec47b8aae953b28546cb9c6b7", 10 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c", 11 | "ng2": "github:gdi2290/typings-ng2/ng2.d.ts#32998ff5584c0eab0cd9dc7704abb1c5c450701c", 12 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#8cf8164641be73e8f1e652c2a5b967c7210b6729", 13 | "selenium-webdriver": "github:DefinitelyTyped/DefinitelyTyped/selenium-webdriver/selenium-webdriver.d.ts#a83677ed13add14c2ab06c7325d182d0ba2784ea", 14 | "webpack": "github:DefinitelyTyped/DefinitelyTyped/webpack/webpack.d.ts#95c02169ba8fa58ac1092422efbd2e3174a206f4", 15 | "zone.js": "github:DefinitelyTyped/DefinitelyTyped/zone.js/zone.js.d.ts#c393f8974d44840a6c9cc6d5b5c0188a8f05143d" 16 | } 17 | } -------------------------------------------------------------------------------- /todos/wallaby.js: -------------------------------------------------------------------------------- 1 | var wallabyWebpack = require('wallaby-webpack'); 2 | 3 | var webpackPostprocessor = wallabyWebpack({ 4 | entryPatterns: [ 5 | 'spec-bundle.js', 6 | 'src/**/*spec.js' 7 | ] 8 | }); 9 | 10 | module.exports = function (w) { 11 | 12 | return { 13 | files: [ 14 | {pattern: 'spec-bundle.js', load: false}, 15 | {pattern: 'src/**/*.ts', load: false}, 16 | {pattern: 'src/**/*spec.ts', ignore: true} 17 | ], 18 | 19 | tests: [ 20 | { pattern: 'src/**/*spec.ts', load: false } 21 | ], 22 | 23 | testFramework: "jasmine", 24 | 25 | compilers: { 26 | '**/*.ts': w.compilers.typeScript({ 27 | emitDecoratorMetadata: true, 28 | experimentalDecorators: true 29 | }) 30 | }, 31 | 32 | postprocessor: webpackPostprocessor, 33 | 34 | bootstrap: function () { 35 | window.__moduleBundler.loadTests(); 36 | } 37 | }; 38 | }; -------------------------------------------------------------------------------- /todos/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @AngularClass 2 | 3 | /* 4 | * Helper: root(), and rootDir() are defined at the bottom 5 | */ 6 | var path = require('path'); 7 | var webpack = require('webpack'); 8 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | var ENV = process.env.ENV = process.env.NODE_ENV = 'development'; 11 | 12 | var metadata = { 13 | title: 'NgRx Example #2 - Todos', 14 | baseUrl: '/', 15 | host: 'localhost', 16 | port: 3000, 17 | ENV: ENV 18 | }; 19 | /* 20 | * Config 21 | */ 22 | module.exports = { 23 | // static data for index.html 24 | metadata: metadata, 25 | // for faster builds use 'eval' 26 | devtool: 'source-map', 27 | debug: true, 28 | // cache: false, 29 | 30 | // our angular app 31 | entry: { 'polyfills': './src/polyfills.ts', 'main': './src/app/bootstrap.ts' }, 32 | 33 | // Config for our build files 34 | output: { 35 | path: root('dist'), 36 | filename: '[name].bundle.js', 37 | sourceMapFilename: '[name].map', 38 | chunkFilename: '[id].chunk.js' 39 | }, 40 | 41 | 42 | resolve: { 43 | // ensure loader extensions match 44 | extensions: prepend(['.ts','.js','.json','.css','.html'], '.async') // ensure .async.ts etc also works 45 | }, 46 | 47 | module: { 48 | preLoaders: [ 49 | { test: /\.js$/, loader: "source-map-loader", exclude: [ root('node_modules/rxjs') ] } 50 | ], 51 | loaders: [ 52 | // Support Angular 2 async routes via .async.ts 53 | { test: /\.async\.ts$/, loaders: ['es6-promise-loader', 'ts-loader'], exclude: [ /\.(spec|e2e)\.ts$/ ] }, 54 | 55 | // Support for .ts files. 56 | { test: /\.ts$/, loader: 'ts-loader', exclude: [ /\.(spec|e2e|async)\.ts$/ ] }, 57 | 58 | // Support for *.json files. 59 | { test: /\.json$/, loader: 'json-loader' }, 60 | 61 | // Support for CSS as raw text 62 | { test: /\.css$/, loader: 'raw-loader' }, 63 | 64 | // support for .html as raw text 65 | { test: /\.html$/, loader: 'raw-loader' } 66 | 67 | // if you add a loader include the resolve file extension above 68 | ] 69 | }, 70 | 71 | plugins: [ 72 | new webpack.optimize.OccurenceOrderPlugin(true), 73 | new webpack.optimize.CommonsChunkPlugin({ name: 'polyfills', filename: 'polyfills.bundle.js', minChunks: Infinity }), 74 | // static assets 75 | new CopyWebpackPlugin([ { from: 'src/assets', to: 'assets' } ]), 76 | // generating html 77 | new HtmlWebpackPlugin({ template: 'src/index.html', inject: false }), 78 | // replace 79 | new webpack.DefinePlugin({ 80 | 'process.env': { 81 | 'ENV': JSON.stringify(metadata.ENV), 82 | 'NODE_ENV': JSON.stringify(metadata.ENV) 83 | } 84 | }) 85 | ], 86 | 87 | // Other module loader config 88 | tslint: { 89 | emitErrors: false, 90 | failOnHint: false, 91 | resourcePath: 'src' 92 | }, 93 | // our Webpack Development Server config 94 | devServer: { 95 | port: metadata.port, 96 | host: metadata.host, 97 | // contentBase: 'src/', 98 | historyApiFallback: true, 99 | watchOptions: { aggregateTimeout: 300, poll: 1000 } 100 | }, 101 | // we need this due to problems with es6-shim 102 | node: {global: 'window', progress: false, crypto: 'empty', module: false, clearImmediate: false, setImmediate: false} 103 | }; 104 | 105 | // Helper functions 106 | 107 | function root(args) { 108 | args = Array.prototype.slice.call(arguments, 0); 109 | return path.join.apply(path, [__dirname].concat(args)); 110 | } 111 | 112 | function prepend(extensions, args) { 113 | args = args || []; 114 | if (!Array.isArray(args)) { args = [args] } 115 | return extensions.reduce(function(memo, val) { 116 | return memo.concat(val, args.map(function(prefix) { 117 | return prefix + val 118 | })); 119 | }, ['']); 120 | } 121 | function rootNode(args) { 122 | args = Array.prototype.slice.call(arguments, 0); 123 | return root.apply(path, ['node_modules'].concat(args)); 124 | } --------------------------------------------------------------------------------