diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0284f82 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# @AngularClass +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false + +[*.json] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index a486940..1244082 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ -.DS_Store -node_modules client/**/*.js client/**/*.map + +typings +node_modules +jspm_packages +bower_components + +.idea/ +.DS_Store diff --git a/README.md b/README.md index 579d0fe..4abba23 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ -## Reactive RESTful Angular 2 application with ngrx store +# Reactive RESTful Angular 2 application with ngrx store A RESTful master-detail application built using Angular 2 and [ngrx store](https://github.com/ngrx/store). -### Getting Started +## Dependencies +- You must have `node v >= 4.0` and `npm` installed (via `brew install node` or [NodeJS.org](https://nodejs.org/en/)); +- `npm i -g typings webpack-dev-server webpack rimraf json-server` +- If you have already installed `typings`, make sure to update it to `1.x` -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. +## Getting Started -To get started run the commands below. +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 `webpack-dev-server` to display. + +To get started, run the commands below. ``` -$ git clone https://github.com/simpulton/ngrx-rest-app.git -$ cd nxrx-rest-app +$ git clone https://github.com/onehungrymind/fem-ng2-ngrx-app.git +$ cd fem-ng2-ngrx-app $ npm install +$ typings install $ npm start ``` + +Then navigate to [http://localhost:3001](http://localhost:3001) in your browser. diff --git a/client/app/app.ts b/client/app/app.ts deleted file mode 100644 index cfcd05d..0000000 --- a/client/app/app.ts +++ /dev/null @@ -1,130 +0,0 @@ -//our root app component -import {Component, Input, Output, EventEmitter, ChangeDetectionStrategy} from 'angular2/core' -import {ItemsService, Item, AppStore} from './items' -import {Observable} from 'rxjs/Observable'; -import {Store} from '@ngrx/store' - -//------------------------------------------------------------------- -// ITEMS-LIST -//------------------------------------------------------------------- -@Component({ - selector: 'items-list', - template: ` -
-
-

{{item.name}}

-
-
- {{item.description}} -
-
- -
-
- ` -}) -class ItemList { - @Input() items: Item[]; - @Output() selected = new EventEmitter(); - @Output() deleted = new EventEmitter(); -} - -//------------------------------------------------------------------- -// ITEM DETAIL -//------------------------------------------------------------------- -@Component({ - selector: 'item-detail', - template: ` -
-
-

Editing {{item.name}}

-

Create New Item

