├── .gitignore ├── README.md ├── client ├── app │ ├── app.ts │ ├── boot.ts │ └── items.ts ├── assets │ ├── css │ │ └── app.css │ └── img │ │ └── eggly-logo.png ├── config.js └── index.html ├── package.json ├── server └── api │ └── db.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | client/**/*.js 4 | client/**/*.map 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Reactive RESTful Angular 2 application with ngrx store 2 | 3 | A RESTful master-detail application built using Angular 2 and [ngrx store](https://github.com/ngrx/store). 4 | 5 | ### Getting Started 6 | 7 | There are two main parts to this application. The first is the server which we are using `json-server` to simulate a REST api. The second part is the Angular 2 application which we will use `lite-server` to display. 8 | 9 | To get started run the commands below. 10 | 11 | ``` 12 | $ git clone https://github.com/simpulton/ngrx-rest-app.git 13 | $ cd nxrx-rest-app 14 | $ npm install 15 | $ npm start 16 | ``` 17 | -------------------------------------------------------------------------------- /client/app/app.ts: -------------------------------------------------------------------------------- 1 | //our root app component 2 | import {Component, Input, Output, EventEmitter, ChangeDetectionStrategy} from 'angular2/core' 3 | import {ItemsService, Item, AppStore} from './items' 4 | import {Observable} from 'rxjs/Observable'; 5 | import {Store} from '@ngrx/store' 6 | 7 | //------------------------------------------------------------------- 8 | // ITEMS-LIST 9 | //------------------------------------------------------------------- 10 | @Component({ 11 | selector: 'items-list', 12 | template: ` 13 |
15 |
16 |

{{item.name}}

17 |
18 |
19 | {{item.description}} 20 |
21 |
22 | 26 |
27 |
28 | ` 29 | }) 30 | class ItemList { 31 | @Input() items: Item[]; 32 | @Output() selected = new EventEmitter(); 33 | @Output() deleted = new EventEmitter(); 34 | } 35 | 36 | //------------------------------------------------------------------- 37 | // ITEM DETAIL 38 | //------------------------------------------------------------------- 39 | @Component({ 40 | selector: 'item-detail', 41 | template: ` 42 |
43 |
44 |

Editing {{item.name}}

45 |

Create New Item

46 |
47 |
48 |
49 |
50 | 51 | 54 |
55 | 56 |
57 | 58 | 61 |
62 |
63 |
64 |
65 | 67 | 69 |
70 |
71 | ` 72 | }) 73 | class ItemDetail { 74 | @Input() item: Item[]; 75 | @Output() saved = new EventEmitter(); 76 | @Output() cancelled = new EventEmitter(); 77 | } 78 | 79 | //------------------------------------------------------------------- 80 | // MAIN COMPONENT 81 | //------------------------------------------------------------------- 82 | @Component({ 83 | selector: 'my-app', 84 | providers: [], 85 | template: ` 86 |
87 | 89 | 90 |
91 |
92 | Select an Item 95 |
96 | `, 97 | directives: [ItemList, ItemDetail], 98 | changeDetection: ChangeDetectionStrategy.OnPush 99 | }) 100 | export class App { 101 | items: Observable>; 102 | selectedItem: Observable; 103 | 104 | constructor(private itemsService:ItemsService, private store: Store) { 105 | this.items = itemsService.items; 106 | this.selectedItem = store.select('selectedItem').filter(id => !!id); 107 | 108 | this.selectedItem.subscribe(v => console.log(v)); 109 | this.items.subscribe(v => this.resetItem()); 110 | 111 | itemsService.loadItems(); 112 | } 113 | 114 | resetItem() { 115 | let emptyItem: Item = {id: null, name:'', description:''}; 116 | this.store.dispatch({type: 'SELECT_ITEM', payload: emptyItem}); 117 | } 118 | 119 | selectItem(item: Item) { 120 | this.store.dispatch({type: 'SELECT_ITEM', payload: item}); 121 | } 122 | 123 | saveItem(item: Item) { 124 | this.itemsService.saveItem(item); 125 | } 126 | 127 | deleteItem(item: Item) { 128 | this.itemsService.deleteItem(item); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /client/app/boot.ts: -------------------------------------------------------------------------------- 1 | //main entry point 2 | import {bootstrap} from 'angular2/platform/browser'; 3 | import {App} from './app'; 4 | import {provideStore} from '@ngrx/store' 5 | import {ItemsService, items, selectedItem} from './items' 6 | import {HTTP_PROVIDERS} from 'angular2/http' 7 | 8 | bootstrap(App, [ 9 | ItemsService, 10 | HTTP_PROVIDERS, 11 | provideStore({items, selectedItem}) 12 | ]) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /client/app/items.ts: -------------------------------------------------------------------------------- 1 | import {Http, Headers} from 'angular2/http' 2 | import {Store} from '@ngrx/store' 3 | import {Injectable} from 'angular2/core' 4 | import {Observable} from 'rxjs/Observable'; 5 | 6 | //------------------------------------------------------------------- 7 | // ITEMS STORE 8 | //------------------------------------------------------------------- 9 | export const items = (state: any = [], {type, payload}) => { 10 | let index:number; 11 | switch(type){ 12 | case 'ADD_ITEMS': 13 | return payload; 14 | case 'CREATE_ITEM': 15 | return [...state, payload]; 16 | case 'UPDATE_ITEM': 17 | index = state.findIndex((i: Item) => i.id === payload.id); 18 | return [ 19 | ...state.slice(0, index), 20 | payload, 21 | ...state.slice(index + 1) 22 | ]; 23 | case 'DELETE_ITEM': 24 | index = state.findIndex((i: Item) => i.id === payload.id); 25 | return [ 26 | ...state.slice(0, index), 27 | ...state.slice(index + 1) 28 | ]; 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | //------------------------------------------------------------------- 35 | // SELECTED ITEM STORE 36 | //------------------------------------------------------------------- 37 | export const selectedItem = (state: any = null, {type, payload}) => { 38 | switch(type){ 39 | case 'SELECT_ITEM': 40 | return payload; 41 | default: 42 | return state; 43 | } 44 | } 45 | 46 | //------------------------------------------------------------------- 47 | // ITEMS SERVICE 48 | //------------------------------------------------------------------- 49 | const BASE_URL = 'http://localhost:3000/items/'; 50 | const HEADER = { headers: new Headers({ 'Content-Type': 'application/json'})}; 51 | 52 | export interface Item{ 53 | id: number; 54 | name: string; 55 | description: string; 56 | } 57 | 58 | export interface AppStore { 59 | items: Item[], 60 | selectedItem: Item 61 | } 62 | 63 | @Injectable() 64 | export class ItemsService { 65 | items: Observable>; 66 | 67 | constructor(private http: Http, private store: Store){ 68 | this.items = store.select('items'); 69 | } 70 | 71 | loadItems() { 72 | this.http.get(BASE_URL) 73 | .map(res => res.json()) 74 | .map(payload => ({type: 'ADD_ITEMS', payload})) 75 | .subscribe(action => this.store.dispatch(action)); 76 | } 77 | 78 | saveItem(item: Item) { 79 | (item.id) ? this.updateItem(item) : this.createItem(item); 80 | } 81 | 82 | createItem(item: Item) { 83 | this.http.post(`${BASE_URL}`, JSON.stringify(item), HEADER) 84 | .subscribe(action => this.store.dispatch({type: 'CREATE_ITEM', payload: item})); 85 | } 86 | 87 | updateItem(item: Item) { 88 | this.http.put(`${BASE_URL}${item.id}`, JSON.stringify(item), HEADER) 89 | .subscribe(action => this.store.dispatch({type: 'UPDATE_ITEM', payload: item})); 90 | } 91 | 92 | deleteItem(item: Item) { 93 | this.http.delete(`${BASE_URL}${item.id}`) 94 | .subscribe(action => this.store.dispatch({type: 'DELETE_ITEM', payload: item})); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /client/assets/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: whitesmoke; 3 | } 4 | .content .mdl-layout__header { 5 | background-color: #263238; 6 | } 7 | .content .mdl-spinner { 8 | margin: 0 auto; 9 | } 10 | .mdl-navigation__link--icon>.material-icons { 11 | margin-right: 8px; 12 | margin-top: 8px; 13 | } 14 | .page-content { 15 | max-width: 1200px; 16 | width: 100%; 17 | margin: 0 auto; 18 | padding: 0; 19 | } 20 | .item-card.mdl-card { 21 | width: 100%; 22 | min-height: inherit; 23 | cursor: pointer; 24 | } 25 | .item-card .mdl-textfield { 26 | width: 100%; 27 | } 28 | .item-card .mdl-textfield__input { 29 | border: none; 30 | border-bottom: 1px solid rgba(0, 0, 0, .12); 31 | display: block; 32 | font-size: 16px; 33 | margin: 0; 34 | padding: 4px 0; 35 | width: 100%; 36 | background: 0 0; 37 | text-align: left; 38 | color: inherit; 39 | } 40 | .item-card label { 41 | font-size: 12px; 42 | font-weight: bold; 43 | } 44 | *:focus { 45 | outline: none; 46 | } 47 | -------------------------------------------------------------------------------- /client/assets/img/eggly-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robwormald/ngrx-rest-app/1a883b53948d2412f504a4120af9bba3ca7011fb/client/assets/img/eggly-logo.png -------------------------------------------------------------------------------- /client/config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | //use typescript for compilation 3 | //transpiler: 'typescript', 4 | //typescript compiler options 5 | typescriptOptions: { 6 | emitDecoratorMetadata: true 7 | }, 8 | //map tells the System loader where to look for things 9 | map: { 10 | app: "./app", 11 | '@ngrx': 'https://npmcdn.com/@ngrx' 12 | }, 13 | //packages defines our app package 14 | packages: { 15 | app: { 16 | main: './main.js', 17 | defaultExtension: 'js' 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 2 REST Website 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | ANGULAR 2 with NGRX 33 | 34 |
35 | 36 | 41 |
42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-rest-website", 3 | "version": "0.0.1", 4 | "description": "Simple REST website in Angular 2 and NGRX", 5 | "repository": "https://github.com/simpulton/ngrx-rest-app", 6 | "scripts": { 7 | "server": "json-server --watch server/api/db.json", 8 | "tsc": "tsc", 9 | "tsc:w": "tsc -w", 10 | "client": "lite-server --baseDir './client' --baseDir './'", 11 | "start": "concurrent \"npm run tsc:w\" \"npm run server\" \"npm run client\"" 12 | }, 13 | "dependencies": { 14 | "angular2": "^2.0.0-beta.0", 15 | "systemjs": "^0.19.9", 16 | "es6-promise": "^3.0.2", 17 | "es6-shim": "^0.33.13", 18 | "reflect-metadata": "0.1.2", 19 | "rxjs": "^5.0.0-beta.1", 20 | "zone.js": "^0.5.10", 21 | "@ngrx/store": "^1.2.1" 22 | }, 23 | "devDependencies": { 24 | "concurrently": "^1.0.0", 25 | "lite-server": "^1.3.2", 26 | "typescript": "^1.7.5" 27 | }, 28 | "author": "Lukas Ruebbelke", 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /server/api/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "id": 1, 5 | "name": "Item 1", 6 | "description": "This is a description" 7 | }, 8 | { 9 | "id": 2, 10 | "name": "Item 2", 11 | "description": "This is a description" 12 | }, 13 | { 14 | "id": 3, 15 | "name": "Item 3", 16 | "description": "This is a description" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "system", 5 | "sourceMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "moduleResolution": "node", 9 | "removeComments": false, 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ], 16 | "filesGlob": [ 17 | "client/app/**/*.ts" 18 | ], 19 | "compileOnSave": false, 20 | "atom": { 21 | "rewriteTsconfig": true 22 | }, 23 | "files": [ 24 | "client/app/app.ts", 25 | "client/app/boot.ts", 26 | "client/app/items.ts" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------