New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add basic Redux infra based on ng-redux #2504
Changes from all commits
53e00cc
1ac206b
6c928aa
abf968a
9ac7fdc
90ee5a0
c363266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"globals": { | ||
"require": false | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { configureNgReduxStore } from './store'; | ||
import { rootReducer, addReducer, clearReducers, applyReducerHash } from './reducer'; | ||
|
||
const app = ManageIQ.angular.app; | ||
|
||
const initialState = {}; | ||
|
||
// configure Angular application to use ng-redux | ||
configureNgReduxStore(app, initialState); | ||
|
||
// allow unit-testing specific module exports | ||
if (jasmine) { | ||
app.constant('_rootReducer', rootReducer); | ||
app.constant('_addReducer', addReducer); | ||
app.constant('_clearReducers', clearReducers); | ||
app.constant('_applyReducerHash', applyReducerHash); | ||
} | ||
|
||
// initialize Redux namespace upon application startup | ||
app.run(['$ngRedux', ($ngRedux) => { | ||
ManageIQ.redux = { | ||
store: $ngRedux, | ||
addReducer, | ||
applyReducerHash | ||
}; | ||
}]); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Middleware } from 'redux'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i find the name a bit confusing with the middleware provider, maybe we should comment that this is redux middleware :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can rename this file to (Still, this file is part of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, the path javascript/miq-redux is pretty clear |
||
|
||
export const middlewares: Middleware[] = []; | ||
|
||
// add middleware for handling async operations and side effects |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}]); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require('miq-redux'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { IModule } from 'angular'; | ||
|
||
import { ReduxApi } from '../miq-redux/redux-typings'; | ||
|
||
interface MiqAngular { | ||
app: IModule; | ||
} | ||
|
||
declare global { | ||
|
||
/** | ||
* `ManageIQ` runtime global, holding application-specific objects. | ||
*/ | ||
namespace ManageIQ { | ||
const angular: MiqAngular; | ||
let redux: ReduxApi; // initialized by miq-redux pack | ||
} | ||
|
||
/** | ||
* This global is available when running tests with Jasmine. | ||
*/ | ||
const jasmine: any; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No redux dependency? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
"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", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need to load it via the asset pipeline? I was under the assumption we can load it directly to the browser from webpack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well this is because the main angular app is bootstrapped here miq_angular_application.js#L1 and sadly you cannot add dependencies (modules), once app is loaded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to run
ng-redux
main script which registers thengRedux
module and hook it to the Angular application module.In
miq_angular_application.js
we declarengRedux
module as a dependency ofManageIQ
(application) module. The important bit is thatmiq_angular_application.js
is Rails-managed.My understanding is that when Rails-managed
miq_angular_application.js
executes, thengRedux
module registration must be already done - and this implies that we need to loadng-redux
library throughapplication.js
file.Officially yes, unofficially there's a way but it only works if you add those extra dependencies prior to application bootstrap (there are some undocumented APIs used by Angular injector).