Skip to content

Commit 3efe18b

Browse files
committed
Add basic Redux infra based on ng-redux
1 parent 6a78ef5 commit 3efe18b

File tree

15 files changed

+377
-0
lines changed

15 files changed

+377
-0
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"sendDataWithRx": false,
2323
"vanillaJsAPI": false, // local: miq_api.js
2424
"ManageIQ": false, // local: mig_global.js
25+
"miqApp": false, // alias: ManageIQ.angular.app
2526
"Promise": false, // bower: es6-shim
2627
"_": false, // bower: lodash
2728
"__": false, // local: i18n.js

app/assets/javascripts/application.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
//= require angular-bootstrap/ui-bootstrap-tpls
1515
//= require angular-sanitize
1616
//= require angular.validators
17+
//= require ng-redux/dist/ng-redux
1718
//= require moment
1819
//= require moment-strftime/lib/moment-strftime
1920
//= require moment-timezone

app/assets/javascripts/miq_angular_application.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ ManageIQ.angular.app = angular.module('ManageIQ', [
66
'patternfly.charts',
77
'frapontillo.bootstrap-switch',
88
'angular.validators',
9+
'ngRedux',
910
'miq.api',
1011
'miq.card',
1112
'miq.compat',

app/assets/javascripts/miq_global.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,6 @@ if (! window.ManageIQ) {
8484
debounced: {}, // running debounces
8585
debounce_counter: 1,
8686
},
87+
redux: null // Redux API
8788
};
8889
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Middleware } from 'redux';
2+
3+
export const middlewares: Middleware[] = [];
4+
5+
// add middleware for handling async operations and side effects
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Unsubscribe } from 'redux';
2+
3+
import { AppState, AppReducer, Action, AppReducerHash } from './redux-typings';
4+
5+
const reducers: Set<AppReducer> = new Set();
6+
7+
/**
8+
* Root reducer, used when creating the Redux store.
9+
*
10+
* The implementation simply invokes each registered `AppReducer`
11+
* in a loop.
12+
*
13+
* @param state Current application state.
14+
* @param action Action being dispatched.
15+
*
16+
* @returns New application state.
17+
*/
18+
export function rootReducer(state: AppState, action: Action): AppState {
19+
let newState = state;
20+
21+
reducers.forEach((appReducer) => {
22+
newState = appReducer(newState, action);
23+
});
24+
25+
return newState;
26+
}
27+
28+
/**
29+
* Add given reducer to the list of registered application reducers.
30+
*
31+
* @param appReducer Reducer to add.
32+
*
33+
* @returns Function to remove (unsubscribe) the given reducer.
34+
*/
35+
export function addReducer(appReducer: AppReducer): Unsubscribe {
36+
reducers.add(appReducer);
37+
38+
return () => {
39+
reducers.delete(appReducer);
40+
};
41+
}
42+
43+
/**
44+
* Remove all registered application reducers.
45+
*
46+
* *For testing purposes only.*
47+
*/
48+
export function clearReducers(): void {
49+
reducers.clear();
50+
}
51+
52+
/**
53+
* Apply a collection of reducers, represented as `AppReducerHash`,
54+
* to compute new application state.
55+
*
56+
* The implementation looks for a key that matches action's `type`.
57+
* If present, the corresponding reducer is invoked to compute the
58+
* new state. Otherwise, original state is returned.
59+
*
60+
* @param reducerHash Reducer hash to use.
61+
* @param state Current application state.
62+
* @param action Action being dispatched.
63+
*
64+
* @returns New application state.
65+
*/
66+
export function applyReducerHash(reducerHash: AppReducerHash, state: AppState, action: Action): AppState {
67+
let newState = state;
68+
69+
if (reducerHash.hasOwnProperty(action.type)) {
70+
newState = reducerHash[action.type](state, action);
71+
}
72+
73+
return newState;
74+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Reducer, Action as BaseAction, Store, Unsubscribe } from 'redux';
2+
3+
/**
4+
* Application state.
5+
*
6+
* Its shape depends on specific `AppReducer` functions added through
7+
* `ReduxApi`. Therefore, its shape is dynamic and declared simply as
8+
* `object`.
9+
*/
10+
export type AppState = object;
11+
12+
/**
13+
* Application reducer, operating on `AppState`.
14+
*
15+
* As per Redux design, reducers should
16+
* - be pure functions without side effects,
17+
* - return new state if `action` was acted upon, otherwise return
18+
* original state.
19+
*/
20+
export type AppReducer = Reducer<AppState>;
21+
22+
/**
23+
* Redux action object.
24+
*
25+
* As per Redux design, each action must define the `type` property.
26+
* Any data associated with the action should go into the `payload`.
27+
*/
28+
export interface Action extends BaseAction {
29+
payload?: any;
30+
}
31+
32+
/**
33+
* Application reducer hash, containing action types as keys and the
34+
* corresponding reducers (to handle those action types) as values.
35+
*/
36+
export interface AppReducerHash {
37+
[propName: string]: AppReducer;
38+
}
39+
40+
/**
41+
* Redux store, holding application's state tree and providing
42+
* functions to dispatch actions and subscribe to state changes.
43+
*/
44+
export type ReduxStore = Store<AppState>;
45+
46+
/**
47+
* `ManageIQ.redux` API.
48+
*/
49+
export interface ReduxApi {
50+
store: ReduxStore;
51+
addReducer(appReducer: AppReducer): Unsubscribe;
52+
applyReducerHash(reducerHash: AppReducerHash, state: AppState, action: Action): AppState;
53+
}

app/javascript/miq-redux/store.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { IModule } from 'angular';
2+
import { devToolsEnhancer, EnhancerOptions } from 'redux-devtools-extension/logOnlyInProduction';
3+
4+
import { rootReducer } from './reducer';
5+
import { middlewares } from './middleware';
6+
import { AppState } from './redux-typings';
7+
8+
const devToolsOptions: EnhancerOptions = {};
9+
10+
/**
11+
* Configure Angular application to use Redux store using `ng-redux`
12+
* implementation.
13+
*
14+
* The store supports Redux DevTools browser extension in development
15+
* as well as production, allowing users to inspect application state.
16+
* In production, however, Redux DevTools runs in *log only* mode.
17+
*
18+
* @param app Angular application to configure.
19+
* @param initialState Initial application state.
20+
*/
21+
export function configureNgReduxStore(app: IModule, initialState: AppState): void {
22+
app.config(['$ngReduxProvider', ($ngReduxProvider) => {
23+
$ngReduxProvider.createStoreWith(
24+
rootReducer,
25+
middlewares,
26+
[devToolsEnhancer(devToolsOptions)],
27+
initialState);
28+
}]);
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { IModule } from 'angular';
2+
3+
import { rootReducer, addReducer, clearReducers, applyReducerHash } from './reducer';
4+
5+
const app: IModule = miqApp;
6+
7+
// allow unit-testing specific module exports
8+
if (jasmine) {
9+
app.constant('_rootReducer', rootReducer);
10+
app.constant('_addReducer', addReducer);
11+
app.constant('_clearReducers', clearReducers);
12+
app.constant('_applyReducerHash', applyReducerHash);
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Runtime global, contains all application-specific objects.
3+
*/
4+
declare const ManageIQ: any;
5+
6+
/**
7+
* Compile-time global, alias for `ManageIQ.angular.app`.
8+
*/
9+
declare const miqApp: any;
10+
11+
/**
12+
* Runtime global, available when running tests with Jasmine.
13+
*/
14+
declare const jasmine: any;

0 commit comments

Comments
 (0)