-
-
-
-
- - -
- -
- - -
-
-
-
- - -
-
- ` -}) -class ItemDetail { - @Input() item: Item[]; - @Output() saved = new EventEmitter(); - @Output() cancelled = new EventEmitter(); -} - -//------------------------------------------------------------------- -// MAIN COMPONENT -//------------------------------------------------------------------- -@Component({ - selector: 'my-app', - providers: [], - template: ` -
- - -
-
- Select an Item -
- `, - directives: [ItemList, ItemDetail], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class App { - items: Observable>; - selectedItem: Observable; - - constructor(private itemsService:ItemsService, private store: Store) { - this.items = itemsService.items; - this.selectedItem = store.select('selectedItem').filter(id => !!id); - - this.selectedItem.subscribe(v => console.log(v)); - this.items.subscribe(v => this.resetItem()); - - itemsService.loadItems(); - } - - resetItem() { - let emptyItem: Item = {id: null, name:'', description:''}; - this.store.dispatch({type: 'SELECT_ITEM', payload: emptyItem}); - } - - selectItem(item: Item) { - this.store.dispatch({type: 'SELECT_ITEM', payload: item}); - } - - saveItem(item: Item) { - this.itemsService.saveItem(item); - } - - deleteItem(item: Item) { - this.itemsService.deleteItem(item); - } -} diff --git a/client/app/boot.ts b/client/app/boot.ts deleted file mode 100644 index 8a691f4..0000000 --- a/client/app/boot.ts +++ /dev/null @@ -1,13 +0,0 @@ -//main entry point -import {bootstrap} from 'angular2/platform/browser'; -import {App} from './app'; -import {provideStore} from '@ngrx/store' -import {ItemsService, items, selectedItem} from './items' -import {HTTP_PROVIDERS} from 'angular2/http' - -bootstrap(App, [ - ItemsService, - HTTP_PROVIDERS, - provideStore({items, selectedItem}) -]) -.catch(err => console.error(err)); diff --git a/client/app/items.ts b/client/app/items.ts deleted file mode 100644 index c09317b..0000000 --- a/client/app/items.ts +++ /dev/null @@ -1,96 +0,0 @@ -import {Http, Headers} from 'angular2/http' -import {Store} from '@ngrx/store' -import {Injectable} from 'angular2/core' -import {Observable} from 'rxjs/Observable'; - -//------------------------------------------------------------------- -// ITEMS STORE -//------------------------------------------------------------------- -export const items = (state: any = [], {type, payload}) => { - let index:number; - switch(type){ - case 'ADD_ITEMS': - return payload; - case 'CREATE_ITEM': - return [...state, payload]; - case 'UPDATE_ITEM': - index = state.findIndex((i: Item) => i.id === payload.id); - return [ - ...state.slice(0, index), - payload, - ...state.slice(index + 1) - ]; - case 'DELETE_ITEM': - index = state.findIndex((i: Item) => i.id === payload.id); - return [ - ...state.slice(0, index), - ...state.slice(index + 1) - ]; - default: - return state; - } -} - -//------------------------------------------------------------------- -// SELECTED ITEM STORE -//------------------------------------------------------------------- -export const selectedItem = (state: any = null, {type, payload}) => { - switch(type){ - case 'SELECT_ITEM': - return payload; - default: - return state; - } -} - -//------------------------------------------------------------------- -// ITEMS SERVICE -//------------------------------------------------------------------- -const BASE_URL = 'http://localhost:3000/items/'; -const HEADER = { headers: new Headers({ 'Content-Type': 'application/json'})}; - -export interface Item{ - id: number; - name: string; - description: string; -} - -export interface AppStore { - items: Item[], - selectedItem: Item -} - -@Injectable() -export class ItemsService { - items: Observable>; - - constructor(private http: Http, private store: Store){ - this.items = store.select('items'); - } - - loadItems() { - this.http.get(BASE_URL) - .map(res => res.json()) - .map(payload => ({type: 'ADD_ITEMS', payload})) - .subscribe(action => this.store.dispatch(action)); - } - - saveItem(item: Item) { - (item.id) ? this.updateItem(item) : this.createItem(item); - } - - createItem(item: Item) { - this.http.post(`${BASE_URL}`, JSON.stringify(item), HEADER) - .subscribe(action => this.store.dispatch({type: 'CREATE_ITEM', payload: item})); - } - - updateItem(item: Item) { - this.http.put(`${BASE_URL}${item.id}`, JSON.stringify(item), HEADER) - .subscribe(action => this.store.dispatch({type: 'UPDATE_ITEM', payload: item})); - } - - deleteItem(item: Item) { - this.http.delete(`${BASE_URL}${item.id}`) - .subscribe(action => this.store.dispatch({type: 'DELETE_ITEM', payload: item})); - } -} diff --git a/client/boot.ts b/client/boot.ts new file mode 100644 index 0000000..d82f158 --- /dev/null +++ b/client/boot.ts @@ -0,0 +1,44 @@ +import 'core-js'; +import 'zone.js/dist/zone'; +import {StoreModule} from '@ngrx/store'; +import {StoreDevtoolsModule} from '@ngrx/store-devtools'; +import {StoreLogMonitorModule, useLogMonitor} from '@ngrx/store-log-monitor'; +import {items} from './src/common/stores/items.store'; +import {selectedItem} from './src/common/stores/selectedItem.store'; +import {selectedWidget} from './src/common/stores/selectedWidget.store'; + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {RouterModule} from '@angular/router'; +import {ReactiveFormsModule, FormsModule} from '@angular/forms'; +import {HttpModule} from '@angular/http'; +import {App} from './src/app'; +import {Items} from './src/items/items.component'; +import {Widgets} from './src/widgets/widgets.component'; +import {GadgetService} from "./src/common/services/gadget.service.ts"; +import {routes} from './routes'; + +@NgModule({ + imports: [ + BrowserModule, + HttpModule, + ReactiveFormsModule, + FormsModule, + RouterModule.forRoot(routes), + StoreModule.provideStore({items, selectedItem, selectedWidget}), + StoreDevtoolsModule.instrumentStore({ + monitor: useLogMonitor({ + visible: false, + position: 'right' + }) + }), + StoreLogMonitorModule + ], + declarations: [App, Items, Widgets], + providers: [GadgetService], + bootstrap: [App] +}) +export class AppModule {} + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/client/config.js b/client/config.js deleted file mode 100644 index 7e66f03..0000000 --- a/client/config.js +++ /dev/null @@ -1,20 +0,0 @@ -System.config({ - //use typescript for compilation - transpiler: 'typescript', - //typescript compiler options - typescriptOptions: { - emitDecoratorMetadata: true - }, - //map tells the System loader where to look for things - map: { - app: "./app", - '@ngrx': 'https://npmcdn.com/@ngrx' - }, - //packages defines our app package - packages: { - app: { - main: './main.ts', - defaultExtension: 'ts' - } - } -}); diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 4fa3db6..0000000 --- a/client/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - Angular 2 REST Website - - - - - - - - - - - - - - - - - - -
-
-
- ANGULAR 2 with NGRX - -
- - -
-
-
- -
-
-
-
- - - diff --git a/client/assets/css/app.css b/client/public/assets/css/app.css similarity index 86% rename from client/assets/css/app.css rename to client/public/assets/css/app.css index bddcdb3..c778507 100644 --- a/client/assets/css/app.css +++ b/client/public/assets/css/app.css @@ -17,15 +17,15 @@ body { margin: 0 auto; padding: 0; } -.item-card.mdl-card { +.fem-card.mdl-card { width: 100%; min-height: inherit; cursor: pointer; } -.item-card .mdl-textfield { +.fem-card .mdl-textfield { width: 100%; } -.item-card .mdl-textfield__input { +.fem-card .mdl-textfield__input { border: none; border-bottom: 1px solid rgba(0, 0, 0, .12); display: block; @@ -37,7 +37,7 @@ body { text-align: left; color: inherit; } -.item-card label { +.fem-card label { font-size: 12px; font-weight: bold; } diff --git a/client/assets/img/eggly-logo.png b/client/public/assets/img/eggly-logo.png similarity index 100% rename from client/assets/img/eggly-logo.png rename to client/public/assets/img/eggly-logo.png diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..b525127 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,25 @@ + + + + + Angular 2 REST Website + + + + + + + + + + + + + + + +
+
+ + + diff --git a/client/routes.ts b/client/routes.ts new file mode 100644 index 0000000..7181075 --- /dev/null +++ b/client/routes.ts @@ -0,0 +1,10 @@ +import { Items } from './src/items/items.component'; +import { Widgets } from './src/widgets/widgets.component'; +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + {path: '', component: Items }, + {path: 'items', component: Items}, + {path: 'widgets', component: Widgets}, + {path: '*', component: Items } +]; diff --git a/client/src/app.html b/client/src/app.html new file mode 100644 index 0000000..06fd03f --- /dev/null +++ b/client/src/app.html @@ -0,0 +1,19 @@ +
+
+
+ Angular 2 REST Website +
+ +
+
+
+ +
+ +
diff --git a/client/src/app.ts b/client/src/app.ts new file mode 100644 index 0000000..547eb15 --- /dev/null +++ b/client/src/app.ts @@ -0,0 +1,12 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'my-app', + template: require('./app.html') +}) +export class App { + links = { + items: ['/items'], + widgets: ['/widgets'] + } +} diff --git a/client/src/common/models/appstore.model.ts b/client/src/common/models/appstore.model.ts new file mode 100644 index 0000000..dc0c36b --- /dev/null +++ b/client/src/common/models/appstore.model.ts @@ -0,0 +1,9 @@ +import {Item} from './item.model'; +import {Widget} from "./widget.model"; + +export interface AppStore { + items: Item[]; + selectedItem: Item; + widgets: Widget[]; + selectedWidget: Widget; +}; diff --git a/client/src/common/models/gadget.model.ts b/client/src/common/models/gadget.model.ts new file mode 100644 index 0000000..e5531d4 --- /dev/null +++ b/client/src/common/models/gadget.model.ts @@ -0,0 +1,7 @@ +import {Item} from './item.model'; +import {Widget} from './widget.model'; + +export interface Gadget { + items: Item[]; + widgets: Widget[]; +}; diff --git a/client/src/common/models/item.model.ts b/client/src/common/models/item.model.ts new file mode 100644 index 0000000..778c7a8 --- /dev/null +++ b/client/src/common/models/item.model.ts @@ -0,0 +1,5 @@ +export interface Item { + id: number; + name: string; + description: string; +}; \ No newline at end of file diff --git a/client/src/common/models/widget.model.ts b/client/src/common/models/widget.model.ts new file mode 100644 index 0000000..5e3a79c --- /dev/null +++ b/client/src/common/models/widget.model.ts @@ -0,0 +1,5 @@ +export interface Widget { + id: number, + name: string, + price: number +} diff --git a/client/src/common/services/gadget.service.ts b/client/src/common/services/gadget.service.ts new file mode 100644 index 0000000..daff535 --- /dev/null +++ b/client/src/common/services/gadget.service.ts @@ -0,0 +1,32 @@ +import {Injectable} from '@angular/core'; +import {Store} from '@ngrx/store'; +import {Observable} from 'rxjs/Observable'; + +import {AppStore} from '../models/appstore.model'; +import {Gadget} from "../models/gadget.model.ts"; +import {Item} from "../models/item.model"; +import {Widget} from "../models/widget.model"; + +import * as Rx from 'rxjs/Rx'; // + +@Injectable() +export class GadgetService { + gadget: Observable; + items: Observable>; + widgets: Observable>; + + constructor(private store: Store) { + this.gadget = Rx.Observable.combineLatest( + store.select('items'), + store.select('widgets'), + (items: Item[] = [], widgets: Widget[] = []) => { + return { + items: [...items], + widgets: [...widgets] + } + }); + + this.gadget + .subscribe(c => console.log('GadgetService.gadget', c)); + } +}; diff --git a/client/src/common/services/items.service.ts b/client/src/common/services/items.service.ts new file mode 100644 index 0000000..7ce5b43 --- /dev/null +++ b/client/src/common/services/items.service.ts @@ -0,0 +1,48 @@ +import {Http, Headers} from '@angular/http'; +import {Injectable} from '@angular/core'; +import {Store} from '@ngrx/store'; +import {Observable} from "rxjs/Observable"; +import 'rxjs/add/operator/map'; + +import {AppStore} from '../models/appstore.model'; +import {Item} from '../models/item.model'; + +const BASE_URL = 'http://localhost:3000/items/'; +const HEADER = { headers: new Headers({ 'Content-Type': 'application/json' }) }; + +@Injectable() +export class ItemsService { + items: Observable>; + + constructor(private http: Http, private store: Store) { + this.items = store.select(state => state.items); + } + + loadItems() { + this.http.get(BASE_URL) + .map(res => res.json()) + .map(payload => ({ type: 'ADD_ITEMS', payload })) + .subscribe(action => this.store.dispatch(action)); + } + + saveItem(item: Item) { + (item.id) ? this.updateItem(item) : this.createItem(item); + } + + createItem(item: Item) { + this.http.post(`${BASE_URL}`, JSON.stringify(item), HEADER) + .map(res => res.json()) + .map(payload => ({ type: 'CREATE_ITEM', payload })) + .subscribe(action => this.store.dispatch(action)); + } + + updateItem(item: Item) { + this.http.put(`${BASE_URL}${item.id}`, JSON.stringify(item), HEADER) + .subscribe(action => this.store.dispatch({ type: 'UPDATE_ITEM', payload: item })); + } + + deleteItem(item: Item) { + this.http.delete(`${BASE_URL}${item.id}`) + .subscribe(action => this.store.dispatch({ type: 'DELETE_ITEM', payload: item })); + } +} diff --git a/client/src/common/services/widgets.service.ts b/client/src/common/services/widgets.service.ts new file mode 100644 index 0000000..d003f78 --- /dev/null +++ b/client/src/common/services/widgets.service.ts @@ -0,0 +1,53 @@ +import {Http, Headers} from '@angular/http'; +import {Injectable} from '@angular/core'; +import {Widget} from "../models/widget.model"; + +const BASE_URL = 'http://localhost:3000/widgets/'; +const HEADER = { headers: new Headers({ 'Content-Type': 'application/json' }) }; + +@Injectable() +export class WidgetsService { + widgets: Widget[] = []; + + constructor(private http: Http) {} + + add(widget: Widget){ + // this.widgets = [...this.widgets, widget]; + return this.http.post(BASE_URL, JSON.stringify(widget), HEADER) + .map(res => res.json()) + .do(data => { + this.widgets = [...this.widgets, data]; + return data; + }); + } + + remove(widget: Widget){ + return this.http.delete(`${BASE_URL}?id=${widget.id}`) + .map(res => res.json()) + .do(removed => { + this.widgets = this.widgets.filter( + (currentWidget) => currentWidget.id !== removed.id + ); + }) + } + + update(widget: Widget, update){ + + return this.http.put(`${BASE_URL}?id=${widget.id}`, JSON.stringify(update), HEADER) + .map(res => res.json()) + .do(updated => { + const index = this.widgets.indexOf(updated); + this.widgets = [ + ...this.widgets.slice(0, index), + updated, + ...this.widgets.slice(index + 1) + ] + }) + } + + loadWidgets() { + return this.http.get(BASE_URL) + .map(res => res.json()) + .do(json => this.widgets = [...this.widgets, ...json]) + } +} diff --git a/client/src/common/stores/items.store.spec.ts b/client/src/common/stores/items.store.spec.ts new file mode 100644 index 0000000..0b83be1 --- /dev/null +++ b/client/src/common/stores/items.store.spec.ts @@ -0,0 +1,45 @@ +import {items} from './items.store'; + +describe('`items` store', () => { + let initialState = [ + { id: 0, name: 'First Item' }, + { id: 1, name: 'Second Item' } + ]; + + it('returns an empty array by default', () => { + let defaultState = items(undefined, {type: 'random', payload: {}}); + + expect(defaultState).toEqual([]); + }); + + it('`ADD_ITEMS`', () => { + let payload = initialState, + stateItems = items([], {type: 'ADD_ITEMS', payload: payload}); + + expect(stateItems).toEqual(payload); + }); + + it('`CREATE_ITEM`', () => { + let payload = {id: 2, name: 'added item'}, + result = [...initialState, payload], + stateItems = items(initialState, {type: 'CREATE_ITEM', payload: payload}); + + expect(stateItems).toEqual(result); + }); + + it('`UPDATE_ITEM`', () => { + let payload = { id: 1, name: 'Updated Item' }, + result = [ initialState[0], { id: 1, name: 'Updated Item' } ], + stateItems = items(initialState, {type: 'UPDATE_ITEM', payload: payload}); + + expect(stateItems).toEqual(result); + }); + + it('`DELETE_ITEM`', () => { + let payload = { id: 0 }, + result = [ initialState[1] ], + stateItems = items(initialState, {type: 'DELETE_ITEM', payload: payload}); + + expect(stateItems).toEqual(result); + }); +}); diff --git a/client/src/common/stores/items.store.ts b/client/src/common/stores/items.store.ts new file mode 100644 index 0000000..e9bc247 --- /dev/null +++ b/client/src/common/stores/items.store.ts @@ -0,0 +1,18 @@ +export const items = (state: any = [], {type, payload}) => { + switch (type) { + case 'ADD_ITEMS': + return payload; + case 'CREATE_ITEM': + return [...state, payload]; + case 'UPDATE_ITEM': + return state.map(item => { + return item.id === payload.id ? Object.assign({}, item, payload) : item; + }); + case 'DELETE_ITEM': + return state.filter(item => { + return item.id !== payload.id; + }); + default: + return state; + } +}; diff --git a/client/src/common/stores/selectedItem.store.spec.ts b/client/src/common/stores/selectedItem.store.spec.ts new file mode 100644 index 0000000..560382b --- /dev/null +++ b/client/src/common/stores/selectedItem.store.spec.ts @@ -0,0 +1,15 @@ +import {selectedItem} from './selectedItem.store'; + +describe('`selectedItem` store', () => { + it('returns null by default', () => { + let defaultState = selectedItem(undefined, {type: 'random', payload: {}}); + + expect(defaultState).toBeNull(); + }); + + it('`SELECT_ITEM` returns the provided payload', () => { + let selectItem = selectedItem(undefined, {type: 'SELECT_ITEM', payload: 'payload'}); + + expect(selectItem).toBe('payload'); + }); +}); diff --git a/client/src/common/stores/selectedItem.store.ts b/client/src/common/stores/selectedItem.store.ts new file mode 100644 index 0000000..14692cf --- /dev/null +++ b/client/src/common/stores/selectedItem.store.ts @@ -0,0 +1,8 @@ +export const selectedItem = (state: any = null, {type, payload}) => { + switch (type) { + case 'SELECT_ITEM': + return payload; + default: + return state; + } +}; diff --git a/client/src/common/stores/selectedWidget.store.ts b/client/src/common/stores/selectedWidget.store.ts new file mode 100644 index 0000000..0bce8c5 --- /dev/null +++ b/client/src/common/stores/selectedWidget.store.ts @@ -0,0 +1,8 @@ +export const selectedWidget = (state: any = null, {type, payload}) => { + switch (type) { + case 'SELECT_WIDGET': + return payload; + default: + return state; + } +}; diff --git a/client/src/items/item-detail.component.ts b/client/src/items/item-detail.component.ts new file mode 100644 index 0000000..478df37 --- /dev/null +++ b/client/src/items/item-detail.component.ts @@ -0,0 +1,50 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {Item} from '../common/models/item.model'; + +@Component({ + selector: 'item-detail', + template: ` +
+
+

