├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------