Skip to content

Commit

Permalink
Add basic Redux infra based on ng-redux
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechszocs committed Oct 24, 2017
1 parent 6a78ef5 commit 3efe18b
Show file tree
Hide file tree
Showing 15 changed files with 377 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Expand Up @@ -22,6 +22,7 @@
"sendDataWithRx": false,
"vanillaJsAPI": false, // local: miq_api.js
"ManageIQ": false, // local: mig_global.js
"miqApp": false, // alias: ManageIQ.angular.app
"Promise": false, // bower: es6-shim
"_": false, // bower: lodash
"__": false, // local: i18n.js
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Expand Up @@ -14,6 +14,7 @@
//= require angular-bootstrap/ui-bootstrap-tpls
//= require angular-sanitize
//= require angular.validators
//= require ng-redux/dist/ng-redux
//= require moment
//= require moment-strftime/lib/moment-strftime
//= require moment-timezone
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/miq_angular_application.js
Expand Up @@ -6,6 +6,7 @@ ManageIQ.angular.app = angular.module('ManageIQ', [
'patternfly.charts',
'frapontillo.bootstrap-switch',
'angular.validators',
'ngRedux',
'miq.api',
'miq.card',
'miq.compat',
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/miq_global.js
Expand Up @@ -84,5 +84,6 @@ if (! window.ManageIQ) {
debounced: {}, // running debounces
debounce_counter: 1,
},
redux: null // Redux API
};
}
5 changes: 5 additions & 0 deletions app/javascript/miq-redux/middleware.ts
@@ -0,0 +1,5 @@
import { Middleware } from 'redux';

export const middlewares: Middleware[] = [];

// add middleware for handling async operations and side effects
74 changes: 74 additions & 0 deletions app/javascript/miq-redux/reducer.ts
@@ -0,0 +1,74 @@
import { Unsubscribe } from 'redux';

import { AppState, AppReducer, Action, AppReducerHash } from './redux-typings';

const reducers: Set<AppReducer> = new Set();

/**
* Root reducer, used when creating the Redux store.
*
* The implementation simply invokes each registered `AppReducer`
* in a loop.
*
* @param state Current application state.
* @param action Action being dispatched.
*
* @returns New application state.
*/
export function rootReducer(state: AppState, action: Action): AppState {
let newState = state;

reducers.forEach((appReducer) => {
newState = appReducer(newState, action);
});

return newState;
}

/**
* Add given reducer to the list of registered application reducers.
*
* @param appReducer Reducer to add.
*
* @returns Function to remove (unsubscribe) the given reducer.
*/
export function addReducer(appReducer: AppReducer): Unsubscribe {
reducers.add(appReducer);

return () => {
reducers.delete(appReducer);
};
}

/**
* Remove all registered application reducers.
*
* *For testing purposes only.*
*/
export function clearReducers(): void {
reducers.clear();
}

/**
* Apply a collection of reducers, represented as `AppReducerHash`,
* to compute new application state.
*
* The implementation looks for a key that matches action's `type`.
* If present, the corresponding reducer is invoked to compute the
* new state. Otherwise, original state is returned.
*
* @param reducerHash Reducer hash to use.
* @param state Current application state.
* @param action Action being dispatched.
*
* @returns New application state.
*/
export function applyReducerHash(reducerHash: AppReducerHash, state: AppState, action: Action): AppState {
let newState = state;

if (reducerHash.hasOwnProperty(action.type)) {
newState = reducerHash[action.type](state, action);
}

return newState;
}
53 changes: 53 additions & 0 deletions app/javascript/miq-redux/redux-typings.ts
@@ -0,0 +1,53 @@
import { Reducer, Action as BaseAction, Store, Unsubscribe } from 'redux';

/**
* Application state.
*
* Its shape depends on specific `AppReducer` functions added through
* `ReduxApi`. Therefore, its shape is dynamic and declared simply as
* `object`.
*/
export type AppState = object;

/**
* Application reducer, operating on `AppState`.
*
* As per Redux design, reducers should
* - be pure functions without side effects,
* - return new state if `action` was acted upon, otherwise return
* original state.
*/
export type AppReducer = Reducer<AppState>;

/**
* Redux action object.
*
* As per Redux design, each action must define the `type` property.
* Any data associated with the action should go into the `payload`.
*/
export interface Action extends BaseAction {
payload?: any;
}

