Skip to content

Commit

Permalink
add NgRx, HMR state management, Redux dev tools
Browse files Browse the repository at this point in the history
Closes #2 #3 #13
  • Loading branch information
MarkPieszak committed Nov 13, 2016
1 parent e16ff29 commit 0e06dbc
Show file tree
Hide file tree
Showing 21 changed files with 327 additions and 48 deletions.
4 changes: 0 additions & 4 deletions Client/app/app.component.html

This file was deleted.

9 changes: 7 additions & 2 deletions Client/app/app.component.ts
Expand Up @@ -3,7 +3,12 @@ import { isBrowser, isNode } from 'angular2-universal';

@Component({
selector: 'app',
template: require('./app.component.html'),
template: `
<div class="container-fluid">
<app-nav-menu></app-nav-menu>
<router-outlet></router-outlet>
</div>
`,
styles: [require('./app.component.css')],
encapsulation: ViewEncapsulation.None
})
Expand All @@ -21,4 +26,4 @@ export class AppComponent {

}

}
}
4 changes: 2 additions & 2 deletions Client/app/app.e2e.ts
@@ -1,9 +1,9 @@
describe('App', () => {

beforeEach(() => {
//browser.get('/');
// browser.get('/');
});

it('e2e test should run', () => {
let subject = true;
let result = true;
Expand Down
10 changes: 6 additions & 4 deletions Client/app/app.routes.ts
Expand Up @@ -4,18 +4,20 @@
import {
HomeComponent,
RestTestComponent,
BootstrapComponent
} from '../containers';
BootstrapComponent,
LoginComponent
} from 'app-containers';

export const ROUTES : Route[] = [
export const ROUTES: Route[] = [
// Base route
{ path: '', redirectTo: 'home', pathMatch: 'full' },

// Other routes
{ path: 'home', component: HomeComponent },
{ path: 'bootstrap', component: BootstrapComponent },
{ path: 'rest-test', component: RestTestComponent },
{ path: 'login', component: LoginComponent },

// All else fails - go home
{ path: '**', redirectTo: 'home' }
]
];
22 changes: 22 additions & 0 deletions Client/app/index.ts
@@ -0,0 +1,22 @@


/*
This is our "Barrels" index
Here we can just export all individual things (in this folder)
We're also using TypeScript2's new "paths" to create non-directory import locations
So instead of having to do something crazy like: "from '../../app/'"
We can just do:
import { AppState } from 'app';
Makes life easier!
*/

export * from './app.component';
export * from './app.routes';

export * from './state/app.reducer';
export * from './state/app-state';
export * from './state/hmr';

@@ -1,16 +1,18 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

// for AoT we need to manually split universal packages (/browser & /node)
import { UniversalModule, isBrowser, isNode } from 'angular2-universal/browser';
import { Store, StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

// Bootstrap (non-jQuery implementation)
import { Ng2BootstrapModule } from 'ng2-bootstrap/ng2-bootstrap';

// for AoT we need to manually split universal packages (/browser & /node)
import { UniversalModule, isBrowser, isNode } from 'angular2-universal/browser';

// Main "APP" Root Component
import { AppComponent } from './app.component';
import { ROUTES } from './app.routes';
import { AppComponent, ROUTES, appReducer } from 'app';

// Component imports
import { NavMenuComponent } from 'app-components';
Expand All @@ -19,14 +21,22 @@ import { NavMenuComponent } from 'app-components';
import {
HomeComponent,
RestTestComponent,
BootstrapComponent
BootstrapComponent,
LoginComponent
} from 'app-containers';

// Provider (aka: "shared" | "services") imports
import {
HttpCacheService, CacheService // Universal : XHR Cache
} from 'app-shared';

//////////////////////////////////////////////////////////////////

// This imports the variable that, in a hot loading situation, holds
// a reference to the previous application's last state before
// it was destroyed.
import { appState } from 'app';

export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';

@NgModule({
Expand All @@ -36,6 +46,7 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
NavMenuComponent,
RestTestComponent,
HomeComponent,
LoginComponent,
BootstrapComponent
],
providers: [
Expand All @@ -51,9 +62,17 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
// Even make it dynamic whether it's for Browser or Server (Dependency Injection)
// isBrowser ? something : somethingElse, <- basic pseudo example

// NgRx
StoreModule.provideStore(appReducer, appState),
EffectsModule,
StoreDevtoolsModule.instrumentOnlyWithExtension(),

// Angular
FormsModule,
ReactiveFormsModule,
Ng2BootstrapModule,

// Routing
RouterModule.forRoot(ROUTES)
]
})
Expand All @@ -64,15 +83,14 @@ export class AppModule {
}

doRehydrate() {
console.log('DO REHYDRATE');
let defaultValue = {};
let serverCache = this._getCacheValue(CacheService.KEY, defaultValue);
this.cache.rehydrate(serverCache);
}

_getCacheValue(key: string, defaultValue: any): any {

console.log('_getCacheValue for ' + key);
console.log('Universal Cache for key :: ' + key);
console.log(window[UNIVERSAL_KEY]);

// browser
Expand Down
@@ -1,6 +1,8 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';

// for AoT we need to manually split universal packages (/browser & /node)
import { UniversalModule, isBrowser, isNode } from 'angular2-universal/node';
Expand All @@ -9,17 +11,20 @@ import { UniversalModule, isBrowser, isNode } from 'angular2-universal/node';
import { Ng2BootstrapModule } from 'ng2-bootstrap/ng2-bootstrap';

// Main "APP" Root Component
import { AppComponent } from './app.component';
import { ROUTES } from './app.routes';
import { AppComponent, ROUTES, appReducer } from 'app';

// Component imports
import { NavMenuComponent } from 'app-components';

// HMR Application State
import { appState } from 'app';

// Container (aka: "pages") imports
import {
HomeComponent,
RestTestComponent,
BootstrapComponent
BootstrapComponent,
LoginComponent
} from 'app-containers';

// Provider (aka: "shared" | "services") imports
Expand All @@ -30,10 +35,11 @@ import {
@NgModule({
bootstrap: [ AppComponent ],
declarations: [
AppComponent,
AppComponent,
NavMenuComponent,
RestTestComponent,
HomeComponent,
LoginComponent,
BootstrapComponent
],
providers: [
Expand All @@ -49,7 +55,12 @@ import {
// Even make it dynamic whether it's for Browser or Server (Dependency Injection)
// isBrowser ? something : somethingElse, <- basic pseudo example

// NgRx
StoreModule.provideStore(appReducer, appState),
EffectsModule,

FormsModule,
ReactiveFormsModule,
Ng2BootstrapModule,

RouterModule.forRoot(ROUTES)
Expand Down
25 changes: 25 additions & 0 deletions Client/app/state/app-state.ts
@@ -0,0 +1,25 @@
/*
* Model definition for the (immutable) application state.
*/
import { TypedRecord, makeTypedFactory } from 'typed-immutable-record';
import { List } from 'immutable';

// The TypeScript interface that defines the application state's properties.
// This is to be imported wherever a reference to the app state is used
// (reducers, components, services...)
export interface AppState {
loggedIn: boolean;
loggedInUser: {};
}

// An Immutable.js Record implementation of the AppState interface.
// This only needs to be imported by reducers, since they produce new versions
// of the state. Components should only ever read the state, never change it,
// so they should only need the interface, not the record.
export interface AppStateRecord extends TypedRecord<AppStateRecord>, AppState { }

// An Immutable.js record factory for the record.
export const appStateFactory = makeTypedFactory<AppState, AppStateRecord>({
loggedIn : false,
loggedInUser: {}
});
42 changes: 42 additions & 0 deletions Client/app/state/app.reducer.ts
@@ -0,0 +1,42 @@
/*
* The main (and only) @ngrx/store reducer for the application.
*
* This implements the application's core logic by handling actions
* and producing new versions of the immutable AppState record
* based on those actions.
*/
import { ActionReducer, Action } from '@ngrx/store';
import { List, Range } from 'immutable';
import { AppStateRecord, appStateFactory } from 'app';

// Action definitions
export const LOGIN_USER = 'LOlGIN_USER';
export const LOGOUT_USER = 'LOGOUT_USER';

// The reducer function. Receives actions and produces new application states.
export const appReducer: ActionReducer<AppStateRecord> = (state = makeInitialState(), action: Action) => {

switch (action.type) {

case LOGIN_USER:
console.log(action.payload);
// state.set('loggedInUser', action.payload);
// state.set('loggedIn', true);
return state.merge({ loggedInUser: action.payload, loggedIn: true });

case LOGOUT_USER:
return state.merge(makeInitialState());

default:
return state;
}
}

// Initial AppState, used to bootstrap the reducer.
function makeInitialState() {
return appStateFactory({
loggedIn: false,
loggedInUser: {}
});
}

62 changes: 62 additions & 0 deletions Client/app/state/hmr.ts
@@ -0,0 +1,62 @@
import { ApplicationRef, NgModuleRef } from '@angular/core';
import { Store } from '@ngrx/store';
import { createNewHosts, removeNgStyles } from '@angularclass/hmr';

import 'rxjs/add/operator/take';


// This variable will contain the application state after a module has been
// unloaded. From here it is read to the next application version when it's
// loaded (see app/app.module.ts).
export let appState: any;

// Called from main.ts when a hot bootstrap should be done.
// This function is called every time the application loads
// (first when the page loads, and then again after each hot reload)
export function handleHmr(
module: any, // The module that we're handling HMR for (it'll be the main.ts module)
bootstrap: () => Promise<NgModuleRef<any>>) { // The bootstrap function (comes from main.ts)

// Store a reference to the NgModule that we will bootstrap.
// We'll need it during unload.
let moduleRef: NgModuleRef<any>;

// Bootstrap the module and grab the NgModule reference from the
// promise when it's resolved. This will start the application.
bootstrap()
.then(mod => moduleRef = mod);

// Let Webpack know that we can handle hot loading for this module
module.hot.accept();

// Attach a callback to module unload. This'll be called during a hot
// reload, before the new version of the application comes in. We need to:
// 1) Grab the current state of the previous application so we can reuse it.
// 2) Destroy the previous application so that the new one can load cleanly.
module.hot.dispose(() => {
// Grab a reference to the running Angular application.
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
// Grab a reference to the application's @ngrx/store.
const store: Store<any> = moduleRef.injector.get(Store);
// Get the current state from the Store. The store is an Observable so
// we can use the Observable API to get the state. We'll get it synchronously
// though this code may look like we might not.
store.take(1).subscribe(s => appState = s);

// When an Angular app is destroyed, it will also remove the DOM elements
// of its root component(s) from the page. When doing hot loading, this is
// a problem because the next version of the app will have nothing to
// attach to. We need to clone the DOM nodes of the current application's root
// component(s)
const cmpLocations = appRef.components.map(cmp => cmp.location.nativeElement);
const disposeOldHosts = createNewHosts(cmpLocations);
moduleRef.destroy();
removeNgStyles();
disposeOldHosts();

// After this, the next version of the app will load momentarily.
// Webpack dev server will execute the new `main.ts` which will then call
// `handleHmr` again...
});

}

0 comments on commit 0e06dbc

Please sign in to comment.