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
-
-
-
- Cancel
- Save
-
-
- `
-})
-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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
+
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
+
+
+
+ Cancel
+ Save
+
+
+ `
+})
+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' }
+ ]
+ }
+};