/**
* Application reducer hash, containing action types as keys and the
* corresponding reducers (to handle those action types) as values.
*/
export interface AppReducerHash {
[propName: string]: AppReducer;
}

/**
* Redux store, holding application's state tree and providing
* functions to dispatch actions and subscribe to state changes.
*/
export type ReduxStore = Store<AppState>;

/**
* `ManageIQ.redux` API.
*/
export interface ReduxApi {
store: ReduxStore;
addReducer(appReducer: AppReducer): Unsubscribe;
applyReducerHash(reducerHash: AppReducerHash, state: AppState, action: Action): AppState;
}
29 changes: 29 additions & 0 deletions app/javascript/miq-redux/store.ts
@@ -0,0 +1,29 @@
import { IModule } from 'angular';
import { devToolsEnhancer, EnhancerOptions } from 'redux-devtools-extension/logOnlyInProduction';

import { rootReducer } from './reducer';
import { middlewares } from './middleware';
import { AppState } from './redux-typings';

const devToolsOptions: EnhancerOptions = {};

/**
* Configure Angular application to use Redux store using `ng-redux`
* implementation.
*
* The store supports Redux DevTools browser extension in development
* as well as production, allowing users to inspect application state.
* In production, however, Redux DevTools runs in *log only* mode.
*
* @param app Angular application to configure.
* @param initialState Initial application state.
*/
export function configureNgReduxStore(app: IModule, initialState: AppState): void {
app.config(['$ngReduxProvider', ($ngReduxProvider) => {
$ngReduxProvider.createStoreWith(
rootReducer,
middlewares,
[devToolsEnhancer(devToolsOptions)],
initialState);
}]);
}
13 changes: 13 additions & 0 deletions app/javascript/miq-redux/test-helper.ts
@@ -0,0 +1,13 @@
import { IModule } from 'angular';

import { rootReducer, addReducer, clearReducers, applyReducerHash } from './reducer';

const app: IModule = miqApp;

// allow unit-testing specific module exports
if (jasmine) {
app.constant('_rootReducer', rootReducer);
app.constant('_addReducer', addReducer);
app.constant('_clearReducers', clearReducers);
app.constant('_applyReducerHash', applyReducerHash);
}
14 changes: 14 additions & 0 deletions app/javascript/packs/custom-typings.ts
@@ -0,0 +1,14 @@
/**
* Runtime global, contains all application-specific objects.
*/
declare const ManageIQ: any;

/**
* Compile-time global, alias for `ManageIQ.angular.app`.
*/
declare const miqApp: any;

/**
* Runtime global, available when running tests with Jasmine.
*/
declare const jasmine: any;
24 changes: 24 additions & 0 deletions app/javascript/packs/miq-redux-common.ts
@@ -0,0 +1,24 @@
import { IModule } from 'angular';

import { configureNgReduxStore } from '../miq-redux/store';
import { addReducer, applyReducerHash } from '../miq-redux/reducer';
import { ReduxStore, ReduxApi } from '../miq-redux/redux-typings';
import '../miq-redux/test-helper';

const app: IModule = miqApp;

const initialState = {};

// configure Angular application to use ng-redux
configureNgReduxStore(app, initialState);

// initialize Redux namespace upon application startup
app.run(['$ngRedux', ($ngRedux: ReduxStore) => {
const reduxApi: ReduxApi = {
store: $ngRedux,
addReducer,
applyReducerHash
};

ManageIQ.redux = reduxApi;
}]);
3 changes: 3 additions & 0 deletions config/webpack/shared.js
Expand Up @@ -44,6 +44,9 @@ module.exports = {

plugins: [
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
new webpack.DefinePlugin({
miqApp: 'ManageIQ.angular.app'
}),
new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'),

// Workaround for angular/angular#11580
Expand Down
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -23,11 +23,15 @@
"@angular/platform-browser": "~4.0.3",
"@angular/platform-browser-dynamic": "~4.0.3",
"core-js": "~2.4.1",
"ng-redux": "^3.5.2",
"redux-devtools-extension": "^2.13.2",
"rxjs": "~5.3.0",
"ui-select": "0.19.8",
"zone.js": "~0.8.5"
},
"devDependencies": {
"@types/angular": "1.6.29",
"@types/redux": "^3.6.0",
"autoprefixer": "~6.7.7",
"babel-core": "~6.24.1",
"babel-eslint": "~6.0.4",
Expand Down

0 comments on commit 3efe18b

Please sign in to comment.