Editing {{originalName}}

+

Create New Item

+
+
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+
+ ` +}) +export class ItemDetail { + originalName: string; + selectedItem: Item; + @Output() saved = new EventEmitter(); + @Output() cancelled = new EventEmitter(); + + @Input() set item(value: Item){ + if (value) this.originalName = value.name; + this.selectedItem = Object.assign({}, value); + } +} diff --git a/client/src/items/items-list.component.ts b/client/src/items/items-list.component.ts new file mode 100644 index 0000000..2b254cd --- /dev/null +++ b/client/src/items/items-list.component.ts @@ -0,0 +1,28 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {Item} from '../common/models/item.model'; + +@Component({ + selector: 'items-list', + template: ` +
+
+

{{item.name}}

+
+
+ {{item.description}} +
+
+ +
+
+ ` +}) +export class ItemsList { + @Input() items: Item[]; + @Output() selected = new EventEmitter(); + @Output() deleted = new EventEmitter(); +} diff --git a/client/src/items/items.component.ts b/client/src/items/items.component.ts new file mode 100644 index 0000000..6e731bb --- /dev/null +++ b/client/src/items/items.component.ts @@ -0,0 +1,78 @@ +import {Component} from '@angular/core'; +import {Observable} from "rxjs/Observable"; +import {Store} from '@ngrx/store'; +import {ItemsService} from '../common/services/items.service.ts'; +import {AppStore} from '../common/models/appstore.model'; +import {Item} from '../common/models/item.model'; +import {ItemsList} from './items-list.component'; +import {ItemDetail} from './item-detail.component'; + +import {Gadget} from '../common/models/gadget.model'; +import {GadgetService} from '../common/services/gadget.service.ts' + +@Component({ + selector: 'items', + template: ` +
+
+ + +
+
+ Select an Item +
+
+ `, + styles: [` + .items { + padding: 20px; + } + `], + providers: [ItemsService], + directives: [ItemsList, ItemDetail] +}) +export class Items { + items: Observable>; + selectedItem: Observable; + gadget: Observable; + + constructor(private itemsService: ItemsService, + private gadgetService: GadgetService, + private store: Store) { + this.items = itemsService.items; + this.selectedItem = store.select(state => state.selectedItem); + this.selectedItem.subscribe(v => console.log(v)); + + this.gadget = gadgetService.gadget; + + itemsService.loadItems(); + } + + resetItem() { + let emptyItem: Item = {id: null, name: '', description: ''}; + this.store.dispatch({type: 'SELECT_ITEM', payload: emptyItem}); + } + + selectItem(item: Item) { + this.store.dispatch({type: 'SELECT_ITEM', payload: item}); + } + + saveItem(item: Item) { + this.itemsService.saveItem(item); + + // Generally, we would want to wait for the result of `itemsService.saveItem` + // before resetting the current item. + this.resetItem(); + } + + deleteItem(item: Item) { + this.itemsService.deleteItem(item); + + // Generally, we would want to wait for the result of `itemsService.deleteItem` + // before resetting the current item. + this.resetItem(); + } +} diff --git a/client/src/widgets/widget-details.component.ts b/client/src/widgets/widget-details.component.ts new file mode 100644 index 0000000..7634bc4 --- /dev/null +++ b/client/src/widgets/widget-details.component.ts @@ -0,0 +1,62 @@ +import {Component, Input, Output, EventEmitter, OnInit} from '@angular/core'; +import {FormGroup, Validators, FormBuilder} from '@angular/forms'; +import {Widget} from "./../common/models/widget.model.ts"; + +@Component({ + selector: 'widget-details', + template: ` +
+
+

