├── .eslintrc.yml ├── .gitignore ├── LICENSE ├── README.md ├── circle.yml ├── karma.conf.js ├── karma.entry.js ├── package.json ├── server ├── main.js └── router.js ├── src ├── app │ ├── components │ │ ├── app.scss │ │ └── app.ts │ └── index.ts ├── index.html ├── main.ts ├── polyfills.ts ├── shared │ ├── index.ts │ ├── services │ │ └── api │ │ │ ├── api-service.ts │ │ │ ├── constants.ts │ │ │ └── index.ts │ └── styles │ │ ├── _base.scss │ │ ├── _grid.scss │ │ ├── _settings.scss │ │ └── styles.scss └── tasks │ ├── components │ ├── task-form.scss │ ├── task-form.ts │ ├── task-item.html │ ├── task-item.scss │ ├── task-item.ts │ ├── task-list.scss │ ├── task-list.ts │ └── tasks.ts │ ├── directives │ └── autofocus-directive.ts │ ├── index.ts │ ├── pipes │ ├── task-list-filter-pipe.spec.ts │ └── task-list-filter-pipe.ts │ ├── task-actions.ts │ ├── task-effects.ts │ ├── task-reducer.ts │ ├── task-service.ts │ └── task.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | extends: "eslint:recommended" 4 | 5 | env: 6 | browser: true 7 | es6: true 8 | node: true 9 | 10 | globals: 11 | jasmine: true 12 | 13 | rules: 14 | array-bracket-spacing: 0 15 | array-callback-return: 0 16 | arrow-body-style: 0 17 | arrow-parens: [2, "as-needed"] 18 | arrow-spacing: [2, {before: true, after: true}] 19 | accessor-pairs: 0 20 | block-scoped-var: 0 21 | block-spacing: 2 22 | brace-style: [2, "stroustrup", {allowSingleLine: true}] 23 | callback-return: 0 24 | camelcase: [2, {properties: "always"}] 25 | comma-dangle: 2 26 | comma-spacing: 2 27 | comma-style: [2, "last"] 28 | complexity: [0, 10] 29 | computed-property-spacing: [2, "never"] 30 | consistent-return: 0 31 | consistent-this: 0 32 | constructor-super: 2 33 | curly: [2, "multi-line"] 34 | default-case: 2 35 | dot-location: [2, "property"] 36 | dot-notation: [2, {allowKeywords: true}] 37 | eol-last: 2 38 | eqeqeq: 2 39 | func-names: 0 40 | func-style: [2, "declaration", {allowArrowFunctions: true }] 41 | generator-star-spacing: [2, {before: false, after: true}] 42 | guard-for-in: 2 43 | global-require: 0 44 | handle-callback-err: 0 45 | id-blacklist: 0 46 | id-length: 0 47 | id-match: 0 48 | indent: [2, 2, {SwitchCase: 1, VariableDeclarator: 2}] 49 | init-declarations: 0 50 | jsx-quotes: 1 51 | key-spacing: [2, {beforeColon: false, afterColon: true}] 52 | keyword-spacing: 2 53 | linebreak-style: 0 54 | lines-around-comment: 0 55 | max-depth: 0 56 | max-len: 0 57 | max-nested-callbacks: 0 58 | max-params: 0 59 | max-statements: 0 60 | max-statements-per-line: 0 61 | new-cap: 2 62 | new-parens: 2 63 | newline-after-var: 0 64 | newline-before-return: 0 65 | newline-per-chained-call: 0 66 | no-alert: 2 67 | no-array-constructor: 2 68 | no-bitwise: 0 69 | no-caller: 2 70 | no-case-declarations: 2 71 | no-catch-shadow: 0 72 | no-class-assign: 2 73 | no-cond-assign: 2 74 | no-confusing-arrow: 0 75 | no-console: 2 76 | no-const-assign: 2 77 | no-constant-condition: 2 78 | no-continue: 0 79 | no-control-regex: 2 80 | no-debugger: 2 81 | no-delete-var: 2 82 | no-div-regex: 0 83 | no-dupe-args: 2 84 | no-dupe-class-members: 2 85 | no-dupe-keys: 2 86 | no-duplicate-case: 2 87 | no-duplicate-imports: 2 88 | no-else-return: 0 89 | no-empty: 2 90 | no-empty-character-class: 2 91 | no-empty-function: 0 92 | no-empty-pattern: 2 93 | no-eq-null: 0 94 | no-eval: 2 95 | no-ex-assign: 2 96 | no-extend-native: 2 97 | no-extra-bind: 2 98 | no-extra-boolean-cast: 2 99 | no-extra-label: 0 100 | no-extra-parens: 0 101 | no-extra-semi: 2 102 | no-fallthrough: 2 103 | no-floating-decimal: 2 104 | no-func-assign: 2 105 | no-implicit-coercion: 0 106 | no-implicit-globals: 0 107 | no-implied-eval: 2 108 | no-inline-comments: 0 109 | no-inner-declarations: 2 110 | no-invalid-regexp: 2 111 | no-invalid-this: 2 112 | no-irregular-whitespace: 2 113 | no-iterator: 2 114 | no-label-var: 2 115 | no-labels: 2 116 | no-lone-blocks: 0 117 | no-lonely-if: 0 118 | no-loop-func: 0 119 | no-magic-numbers: 0 120 | no-mixed-requires: 2 121 | no-mixed-spaces-and-tabs: [2, false] 122 | no-multi-spaces: 2 123 | no-multi-str: 2 124 | no-multiple-empty-lines: 0 125 | no-native-reassign: 2 126 | no-negated-condition: 0 127 | no-negated-in-lhs: 2 128 | no-nested-ternary: 2 129 | no-new: 2 130 | no-new-func: 2 131 | no-new-object: 2 132 | no-new-require: 2 133 | no-new-symbol: 2 134 | no-new-wrappers: 2 135 | no-obj-calls: 2 136 | no-octal: 2 137 | no-octal-escape: 2 138 | no-param-reassign: 0 139 | no-path-concat: 2 140 | no-plusplus: 0 141 | no-process-env: 0 142 | no-process-exit: 2 143 | no-proto: 2 144 | no-redeclare: 2 145 | no-regex-spaces: 2 146 | no-restricted-globals: 0 147 | no-restricted-imports: 0 148 | no-restricted-modules: 0 149 | no-restricted-syntax: 0 150 | no-return-assign: 0 151 | no-script-url: 2 152 | no-self-assign: 2 153 | no-self-compare: 0 154 | no-sequences: 2 155 | no-shadow: 0 156 | no-shadow-restricted-names: 2 157 | no-spaced-func: 2 158 | no-sparse-arrays: 2 159 | no-sync: 0 160 | no-ternary: 0 161 | no-trailing-spaces: 2 162 | no-this-before-super: 2 163 | no-throw-literal: 0 164 | no-undef: 2 165 | no-undef-init: 2 166 | no-undefined: 0 167 | no-unexpected-multiline: 2 168 | no-underscore-dangle: [2, {allowAfterThis: true}] 169 | no-unmodified-loop-condition: 2 170 | no-unneeded-ternary: 0 171 | no-unreachable: 2 172 | no-unsafe-finally: 0 173 | no-unused-expressions: 2 174 | no-unused-labels: 2 175 | no-unused-vars: [2, {vars: "all", args: "after-used"}] 176 | no-use-before-define: 0 177 | no-useless-call: 0 178 | no-useless-computed-key: 0 179 | no-useless-concat: 2 180 | no-useless-constructor: 0 181 | no-useless-escape: 0 182 | no-void: 0 183 | no-var: 0 184 | no-warning-comments: 0 185 | no-whitespace-before-property: 2 186 | no-with: 2 187 | object-curly-spacing: [0, "never"] 188 | object-shorthand: 0 189 | one-var: 0 190 | one-var-declaration-per-line: 0 191 | operator-assignment: 0 192 | operator-linebreak: 0 193 | padded-blocks: 0 194 | prefer-arrow-callback: 0 195 | prefer-const: 0 196 | prefer-reflect: 0 197 | prefer-rest-params: 0 198 | prefer-spread: 0 199 | prefer-template: 0 200 | quote-props: 0 201 | quotes: [2, "single"] 202 | radix: 0 203 | require-jsdoc: 0 204 | require-yield: 0 205 | semi: 2 206 | semi-spacing: [2, {before: false, after: true}] 207 | sort-imports: 0 208 | sort-vars: 0 209 | space-before-blocks: 2 210 | space-before-function-paren: [2, "never"] 211 | space-in-parens: 0 212 | space-infix-ops: 2 213 | space-unary-ops: [2, {words: true, nonwords: false}] 214 | spaced-comment: [2, "always", {exceptions: ["-", "="]}] 215 | strict: 0 216 | template-curly-spacing: 0 217 | use-isnan: 2 218 | valid-jsdoc: [2, {prefer: {return: "returns"}}] 219 | valid-typeof: 2 220 | vars-on-top: 0 221 | wrap-iife: 2 222 | wrap-regex: 0 223 | yield-star-spacing: 0 224 | yoda: [2, "never"] 225 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #====================================== 2 | # Files and Directories 3 | #-------------------------------------- 4 | coverage/ 5 | node_modules/ 6 | server/db.json 7 | target/ 8 | tmp/ 9 | 10 | 11 | #====================================== 12 | # Extensions 13 | #-------------------------------------- 14 | *.gz 15 | *.log 16 | *.rar 17 | *.tar 18 | *.zip 19 | 20 | 21 | #====================================== 22 | # IDE generated 23 | #-------------------------------------- 24 | .idea/ 25 | .project 26 | *.iml 27 | 28 | 29 | #====================================== 30 | # OS generated 31 | #-------------------------------------- 32 | __MACOSX/ 33 | .DS_Store 34 | Thumbs.db 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Richard Park 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/r-park/todo-angular2-ngrx.svg?style=shield&circle-token=dd1325d92e93517cb5c52669e92c7f1698b55bc0)](https://circleci.com/gh/r-park/todo-angular2-ngrx) 2 | 3 | 4 | # Todo app with Angular2 and ngrx/store 5 | A simple Todo app example featuring [ngrx/store](https://github.com/ngrx/store) — RxJS powered state management inspired by Redux for Angular2 apps. 6 | 7 | 8 | Stack 9 | ----- 10 | 11 | - Angular 2 12 | - [ngrx/store](https://github.com/ngrx/store) 13 | - [ngrx/effects](https://github.com/ngrx/effects) 14 | - [ngrx/store-devtools](https://github.com/ngrx/store-devtools) 15 | - RxJS 16 | - SASS 17 | - Typescript 2 18 | - Webpack 2 19 | 20 | 21 | Getting Started 22 | --------------- 23 | 24 | #### Recommended 25 | - `node >= 6` 26 | 27 | #### Quick Start 28 | ```shell 29 | $ git clone https://github.com/r-park/todo-angular2-ngrx.git 30 | $ cd todo-angular2-ngrx 31 | $ npm install 32 | $ npm start 33 | ``` 34 | 35 | 36 | Usage 37 | ----- 38 | 39 | |Script|Description| 40 | |---|---| 41 | |`npm start`|Start webpack development server @ `localhost:3000` and api server @ `localhost:3001`| 42 | |`npm run build`|Lint, test, and build the application to `./target`| 43 | |`npm run lint`|Lint `.ts` and `.js` files| 44 | |`npm run lint:js`|Lint `.js` files with eslint| 45 | |`npm run lint:ts`|Lint `.ts` files with tslint| 46 | |`npm test`|Run unit tests with Karma and Jasmine| 47 | |`npm run test:watch`|Run unit tests with Karma and Jasmine; watch for changes to re-run tests| 48 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.4 4 | 5 | dependencies: 6 | pre: 7 | - rm -rf node_modules 8 | 9 | test: 10 | override: 11 | - npm run build 12 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = config => { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | files: ['karma.entry.js'], 6 | 7 | preprocessors: { 8 | 'karma.entry.js': ['webpack', 'sourcemap'] 9 | }, 10 | 11 | webpack: require('./webpack.config'), 12 | 13 | webpackServer: { 14 | noInfo: true 15 | }, 16 | 17 | reporters: ['dots'], 18 | 19 | logLevel: config.LOG_INFO, 20 | 21 | autoWatch: true, 22 | 23 | singleRun: false, 24 | 25 | browsers: ['Chrome'] 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /karma.entry.js: -------------------------------------------------------------------------------- 1 | require('core-js/es6/array'); 2 | require('core-js/es6/map'); 3 | require('core-js/es6/set'); 4 | require('core-js/es6/string'); 5 | require('core-js/es6/symbol'); 6 | require('core-js/fn/object/assign'); 7 | require('core-js/es7/reflect'); 8 | 9 | require('zone.js/dist/zone'); 10 | require('zone.js/dist/long-stack-trace-zone'); 11 | require('zone.js/dist/proxy'); 12 | require('zone.js/dist/sync-test'); 13 | require('zone.js/dist/jasmine-patch'); 14 | require('zone.js/dist/async-test'); 15 | require('zone.js/dist/fake-async-test'); 16 | 17 | require('ts-helpers'); 18 | 19 | 20 | Error.stackTraceLimit = Infinity; 21 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; 22 | 23 | 24 | var browser = require('@angular/platform-browser-dynamic/testing'); 25 | var testing = require('@angular/core/testing'); 26 | 27 | testing.TestBed.initTestEnvironment( 28 | browser.BrowserDynamicTestingModule, 29 | browser.platformBrowserDynamicTesting() 30 | ); 31 | 32 | 33 | // Load spec files 34 | var context = require.context('./src', true, /\.spec\.ts/); 35 | context.keys().forEach(context); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-angular2-ngrx", 3 | "version": "0.1.0", 4 | "description": "todo-angular2-ngrx", 5 | "homepage": "https://github.com/r-park/todo-angular2-ngrx", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/r-park/todo-angular2-ngrx.git" 9 | }, 10 | "author": { 11 | "name": "Richard Park", 12 | "email": "objectiv@gmail.com" 13 | }, 14 | "license": "MIT", 15 | "private": true, 16 | "engines": { 17 | "node": ">=6.4" 18 | }, 19 | "scripts": { 20 | "build": "cross-env NODE_ENV=production webpack --display-chunks", 21 | "clean": "del-cli target", 22 | "lint": "run-s lint:js lint:ts", 23 | "lint:js": "eslint -c .eslintrc.yml *.js server", 24 | "lint:ts": "tslint 'src/**/*.ts'", 25 | "prebuild": "run-s clean test", 26 | "pretest": "run-s lint", 27 | "server:api": "cross-env NODE_ENV=development nodemon -w 'server/*.js' server/main.js", 28 | "server:dev": "cross-env NODE_ENV=development webpack-dev-server", 29 | "start": "npm run server:api | npm run server:dev", 30 | "test": "cross-env NODE_ENV=test karma start --single-run", 31 | "test:watch": "cross-env NODE_ENV=test karma start" 32 | }, 33 | "dependencies": { 34 | "@angular/common": "2.0.1", 35 | "@angular/compiler": "2.0.1", 36 | "@angular/core": "2.0.1", 37 | "@angular/forms": "2.0.1", 38 | "@angular/http": "2.0.1", 39 | "@angular/platform-browser": "2.0.1", 40 | "@angular/platform-browser-dynamic": "2.0.1", 41 | "@angular/router": "3.0.1", 42 | "@ngrx/core": "1.2.0", 43 | "@ngrx/effects": "2.0.0", 44 | "@ngrx/store": "2.2.1", 45 | "core-js": "~2.4.1", 46 | "rxjs": "5.0.0-beta.12", 47 | "zone.js": "~0.6.25" 48 | }, 49 | "devDependencies": { 50 | "@ngrx/store-devtools": "~3.1.0", 51 | "@types/jasmine": "~2.2.34", 52 | "@types/node": "~6.0.41", 53 | "autoprefixer": "~6.5.0", 54 | "body-parser": "~1.15.2", 55 | "cross-env": "~3.0.0", 56 | "css-loader": "~0.25.0", 57 | "del-cli": "~0.2.0", 58 | "eslint": "~3.6.1", 59 | "express": "~4.14.0", 60 | "extract-text-webpack-plugin": "2.0.0-beta.4", 61 | "html-webpack-plugin": "~2.22.0", 62 | "jasmine-core": "~2.5.2", 63 | "karma": "~1.3.0", 64 | "karma-chrome-launcher": "~2.0.0", 65 | "karma-jasmine": "~1.0.2", 66 | "karma-sourcemap-loader": "~0.3.7", 67 | "karma-webpack": "~1.8.0", 68 | "lowdb": "~0.13.1", 69 | "minx": "r-park/minx.git", 70 | "morgan": "~1.7.0", 71 | "node-sass": "~3.10.0", 72 | "node-uuid": "~1.4.7", 73 | "nodemon": "~1.10.2", 74 | "npm-run-all": "~3.1.0", 75 | "postcss-loader": "~0.13.0", 76 | "raw-loader": "~0.5.1", 77 | "sass-loader": "~4.0.2", 78 | "style-loader": "~0.13.1", 79 | "ts-helpers": "~1.1.1", 80 | "ts-loader": "~0.8.2", 81 | "tslint": "~3.15.1", 82 | "typescript": "~2.0.3", 83 | "webpack": "2.1.0-beta.25", 84 | "webpack-dev-server": "2.1.0-beta.4", 85 | "webpack-md5-hash": "~0.0.5", 86 | "winston": "~2.2.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const express = require('express'); 3 | const logger = require('winston'); 4 | const morgan = require('morgan'); 5 | 6 | 7 | //========================================================= 8 | // SETUP SERVER 9 | //--------------------------------------------------------- 10 | const app = express(); 11 | 12 | // server address 13 | app.set('host', 'localhost'); 14 | app.set('port', 3001); 15 | 16 | app.use(morgan('dev')); 17 | 18 | app.use(bodyParser.text()); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({extended: true})); 21 | 22 | app.use(require('./router')); 23 | 24 | 25 | //========================================================= 26 | // START SERVER 27 | //--------------------------------------------------------- 28 | app.listen(app.get('port'), app.get('host'), error => { 29 | if (error) { 30 | logger.error(error); 31 | } 32 | else { 33 | logger.info(`Server listening @ ${app.get('host')}:${app.get('port')}`); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /server/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const low = require('lowdb'); 5 | const path = require('path'); 6 | const storage = require('lowdb/lib/file-sync'); 7 | const uuid = require('node-uuid'); 8 | 9 | 10 | //========================================================= 11 | // DATABASE 12 | //--------------------------------------------------------- 13 | const db = low(path.join(__dirname, 'db.json'), {storage}); 14 | db.set('tasks', []).value(); 15 | 16 | 17 | //========================================================= 18 | // ROUTER 19 | //--------------------------------------------------------- 20 | const router = new express.Router(); 21 | module.exports = router; 22 | 23 | 24 | router.use((req, res, next) => { 25 | res.header('Access-Control-Allow-Origin', '*'); 26 | res.header('Access-Control-Allow-Methods', 'GET, DELETE, OPTIONS, POST, PUT'); 27 | res.header('Access-Control-Allow-Headers', 'Accept, Content-Type, Origin, X-Requested-With'); 28 | next(); 29 | }); 30 | 31 | 32 | router.post('/tasks', (req, res) => { 33 | let data = req.body; 34 | data.id = uuid.v4(); 35 | let task = db.get('tasks').push(data).last().value(); 36 | res.status(200).json(task); 37 | }); 38 | 39 | 40 | router.get('/tasks', (req, res) => { 41 | res.status(200).json(db.get('tasks').value()); 42 | }); 43 | 44 | 45 | router.get('/tasks/:id', (req, res) => { 46 | res.status(200); 47 | }); 48 | 49 | 50 | router.put('/tasks/:id', (req, res) => { 51 | let id = req.params.id; 52 | let task = db.get('tasks').find({id}).assign(req.body).value(); 53 | res.status(200).json(task); 54 | }); 55 | 56 | 57 | router.delete('/tasks/:id', (req, res) => { 58 | let id = req.params.id; 59 | let task = db.get('tasks').find({id}).value(); 60 | db.get('tasks').remove({id}).value(); 61 | res.status(200).json(task); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/components/app.scss: -------------------------------------------------------------------------------- 1 | @import 2 | "~minx/src/settings", 3 | "~minx/src/functions", 4 | "~minx/src/mixins"; 5 | 6 | //=================================================================== 7 | // APP 8 | //=================================================================== 9 | 10 | .header { 11 | padding: 10px 0; 12 | height: 60px; 13 | overflow: hidden; 14 | line-height: 40px; 15 | } 16 | 17 | 18 | .header__title { 19 | @include fa-icon(circle-o); 20 | float: left; 21 | font-size: rem(14px); 22 | font-weight: 400; 23 | text-rendering: auto; 24 | transform: translate(0,0); 25 | 26 | &:before { 27 | padding-right: 5px; 28 | color: #fff; 29 | font-family: 'FontAwesome'; 30 | line-height: 20px; 31 | } 32 | } 33 | 34 | 35 | .header__link { 36 | @include fa-icon(github); 37 | display: block; 38 | float: right; 39 | color: inherit; 40 | font-size: rem(24px); 41 | text-decoration: none; 42 | text-rendering: auto; 43 | transform: translate(0,0); 44 | 45 | &:before { 46 | font-family: 'FontAwesome'; 47 | line-height: 24px; 48 | } 49 | } 50 | 51 | 52 | .main { 53 | padding-bottom: 90px; 54 | } 55 | -------------------------------------------------------------------------------- /src/app/components/app.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | selector: 'app', 6 | styles: [ 7 | require('./app.scss') 8 | ], 9 | 10 | template: ` 11 |
12 |
13 |
14 |