Editing {{originalName}}

+

Create New Widget

+
+
+
+
+ + +
+ +
+ + +
+ +
+
+
+ `, + styles: [` + .error { color: red; } + `] +}) +export class WidgetDetails implements OnInit { + originalName: string; + selectedWidget: Widget; + @Output() saved = new EventEmitter(); + @Output() cancelled = new EventEmitter(); + widgetForm: FormGroup; + + @Input() set widget(value: Widget){ + if (value) this.originalName = value.name; + this.selectedWidget = Object.assign({}, value); + } + + constructor(private fb: FormBuilder) { } + + ngOnInit() { + this.widgetForm = this.fb.group({ + widgetName: [this.selectedWidget.name, Validators.required], + widgetPrice: [this.selectedWidget.price, Validators.required] + }); + } +} diff --git a/client/src/widgets/widgets-list.component.ts b/client/src/widgets/widgets-list.component.ts new file mode 100644 index 0000000..c3f7d74 --- /dev/null +++ b/client/src/widgets/widgets-list.component.ts @@ -0,0 +1,22 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {Widget} from './../common/models/widget.model.ts'; + +@Component({ + selector: 'widgets-list', + template: ` +
+
+

{{widget.name}}

+
+
+ {{widget.price}} +
+
+ ` +}) +export class WidgetsList { + @Input()widgets: Widget[]; + @Output()selected = new EventEmitter(); +} diff --git a/client/src/widgets/widgets.component.ts b/client/src/widgets/widgets.component.ts new file mode 100644 index 0000000..39bb139 --- /dev/null +++ b/client/src/widgets/widgets.component.ts @@ -0,0 +1,55 @@ +import {Observable} from "rxjs/Observable"; +import {Store} from '@ngrx/store'; +import {Component} from '@angular/core' +import {WidgetsService} from './../common/services/widgets.service.ts'; +import {WidgetsList} from './widgets-list.component'; +import {WidgetDetails} from './widget-details.component'; +import {AppStore} from "../common/models/appstore.model"; +import {Widget} from "../common/models/widget.model"; + +@Component({ + selector: 'widgets', + template: ` +