Todo Angular2 NgRx

15 | 16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | ` 24 | }) 25 | 26 | export class AppComponent {} 27 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | import { StoreModule } from '@ngrx/store'; 6 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 7 | 8 | import { TasksModule, taskReducer } from 'src/tasks'; 9 | import { AppComponent } from './components/app'; 10 | 11 | 12 | const routes: Routes = [ 13 | {path: '', pathMatch: 'full', redirectTo: 'tasks'} 14 | ]; 15 | 16 | 17 | @NgModule({ 18 | bootstrap: [ 19 | AppComponent 20 | ], 21 | declarations: [ 22 | AppComponent 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | RouterModule.forRoot(routes, {useHash: false}), 27 | StoreModule.provideStore({ 28 | tasks: taskReducer 29 | }), 30 | StoreDevtoolsModule.instrumentOnlyWithExtension(), 31 | TasksModule 32 | ], 33 | providers: [ 34 | {provide: APP_BASE_HREF, useValue: '/'} 35 | ] 36 | }) 37 | export class AppModule {} 38 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Todo Angular2 NgRx 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | // Root module 5 | import { AppModule } from './app'; 6 | 7 | // shared styles 8 | import './shared/styles/styles.scss'; 9 | 10 | 11 | if (process.env.NODE_ENV === 'production') { 12 | enableProdMode(); 13 | } 14 | 15 | 16 | document.addEventListener('DOMContentLoaded', () => { 17 | platformBrowserDynamic().bootstrapModule(AppModule); 18 | }); 19 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // Core-JS 2 | import 'core-js/es6/array'; 3 | import 'core-js/es6/map'; 4 | import 'core-js/es6/set'; 5 | import 'core-js/es6/string'; 6 | import 'core-js/es6/symbol'; 7 | import 'core-js/es7/reflect'; 8 | import 'core-js/fn/object/assign'; 9 | 10 | // Zone 11 | import 'zone.js/dist/zone'; 12 | 13 | // Typescript helpers 14 | import 'ts-helpers'; 15 | 16 | 17 | if (process.env.NODE_ENV === 'development') { 18 | Error.stackTraceLimit = Infinity; 19 | require('zone.js/dist/long-stack-trace-zone'); 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpModule } from '@angular/http'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { ApiService } from './services/api'; 6 | 7 | 8 | @NgModule({ 9 | exports: [ 10 | CommonModule 11 | ], 12 | imports: [ 13 | CommonModule, 14 | HttpModule 15 | ], 16 | providers: [ 17 | ApiService 18 | ] 19 | }) 20 | export class SharedModule {} 21 | 22 | 23 | export { ApiService }; 24 | -------------------------------------------------------------------------------- /src/shared/services/api/api-service.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/operator/map'; 2 | 3 | import { Injectable } from '@angular/core'; 4 | import { Headers, Http, Request, RequestMethod, Response } from '@angular/http'; 5 | import { Observable } from 'rxjs/Observable'; 6 | import { Task } from 'src/tasks'; 7 | import { API_TASKS_URL } from './constants'; 8 | 9 | 10 | @Injectable() 11 | export class ApiService { 12 | constructor(private http: Http) {} 13 | 14 | createTask(body: any): Observable { 15 | return this.request({ 16 | body, 17 | method: RequestMethod.Post, 18 | url: API_TASKS_URL 19 | }); 20 | } 21 | 22 | deleteTask(taskId: string): Observable { 23 | return this.request({ 24 | method: RequestMethod.Delete, 25 | url: `${API_TASKS_URL}/${taskId}` 26 | }); 27 | } 28 | 29 | fetchTasks(): Observable { 30 | return this.request({ 31 | method: RequestMethod.Get, 32 | url: API_TASKS_URL 33 | }); 34 | } 35 | 36 | updateTask(taskId: string, body: any): Observable { 37 | return this.request({ 38 | body, 39 | method: RequestMethod.Put, 40 | url: `${API_TASKS_URL}/${taskId}` 41 | }); 42 | } 43 | 44 | request(options: any): Observable { 45 | if (options.body) { 46 | if (typeof options.body !== 'string') { 47 | options.body = JSON.stringify(options.body); 48 | } 49 | 50 | options.headers = new Headers({ 51 | 'Content-Type': 'application/json' 52 | }); 53 | } 54 | 55 | return this.http.request(new Request(options)) 56 | .map((res: Response) => res.json()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/shared/services/api/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_BASE_URL: string = 'http://localhost:3001'; 2 | export const API_TASKS_URL: string = `${API_BASE_URL}/tasks`; 3 | -------------------------------------------------------------------------------- /src/shared/services/api/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiService } from './api-service'; 2 | -------------------------------------------------------------------------------- /src/shared/styles/_base.scss: -------------------------------------------------------------------------------- 1 | @import 2 | 'settings', 3 | '~minx/src/settings', 4 | '~minx/src/functions', 5 | '~minx/src/mixins'; 6 | -------------------------------------------------------------------------------- /src/shared/styles/_grid.scss: -------------------------------------------------------------------------------- 1 | //=================================================================== 2 | // GRID 3 | //=================================================================== 4 | 5 | .g-row { 6 | @include grid-row; 7 | } 8 | 9 | .g-col { 10 | @include grid-column; 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/styles/_settings.scss: -------------------------------------------------------------------------------- 1 | //=================================================================== 2 | // SETTINGS 3 | //=================================================================== 4 | 5 | $base-background-color: #222 !default; 6 | $base-font-color: #999 !default; 7 | $base-font-family: 'aktiv-grotesk-std', Helvetica Neue, Arial, sans-serif !default; 8 | $base-font-size: 18px !default; 9 | $base-line-height: 24px !default; 10 | 11 | 12 | //=============================================== 13 | // GRID 14 | //=============================================== 15 | $grid-max-width: 810px !default; 16 | -------------------------------------------------------------------------------- /src/shared/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import 2 | 'base', 3 | '~minx/src/reset', 4 | '~minx/src/elements', 5 | 'grid'; 6 | 7 | 8 | [hidden] { 9 | display: none !important; 10 | } 11 | 12 | ::selection { 13 | background: rgba(200,200,255,.1); 14 | } 15 | -------------------------------------------------------------------------------- /src/tasks/components/task-form.scss: -------------------------------------------------------------------------------- 1 | @import 2 | "~minx/src/settings", 3 | "~minx/src/functions", 4 | "~minx/src/mixins"; 5 | 6 | //=================================================================== 7 | // TASK-FORM 8 | //=================================================================== 9 | 10 | .task-form { 11 | margin: 40px 0 10px; 12 | 13 | @include media-query(540) { 14 | margin: 80px 0 20px; 15 | } 16 | } 17 | 18 | 19 | .task-form__input { 20 | outline: none; 21 | border: 0; 22 | border-bottom: 1px dotted #666; 23 | border-radius: 0; 24 | padding: 0 0 5px 0; 25 | width: 100%; 26 | height: 50px; 27 | font-family: inherit; 28 | font-size: rem(24px); 29 | font-weight: 300; 30 | color: #fff; 31 | background: transparent; 32 | 33 | @include media-query(540) { 34 | height: 61px; 35 | font-size: rem(32px); 36 | } 37 | 38 | &::placeholder { 39 | color: #999; 40 | opacity: 1; // firefox native placeholder style has opacity < 1 41 | } 42 | 43 | &:focus::placeholder { 44 | color: #777; 45 | opacity: 1; 46 | } 47 | 48 | // webkit input doesn't inherit font-smoothing from ancestors 49 | -webkit-font-smoothing: antialiased; 50 | 51 | // remove `x` 52 | &::-ms-clear { 53 | display: none; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/tasks/components/task-form.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | changeDetection: ChangeDetectionStrategy.OnPush, 6 | selector: 'task-form', 7 | styles: [ 8 | require('./task-form.scss') 9 | ], 10 | template: ` 11 |
12 | 22 |
23 | ` 24 | }) 25 | 26 | export class TaskFormComponent { 27 | @Output() createTask: EventEmitter = new EventEmitter(false); 28 | 29 | title: string = ''; 30 | 31 | clear(): void { 32 | this.title = ''; 33 | } 34 | 35 | submit(): void { 36 | const title: string = this.title.trim(); 37 | if (title.length) { 38 | this.createTask.emit(title); 39 | } 40 | this.clear(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/tasks/components/task-item.html: -------------------------------------------------------------------------------- 1 |
5 | 6 |
7 | 15 |
16 | 17 |
18 |
22 | {{ task.title }} 23 |
24 | 25 |
26 | 35 |
36 |
37 | 38 |
39 | 47 | 55 | 63 |
64 |
65 | -------------------------------------------------------------------------------- /src/tasks/components/task-item.scss: -------------------------------------------------------------------------------- 1 | @import 2 | "~minx/src/settings", 3 | "~minx/src/functions", 4 | "~minx/src/mixins"; 5 | 6 | //=================================================================== 7 | // TASK-ITEM 8 | //=================================================================== 9 | 10 | .task-item { 11 | display: flex; 12 | outline: none; 13 | border-bottom: 1px dotted #666; 14 | height: 60px; 15 | overflow: hidden; 16 | color: #fff; 17 | font-size: rem(18px); 18 | font-weight: 300; 19 | 20 | @include media-query(540) { 21 | font-size: rem(24px); 22 | } 23 | } 24 | 25 | .task-item--editing { 26 | border-bottom: 1px dotted #ccc; 27 | } 28 | 29 | 30 | //===================================== 31 | // CELLS 32 | //------------------------------------- 33 | .cell { 34 | &:first-child, 35 | &:last-child { 36 | display: flex; 37 | flex: 0 0 auto; 38 | align-items: center; 39 | } 40 | 41 | &:first-child { 42 | padding-right: 20px; 43 | } 44 | 45 | &:nth-child(2) { 46 | flex: 1; 47 | padding-right: 30px; 48 | overflow: hidden; 49 | } 50 | } 51 | 52 | 53 | //===================================== 54 | // ICON BUTTONS 55 | //------------------------------------- 56 | .task-item__button { 57 | @include button-base; 58 | margin-left: 5px; 59 | outline: none; 60 | border: 0; 61 | border-radius: 100px; 62 | padding: 0; 63 | width: 40px; 64 | height: 40px; 65 | overflow: hidden; 66 | background: #2a2a2a; 67 | transform: translate(0, 0); 68 | 69 | &:first-child { 70 | margin: 0; 71 | } 72 | } 73 | 74 | .icon { 75 | line-height: 40px !important; 76 | color: #555; 77 | 78 | .task-item__button:hover & { 79 | color: #999; 80 | } 81 | } 82 | 83 | .icon--active { 84 | &, .task-item__button:hover & { 85 | color: #85bf6b; 86 | } 87 | } 88 | 89 | 90 | //===================================== 91 | // TITLE : STATIC 92 | //------------------------------------- 93 | @keyframes fade-title { 94 | from { color: #fff; } 95 | to { color: #666; } 96 | } 97 | 98 | @keyframes strike-title { 99 | from { width: 0; } 100 | to { width: 100%; } 101 | } 102 | 103 | .task-item__title { 104 | display: inline-block; 105 | position: relative; 106 | max-width: 100%; 107 | line-height: 60px; 108 | outline: none; 109 | overflow: hidden; 110 | text-overflow: ellipsis; 111 | white-space: nowrap; 112 | 113 | &:after { 114 | position: absolute; 115 | left: 0; 116 | bottom: 0; 117 | border-top: 2px solid #85bf6b; 118 | width: 0; 119 | height: 46%; 120 | content: ''; 121 | } 122 | 123 | .task-item--completed & { 124 | color: #666; 125 | } 126 | 127 | .task-item--completed &:after { 128 | width: 100%; 129 | } 130 | 131 | .task-item--completed.task-item--status-updated & { 132 | animation: fade-title 120ms ease-in-out; 133 | } 134 | 135 | .task-item--completed.task-item--status-updated &:after { 136 | animation: strike-title 180ms ease-in-out; 137 | } 138 | } 139 | 140 | 141 | //===================================== 142 | // TITLE : INPUT 143 | //------------------------------------- 144 | .task-item__input { 145 | outline: none; 146 | border: 0; 147 | padding: 0; 148 | width: 100%; 149 | height: 60px; 150 | color: inherit; 151 | font: inherit; 152 | background: transparent; 153 | 154 | // remove `x` 155 | &::-ms-clear { 156 | display: none; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/tasks/components/task-item.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Task } from '../task'; 3 | 4 | 5 | @Component({ 6 | changeDetection: ChangeDetectionStrategy.OnPush, 7 | selector: 'task-item', 8 | styles: [ 9 | require('./task-item.scss') 10 | ], 11 | template: require('./task-item.html') 12 | }) 13 | 14 | export class TaskItemComponent { 15 | @Input() task: Task; 16 | @Output() remove: EventEmitter = new EventEmitter(false); 17 | @Output() update: EventEmitter = new EventEmitter(false); 18 | 19 | editing: boolean = false; 20 | title: string = ''; 21 | 22 | editTitle(): void { 23 | this.editing = true; 24 | this.title = this.task.title; 25 | } 26 | 27 | saveTitle(): void { 28 | if (this.editing) { 29 | const title = this.title.trim(); 30 | if (title.length && title !== this.task.title) { 31 | this.update.emit({title}); 32 | } 33 | this.stopEditing(); 34 | } 35 | } 36 | 37 | stopEditing(): void { 38 | this.editing = false; 39 | } 40 | 41 | toggleStatus(): void { 42 | this.update.emit({ 43 | completed: !this.task.completed 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/tasks/components/task-list.scss: -------------------------------------------------------------------------------- 1 | @import 2 | "~minx/src/settings", 3 | "~minx/src/functions", 4 | "~minx/src/mixins"; 5 | 6 | //=================================================================== 7 | // TASK-FILTERS 8 | //=================================================================== 9 | 10 | .task-filters { 11 | @include clearfix; 12 | margin-bottom: 45px; 13 | padding-left: 1px; 14 | font-size: rem(16px); 15 | line-height: 24px; 16 | list-style-type: none; 17 | 18 | @include media-query(540) { 19 | margin-bottom: 55px; 20 | } 21 | 22 | li { 23 | float: left; 24 | 25 | &:not(:first-child) { 26 | margin-left: 12px; 27 | } 28 | 29 | &:not(:first-child):before { 30 | padding-right: 12px; 31 | content: '/'; 32 | font-weight: 300; 33 | } 34 | } 35 | 36 | a { 37 | color: #999; 38 | text-decoration: none; 39 | 40 | &.active { 41 | color: #fff; 42 | } 43 | } 44 | } 45 | 46 | 47 | //=================================================================== 48 | // TASK-LIST 49 | //=================================================================== 50 | 51 | .task-list { 52 | border-top: 1px dotted #666; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/tasks/components/task-list.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Task } from '../task'; 4 | 5 | 6 | @Component({ 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | selector: 'task-list', 9 | styles: [ 10 | require('./task-list.scss') 11 | ], 12 | template: ` 13 | 18 | 19 |
20 | 25 |
26 | ` 27 | }) 28 | 29 | export class TaskListComponent { 30 | @Input() filter: string; 31 | @Input() tasks: Observable; 32 | 33 | @Output() remove: EventEmitter = new EventEmitter(false); 34 | @Output() update: EventEmitter = new EventEmitter(false); 35 | } 36 | -------------------------------------------------------------------------------- /src/tasks/components/tasks.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/operator/pluck'; 2 | 3 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { Observable } from 'rxjs/Observable'; 6 | import { TaskService } from '../task-service'; 7 | 8 | 9 | @Component({ 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | selector: 'tasks', 12 | template: ` 13 |
14 |
15 | 16 |
17 | 18 |
19 | 24 |
25 |
26 | ` 27 | }) 28 | 29 | export class TasksComponent { 30 | filter$: Observable; 31 | 32 | constructor(public route: ActivatedRoute, public taskService: TaskService) { 33 | this.filter$ = route.params 34 | .pluck('filter'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/tasks/directives/autofocus-directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, OnInit } from '@angular/core'; 2 | 3 | 4 | @Directive({ 5 | selector: '[autoFocus]' 6 | }) 7 | 8 | export class AutoFocusDirective implements OnInit { 9 | constructor(public element: ElementRef) {} 10 | 11 | ngOnInit(): void { 12 | this.element.nativeElement.focus(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | import { EffectsModule } from '@ngrx/effects'; 5 | 6 | import { SharedModule } from 'src/shared'; 7 | import { TaskFormComponent } from './components/task-form'; 8 | import { TaskItemComponent } from './components/task-item'; 9 | import { TaskListComponent } from './components/task-list'; 10 | import { TasksComponent } from './components/tasks'; 11 | import { AutoFocusDirective } from './directives/autofocus-directive'; 12 | import { TaskListFilterPipe } from './pipes/task-list-filter-pipe'; 13 | import { Task } from './task'; 14 | import { TaskActions } from './task-actions'; 15 | import { TaskEffects } from './task-effects'; 16 | import { taskReducer } from './task-reducer'; 17 | import { TaskService } from './task-service'; 18 | 19 | 20 | const routes: Routes = [ 21 | {path: 'tasks', component: TasksComponent} 22 | ]; 23 | 24 | 25 | @NgModule({ 26 | declarations: [ 27 | AutoFocusDirective, 28 | TaskFormComponent, 29 | TaskItemComponent, 30 | TaskListComponent, 31 | TaskListFilterPipe, 32 | TasksComponent 33 | ], 34 | imports: [ 35 | EffectsModule.runAfterBootstrap(TaskEffects), 36 | FormsModule, 37 | RouterModule.forChild(routes), 38 | SharedModule 39 | ], 40 | providers: [ 41 | TaskActions, 42 | TaskService 43 | ] 44 | }) 45 | 46 | export class TasksModule {} 47 | 48 | export { Task, TaskService, taskReducer }; 49 | -------------------------------------------------------------------------------- /src/tasks/pipes/task-list-filter-pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { TaskListFilterPipe } from './task-list-filter-pipe'; 2 | 3 | 4 | describe('TaskListFilterPipe', () => { 5 | let pipe: TaskListFilterPipe; 6 | let list: any[]; 7 | 8 | beforeEach(() => { 9 | list = [{completed: true}, {completed: false}]; 10 | pipe = new TaskListFilterPipe(); 11 | }); 12 | 13 | it('should return list of active tasks if param `filterType` is `active`', () => { 14 | expect(pipe.transform(list, 'active')).toEqual([{completed: false}]); 15 | }); 16 | 17 | it('should return list of active tasks if param `filterType` is `completed`', () => { 18 | expect(pipe.transform(list, 'completed')).toEqual([{completed: true}]); 19 | }); 20 | 21 | it('should return provided list if param `filterType` is not `active` or `completed`', () => { 22 | expect(pipe.transform(list, '')).toBe(list); 23 | }); 24 | 25 | it('should return provided list if list is undefined and filter is provided', () => { 26 | list = undefined; 27 | expect(pipe.transform(list, 'active')).toBe(list); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/tasks/pipes/task-list-filter-pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Task } from '../task'; 3 | 4 | 5 | @Pipe({ 6 | name: 'filterTasks', 7 | pure: true 8 | }) 9 | 10 | export class TaskListFilterPipe implements PipeTransform { 11 | transform(list: Task[], filterType: string): Task[] { 12 | if (!list || !filterType) { 13 | return list; 14 | } 15 | 16 | switch (filterType) { 17 | case 'active': 18 | return list.filter((task: Task) => { 19 | return !task.completed; 20 | }); 21 | 22 | case 'completed': 23 | return list.filter((task: Task) => { 24 | return task.completed; 25 | }); 26 | 27 | default: 28 | return list; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tasks/task-actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Task } from './task'; 3 | 4 | 5 | export class TaskActions { 6 | static CREATE_TASK = 'CREATE_TASK'; 7 | static CREATE_TASK_FAILED = 'CREATE_TASK_FAILED'; 8 | static CREATE_TASK_FULFILLED = 'CREATE_TASK_FULFILLED'; 9 | 10 | static DELETE_TASK = 'DELETE_TASK'; 11 | static DELETE_TASK_FAILED = 'DELETE_TASK_FAILED'; 12 | static DELETE_TASK_FULFILLED = 'DELETE_TASK_FULFILLED'; 13 | 14 | static FETCH_TASKS = 'FETCH_TASKS'; 15 | static FETCH_TASKS_FAILED = 'FETCH_TASKS_FAILED'; 16 | static FETCH_TASKS_FULFILLED = 'FETCH_TASKS_FULFILLED'; 17 | 18 | static UPDATE_TASK = 'UPDATE_TASK'; 19 | static UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED'; 20 | static UPDATE_TASK_FULFILLED = 'UPDATE_TASK_FULFILLED'; 21 | 22 | 23 | //=================================== 24 | // CREATE 25 | //----------------------------------- 26 | 27 | createTask(task: Task): Action { 28 | return { 29 | type: TaskActions.CREATE_TASK, 30 | payload: { 31 | task 32 | } 33 | }; 34 | } 35 | 36 | createTaskFailed(error: any): Action { 37 | return { 38 | type: TaskActions.CREATE_TASK_FAILED, 39 | payload: error 40 | }; 41 | } 42 | 43 | createTaskFulfilled(task: Task): Action { 44 | return { 45 | type: TaskActions.CREATE_TASK_FULFILLED, 46 | payload: { 47 | task 48 | } 49 | }; 50 | } 51 | 52 | 53 | //=================================== 54 | // DELETE 55 | //----------------------------------- 56 | 57 | deleteTask(taskId: string): Action { 58 | return { 59 | type: TaskActions.DELETE_TASK, 60 | payload: { 61 | taskId 62 | } 63 | }; 64 | } 65 | 66 | deleteTaskFailed(error: any): Action { 67 | return { 68 | type: TaskActions.DELETE_TASK_FAILED, 69 | payload: error 70 | }; 71 | } 72 | 73 | deleteTaskFulfilled(task: Task): Action { 74 | return { 75 | type: TaskActions.DELETE_TASK_FULFILLED, 76 | payload: { 77 | task 78 | } 79 | }; 80 | } 81 | 82 | 83 | //=================================== 84 | // FETCH 85 | //----------------------------------- 86 | 87 | fetchTasks(): Action { 88 | return { 89 | type: TaskActions.FETCH_TASKS 90 | }; 91 | } 92 | 93 | fetchTasksFailed(error: any): Action { 94 | return { 95 | type: TaskActions.FETCH_TASKS_FAILED, 96 | payload: error 97 | }; 98 | } 99 | 100 | fetchTasksFulfilled(tasks: Task[]): Action { 101 | return { 102 | type: TaskActions.FETCH_TASKS_FULFILLED, 103 | payload: { 104 | tasks 105 | } 106 | }; 107 | } 108 | 109 | 110 | //=================================== 111 | // UPDATE 112 | //----------------------------------- 113 | 114 | updateTask(taskId: string, changes: any): Action { 115 | return { 116 | type: TaskActions.UPDATE_TASK, 117 | payload: { 118 | changes, 119 | taskId 120 | } 121 | }; 122 | } 123 | 124 | updateTaskFailed(error: any): Action { 125 | return { 126 | type: TaskActions.UPDATE_TASK_FAILED, 127 | payload: error 128 | }; 129 | } 130 | 131 | updateTaskFulfilled(task: Task): Action { 132 | return { 133 | type: TaskActions.UPDATE_TASK_FULFILLED, 134 | payload: { 135 | task 136 | } 137 | }; 138 | } 139 | } 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/tasks/task-effects.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/observable/of'; 2 | import 'rxjs/add/operator/catch'; 3 | import 'rxjs/add/operator/map'; 4 | import 'rxjs/add/operator/switchMap'; 5 | 6 | import { Injectable } from '@angular/core'; 7 | import { Actions, Effect } from '@ngrx/effects'; 8 | import { Store } from '@ngrx/store'; 9 | import { Observable } from 'rxjs/Observable'; 10 | import { ApiService } from 'src/shared'; 11 | import { TaskActions } from './task-actions'; 12 | 13 | 14 | @Injectable() 15 | export class TaskEffects { 16 | constructor( 17 | private actions$: Actions, 18 | private api: ApiService, 19 | private store$: Store, 20 | private taskActions: TaskActions 21 | ) {} 22 | 23 | 24 | @Effect() 25 | createTask$ = this.actions$ 26 | .ofType(TaskActions.CREATE_TASK) 27 | .switchMap(({payload}) => this.api.createTask(payload.task) 28 | .map(task => this.taskActions.createTaskFulfilled(task)) 29 | .catch(error => Observable.of(this.taskActions.createTaskFailed(error))) 30 | ); 31 | 32 | @Effect() 33 | deleteTask$ = this.actions$ 34 | .ofType(TaskActions.DELETE_TASK) 35 | .switchMap(({payload}) => this.api.deleteTask(payload.taskId) 36 | .map(task => this.taskActions.deleteTaskFulfilled(task)) 37 | .catch(error => Observable.of(this.taskActions.deleteTaskFailed(error))) 38 | ); 39 | 40 | @Effect() 41 | fetchTasks$ = this.actions$ 42 | .ofType(TaskActions.FETCH_TASKS) 43 | .switchMap(() => this.api.fetchTasks() 44 | .map(task => this.taskActions.fetchTasksFulfilled(task)) 45 | .catch(error => Observable.of(this.taskActions.fetchTasksFailed(error))) 46 | ); 47 | 48 | @Effect() 49 | updateTask$ = this.actions$ 50 | .ofType(TaskActions.UPDATE_TASK) 51 | .switchMap(({payload}) => this.api.updateTask(payload.taskId, payload.changes) 52 | .map(task => this.taskActions.updateTaskFulfilled(task)) 53 | .catch(error => Observable.of(this.taskActions.updateTaskFailed(error))) 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/tasks/task-reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionReducer } from '@ngrx/store'; 2 | import { Task } from './task'; 3 | import { TaskActions } from './task-actions'; 4 | 5 | 6 | export const taskReducer: ActionReducer = (state: Task[] = [], {payload, type}: Action) => { 7 | switch (type) { 8 | case TaskActions.CREATE_TASK_FULFILLED: 9 | return [ ...state, payload.task ]; 10 | 11 | case TaskActions.DELETE_TASK_FULFILLED: 12 | return state.filter((task: Task) => { 13 | return task.id !== payload.task.id; 14 | }); 15 | 16 | case TaskActions.FETCH_TASKS_FULFILLED: 17 | return payload.tasks || []; 18 | 19 | case TaskActions.UPDATE_TASK_FULFILLED: 20 | return state.map((task: Task) => { 21 | return task.id === payload.task.id ? payload.task : task; 22 | }); 23 | 24 | default: 25 | return state; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/tasks/task-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Task } from './task'; 5 | import { TaskActions } from './task-actions'; 6 | 7 | 8 | @Injectable() 9 | export class TaskService { 10 | tasks$: Observable; 11 | 12 | constructor(private actions: TaskActions, private store: Store) { 13 | this.tasks$ = store.select('tasks') as Observable; 14 | store.dispatch(this.actions.fetchTasks()); 15 | } 16 | 17 | createTask(title: string): void { 18 | this.store.dispatch( 19 | this.actions.createTask(new Task(title)) 20 | ); 21 | } 22 | 23 | deleteTask(task: Task): void { 24 | this.store.dispatch( 25 | this.actions.deleteTask(task.id) 26 | ); 27 | } 28 | 29 | updateTask(task: Task, changes: any): void { 30 | this.store.dispatch( 31 | this.actions.updateTask(task.id, changes) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/tasks/task.ts: -------------------------------------------------------------------------------- 1 | export class Task { 2 | completed: boolean = false; 3 | id: string; 4 | title: string; 5 | 6 | constructor(title: string) { 7 | this.title = title; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": [ 7 | "dom", 8 | "es6" 9 | ], 10 | "module": "es6", 11 | "moduleResolution": "node", 12 | "noEmitHelpers": true, 13 | "removeComments": true, 14 | "sourceMap": true, 15 | "target": "es5", 16 | "types": [ 17 | "jasmine", 18 | "node" 19 | ] 20 | }, 21 | 22 | "exclude": [ 23 | "node_modules", 24 | "target", 25 | "tmp" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": false, 4 | "ban": [ 5 | true, 6 | ["_", "forEach"], 7 | ["_", "each"], 8 | ["$", "each"], 9 | ["angular", "forEach"] 10 | ], 11 | "class-name": true, 12 | "comment-format": false, 13 | "curly": false, 14 | "eofline": true, 15 | "forin": true, 16 | "indent": [ 17 | true, 18 | "spaces" 19 | ], 20 | "interface-name": [ 21 | true, 22 | "never-prefix" 23 | ], 24 | "jsdoc-format": true, 25 | "label-position": true, 26 | "label-undefined": true, 27 | "max-line-length": false, 28 | "member-access": false, 29 | "member-ordering": false, 30 | "new-parens": true, 31 | "no-angle-bracket-type-assertion": true, 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "debug", 40 | "info", 41 | "time", 42 | "timeEnd", 43 | "trace" 44 | ], 45 | "no-construct": true, 46 | "no-constructor-vars": false, 47 | "no-debugger": true, 48 | "no-default-export": false, 49 | "no-duplicate-key": true, 50 | "no-duplicate-variable": true, 51 | "no-empty": true, 52 | "no-eval": true, 53 | "no-inferrable-types": false, 54 | "no-internal-module": true, 55 | "no-invalid-this": true, 56 | "no-null-keyword": false, 57 | "no-reference": true, 58 | "no-require-imports": false, 59 | "no-shadowed-variable": false, 60 | "no-string-literal": true, 61 | "no-switch-case-fall-through": true, 62 | "no-trailing-whitespace": true, 63 | "no-unreachable": true, 64 | "no-unused-expression": true, 65 | "no-unused-variable": true, 66 | "no-use-before-declare": true, 67 | "no-var-keyword": true, 68 | "no-var-requires": false, 69 | "object-literal-sort-keys": false, 70 | "one-line": [ 71 | true, 72 | "check-open-brace", 73 | "check-whitespace" 74 | ], 75 | "quotemark": [ 76 | true, 77 | "single", 78 | "avoid-escape" 79 | ], 80 | "radix": true, 81 | "semicolon": [ 82 | true, 83 | "always" 84 | ], 85 | "switch-default": false, 86 | "trailing-comma": [ 87 | true, 88 | { 89 | "multiline": "never", 90 | "singleline": "never" 91 | } 92 | ], 93 | "triple-equals": [ 94 | true, 95 | "allow-null-check" 96 | ], 97 | "typedef": [ 98 | true, 99 | "call-signature", 100 | "parameter", 101 | "property-declaration" 102 | ], 103 | "typedef-whitespace": [ 104 | true, 105 | { 106 | "call-signature": "nospace", 107 | "index-signature": "nospace", 108 | "parameter": "nospace", 109 | "property-declaration": "nospace", 110 | "variable-declaration": "nospace" 111 | }, 112 | { 113 | "call-signature": "space", 114 | "index-signature": "space", 115 | "parameter": "space", 116 | "property-declaration": "space", 117 | "variable-declaration": "space" 118 | } 119 | ], 120 | "use-isnan": true, 121 | "use-strict": false, 122 | "variable-name": [ 123 | true, 124 | "allow-leading-underscore", 125 | "ban-keywords" 126 | ], 127 | "whitespace": [ 128 | true, 129 | "check-branch", 130 | "check-decl", 131 | "check-operator", 132 | "check-type" 133 | ] 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const autoprefixer = require('autoprefixer'); 4 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 5 | const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); 6 | const DefinePlugin = require('webpack/lib/DefinePlugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 10 | const ProgressPlugin = require('webpack/lib/ProgressPlugin'); 11 | const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); 12 | const WebpackMd5Hash = require('webpack-md5-hash'); 13 | 14 | 15 | //========================================================= 16 | // VARS 17 | //--------------------------------------------------------- 18 | const NODE_ENV = process.env.NODE_ENV; 19 | 20 | const ENV_DEVELOPMENT = NODE_ENV === 'development'; 21 | const ENV_PRODUCTION = NODE_ENV === 'production'; 22 | const ENV_TEST = NODE_ENV === 'test'; 23 | 24 | const HOST = '0.0.0.0'; 25 | const PORT = 3000; 26 | 27 | 28 | //========================================================= 29 | // LOADERS 30 | //--------------------------------------------------------- 31 | const rules = { 32 | componentStyles: { 33 | test: /\.scss$/, 34 | loader: 'raw!postcss!sass', 35 | exclude: path.resolve('src/shared/styles') 36 | }, 37 | sharedStyles: { 38 | test: /\.scss$/, 39 | loader: 'style!css!postcss!sass', 40 | include: path.resolve('src/shared/styles') 41 | }, 42 | html: { 43 | test: /\.html$/, 44 | loader: 'raw' 45 | }, 46 | typescript: { 47 | test: /\.ts$/, 48 | loader: 'ts', 49 | exclude: /node_modules/ 50 | } 51 | }; 52 | 53 | 54 | //========================================================= 55 | // CONFIG 56 | //--------------------------------------------------------- 57 | const config = module.exports = {}; 58 | 59 | config.resolve = { 60 | extensions: ['.ts', '.js'], 61 | modules: [ 62 | path.resolve('.'), 63 | 'node_modules' 64 | ] 65 | }; 66 | 67 | config.module = { 68 | rules: [ 69 | rules.typescript, 70 | rules.componentStyles, 71 | rules.html 72 | ] 73 | }; 74 | 75 | config.plugins = [ 76 | new DefinePlugin({ 77 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV) 78 | }), 79 | new LoaderOptionsPlugin({ 80 | debug: false, 81 | minimize: true, 82 | options: { 83 | postcss: [ 84 | autoprefixer({browsers: ['last 3 versions']}) 85 | ], 86 | resolve: {}, 87 | sassLoader: { 88 | outputStyle: 'compressed', 89 | precision: 10, 90 | sourceComments: false 91 | } 92 | } 93 | }), 94 | new ContextReplacementPlugin( 95 | /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, 96 | path.resolve('src') 97 | ) 98 | ]; 99 | 100 | 101 | //===================================== 102 | // DEVELOPMENT or PRODUCTION 103 | //------------------------------------- 104 | if (ENV_DEVELOPMENT || ENV_PRODUCTION) { 105 | config.entry = { 106 | main: './src/main.ts', 107 | polyfills: './src/polyfills.ts' 108 | }; 109 | 110 | config.output = { 111 | filename: '[name].js', 112 | path: path.resolve('./target'), 113 | publicPath: '/' 114 | }; 115 | 116 | config.plugins.push( 117 | new CommonsChunkPlugin({ 118 | name: ['polyfills'], 119 | minChunks: Infinity 120 | }), 121 | new HtmlWebpackPlugin({ 122 | filename: 'index.html', 123 | hash: false, 124 | inject: 'body', 125 | template: './src/index.html' 126 | }) 127 | ); 128 | } 129 | 130 | 131 | //===================================== 132 | // DEVELOPMENT 133 | //------------------------------------- 134 | if (ENV_DEVELOPMENT) { 135 | config.devtool = 'cheap-module-source-map'; 136 | 137 | config.module.rules.push(rules.sharedStyles); 138 | 139 | config.plugins.push(new ProgressPlugin()); 140 | 141 | config.devServer = { 142 | contentBase: './src', 143 | historyApiFallback: true, 144 | host: HOST, 145 | port: PORT, 146 | stats: { 147 | cached: true, 148 | cachedAssets: true, 149 | chunks: true, 150 | chunkModules: false, 151 | colors: true, 152 | hash: false, 153 | reasons: true, 154 | timings: true, 155 | version: false 156 | } 157 | }; 158 | } 159 | 160 | 161 | //===================================== 162 | // PRODUCTION 163 | //------------------------------------- 164 | if (ENV_PRODUCTION) { 165 | config.devtool = 'hidden-source-map'; 166 | 167 | config.output.filename = '[name].[chunkhash].js'; 168 | 169 | config.module.rules.push({ 170 | test: /\.scss$/, 171 | loader: ExtractTextPlugin.extract('css?-autoprefixer!postcss!sass'), 172 | include: path.resolve('src/shared/styles') 173 | }); 174 | 175 | config.plugins.push( 176 | new WebpackMd5Hash(), 177 | new ExtractTextPlugin('styles.[contenthash].css'), 178 | new UglifyJsPlugin({ 179 | comments: false, 180 | compress: { 181 | dead_code: true, // eslint-disable-line camelcase 182 | screw_ie8: true, // eslint-disable-line camelcase 183 | unused: true, 184 | warnings: false 185 | }, 186 | mangle: { 187 | screw_ie8: true // eslint-disable-line camelcase 188 | } 189 | }) 190 | ); 191 | } 192 | 193 | 194 | //===================================== 195 | // TEST 196 | //------------------------------------- 197 | if (ENV_TEST) { 198 | config.devtool = 'inline-source-map'; 199 | 200 | config.module.rules.push(rules.sharedStyles); 201 | } 202 | --------------------------------------------------------------------------------