Fix my inputs and outputs!

+
+
+ +
+
+ +
+
+ `, + styles: [` + .widgets { + padding: 20px; + } + `], + directives: [WidgetsList, WidgetDetails], + providers: [WidgetsService] +}) +export class Widgets { + widgets = []; + selectedWidget: Observable; + + constructor(private _widgetsService: WidgetsService, + private _store: Store) { + this.selectedWidget = _store.select(state => state.selectedWidget); + + _widgetsService.loadWidgets() + .subscribe( + widgets => this.widgets = widgets, + error => console.error(error.json()) + ); + } + + selectWidget(widget) { + this._store.dispatch({type: 'SELECT_WIDGET', payload: widget}); + } + + saveWidget(widget) { + console.log('widget', widget); + } +} diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..421f79d --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,19 @@ +module.exports = function(config) { + var webpackConfig = require('./webpack.test.config.js'); + + config.set({ + basePath: '', + frameworks: [ 'jasmine' ], + files: [ { pattern: 'spec-bundle.js', watched: false } ], + preprocessors: { 'spec-bundle.js': ['webpack', 'sourcemap'] }, + webpack: webpackConfig, + webpackServer: { noInfo: true }, + reporters: [ 'spec' ], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: false, + browsers: [ 'PhantomJS' ], + singleRun: true + }); +}; diff --git a/package.json b/package.json index f480f33..eb4f521 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,55 @@ { - "name": "ng2-rest-website", + "name": "fem-ng2-ngrx-app", "version": "0.0.1", "description": "Simple REST website in Angular 2 and NGRX", - "repository": "https://github.com/simpulton/ngrx-rest-app", + "repository": "https://github.com/onehungrymind/fem-ng2-ngrx-app", "scripts": { + "client": "npm run client:dev:hmr", + "client:dev": "webpack-dev-server --inline --colors --watch --display-error-details --display-cached --port 3001 --content-base client/public", + "client:dev:hmr": "npm run client:dev -- --hot", "server": "json-server --watch server/api/db.json", - "tsc": "tsc", - "tsc:w": "tsc -w", - "client": "lite-server --baseDir './client' --baseDir './'", - "start": "concurrent \"npm run tsc:w\" \"npm run server\" \"npm run client\"" + "test": "karma start", + "start": "concurrently \"npm run server\" \"npm run client\"" }, "dependencies": { - "angular2": "^2.0.0-beta.0", - "systemjs": "^0.19.9", - "es6-promise": "^3.0.2", - "es6-shim": "^0.33.13", - "reflect-metadata": "0.1.2", - "rxjs": "^5.0.0-beta.1", - "zone.js": "^0.5.10", - "@ngrx/store": "^1.2.1" + "@angular/common": "2.0.0-rc.5", + "@angular/compiler": "2.0.0-rc.5", + "@angular/core": "2.0.0-rc.5", + "@angular/forms": "0.3.0", + "@angular/http": "2.0.0-rc.5", + "@angular/platform-browser": "2.0.0-rc.5", + "@angular/platform-browser-dynamic": "2.0.0-rc.5", + "@angular/router": "3.0.0-rc.1", + "@ngrx/core": "1.0.0", + "@ngrx/store": "2.1.2", + "@ngrx/store-devtools": "3.0.0", + "@ngrx/store-log-monitor": "3.0.0", + "es6-promise": "3.1.2", + "es6-shim": "0.35.0", + "lodash": "4.12.0", + "reflect-metadata": "0.1.3", + "rxjs": "5.0.0-beta.6", + "systemjs": "0.19.27", + "zone.js": "0.6.12" }, "devDependencies": { - "concurrently": "^1.0.0", - "lite-server": "^1.3.2", - "typescript": "^1.7.5" + "awesome-typescript-loader": "^0.17.0", + "concurrently": "^2.0.0", + "core-js": "^2.4.0", + "jasmine-core": "^2.4.1", + "karma": "^0.13.22", + "karma-coverage": "^1.0.0", + "karma-jasmine": "^1.0.2", + "karma-phantomjs-launcher": "^1.0.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.26", + "karma-webpack": "^1.7.0", + "phantomjs-prebuilt": "^2.1.7", + "raw-loader": "^0.5.1", + "source-map-loader": "^0.1.5", + "typescript": "^1.8.10", + "webpack": "^1.13.0", + "webpack-dev-server": "^1.14.1" }, "author": "Lukas Ruebbelke", "license": "MIT" diff --git a/server/api/db.json b/server/api/db.json index 2952a44..c814fec 100644 --- a/server/api/db.json +++ b/server/api/db.json @@ -13,7 +13,29 @@ { "id": 3, "name": "Item 3", - "description": "This is a description" + "description": "This is a lovely item" + }, + { + "name": "hghg", + "description": "gfhg", + "id": 4 + } + ], + "widgets": [ + { + "id": 1, + "name": "Widget 1", + "price": 100 + }, + { + "id": 2, + "name": "Widget 2", + "price": 200 + }, + { + "id": 3, + "name": "Widget 3", + "price": 300 } ] -} +} \ No newline at end of file diff --git a/spec-bundle.js b/spec-bundle.js new file mode 100644 index 0000000..3da6a10 --- /dev/null +++ b/spec-bundle.js @@ -0,0 +1,10 @@ +require('core-js'); +require('zone.js'); + +var testContext = require.context('./client', true, /\.spec\.ts/); + +function requireAll(requireContext) { + return requireContext.keys().map(requireContext); +} + +var modules = requireAll(testContext); diff --git a/tsconfig.json b/tsconfig.json index 0ee8071..a38df26 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,28 +1,22 @@ { "compilerOptions": { "target": "es5", - "module": "system", - "sourceMap": true, + "module": "commonjs", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "moduleResolution": "node", - "removeComments": false, - "noImplicitAny": true, - "suppressImplicitAnyIndexErrors": true + "sourceMap": true, + "suppressImplicitAnyIndexErrors":true }, + "compileOnSave": false, + "buildOnSave": false, "exclude": [ "node_modules" ], "filesGlob": [ - "client/app/**/*.ts" + "client/**/*.ts", + "typings/index.d.ts" ], - "compileOnSave": false, "atom": { - "rewriteTsconfig": true - }, - "files": [ - "client/app/app.ts", - "client/app/boot.ts", - "client/app/items.ts" - ] + "rewriteTsconfig": false + } } diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..fab62f8 --- /dev/null +++ b/tslint.json @@ -0,0 +1,70 @@ +{ + "rulesDirectory": [ + "node_modules/ng2lint/dist/src" + ], + "rules": { + "component-selector-name": [true, "kebab-case"], + "component-selector-type": [true, "element"], + "host-parameter-decorator": true, + "input-parameter-decorator": true, + "output-parameter-decorator": true, + "attribute-parameter-decorator": false, + "input-property-directive": true, + "output-property-directive": true, + + "class-name": true, + "curly": false, + "eofline": true, + "indent": [ + true, + "spaces" + ], + "max-line-length": [ + true, + 100 + ], + "member-ordering": [ + true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ], + "no-arg": true, + "no-construct": true, + "no-duplicate-key": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "trailing-comma": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-unused-variable": false, + "no-unreachable": true, + "no-use-before-declare": true, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "semicolon": true, + "triple-equals": [ + true, + "allow-null-check" + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..1a9f282 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,15 @@ +{ + "mode": "modules", + "out": "doc", + "theme": "default", + "ignoreCompilerErrors": "true", + "experimentalDecorators": "true", + "emitDecoratorMetadata": "true", + "target": "ES5", + "moduleResolution": "node", + "preserveConstEnums": "true", + "stripInternal": "true", + "suppressExcessPropertyErrors": "true", + "suppressImplicitAnyIndexErrors": "true", + "module": "commonjs" +} \ No newline at end of file diff --git a/typings.json b/typings.json new file mode 100644 index 0000000..09c5a1c --- /dev/null +++ b/typings.json @@ -0,0 +1,10 @@ +{ + "globalDependencies": { + "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2", + "hammerjs": "github:DefinitelyTyped/DefinitelyTyped/hammerjs/hammerjs.d.ts#74a4dfc1bc2dfadec47b8aae953b28546cb9c6b7", + "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#4b36b94d5910aa8a4d20bdcd5bd1f9ae6ad18d3c", + "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#8cf8164641be73e8f1e652c2a5b967c7210b6729", + "selenium-webdriver": "github:DefinitelyTyped/DefinitelyTyped/selenium-webdriver/selenium-webdriver.d.ts#a83677ed13add14c2ab06c7325d182d0ba2784ea", + "webpack": "github:DefinitelyTyped/DefinitelyTyped/webpack/webpack.d.ts#95c02169ba8fa58ac1092422efbd2e3174a206f4" + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..465e0f0 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,32 @@ +var webpack = require('webpack'), + path = require('path'); + +function root(args) { + args = Array.prototype.slice.call(arguments, 0); + return path.join.apply(path, [__dirname].concat(args)); +} + +module.exports = { + devtool: 'source-map', + debug: true, + entry: './client/boot.ts', + resolve: { + extensions: ['', '.ts', '.js'] + }, + output: { + path: './build', + filename: 'bundle.js' + }, + module: { + preLoaders: [ + { test: /\.js$/, loader: 'source-map-loader', exclude: [ root('node_modules/rxjs'), root('node_modules/@ngrx') ] } + ], + loaders: [ + { test: /\.ts$/, loader: 'awesome-typescript-loader', exclude: [ /\.(spec|e2e)\.ts$/ ] }, + { test: /\.html$/, loader: 'raw' } + ] + }, + devServer: { + historyApiFallback: true + } +}; diff --git a/webpack.test.config.js b/webpack.test.config.js new file mode 100644 index 0000000..1e5d54b --- /dev/null +++ b/webpack.test.config.js @@ -0,0 +1,23 @@ +var webpack = require('webpack'), + path = require('path'); + +function root(args) { + args = Array.prototype.slice.call(arguments, 0); + return path.join.apply(path, [__dirname].concat(args)); +} + +module.exports = { + // the difference from the main webpack config + // is that we have removed "debug", "entry", "output", and "devServer" configs + // and moved the source map loader from "preloaders" to "loaders" + devtool: 'inline-source-map', + resolve: { + extensions: ['', '.ts', '.js'] + }, + module: { + loaders: [ + { test: /\.js$/, loader: "source-map-loader", exclude: [ root('node_modules/rxjs') ]}, + { test: /\.ts$/, loader: 'awesome-typescript-loader' } + ] + } +};