Skip to content
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

Merged
merged 7 commits into from Dec 23, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -14,6 +14,7 @@ spec/reports/
tmp/
.rubocop-*
.idea/
.vscode/
.ruby-version
vendor/assets/bower_components/
node_modules/
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
Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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 the ngRedux module and hook it to the Angular application module.

In miq_angular_application.js we declare ngRedux module as a dependency of ManageIQ (application) module. The important bit is that miq_angular_application.js is Rails-managed.

My understanding is that when Rails-managed miq_angular_application.js executes, the ngRedux module registration must be already done - and this implies that we need to load ng-redux library through application.js file.

you cannot add dependencies (modules), once app is loaded.

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).

//= 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/.eslintrc.json
@@ -0,0 +1,5 @@
{
"globals": {
"require": false
}
}
26 changes: 26 additions & 0 deletions app/javascript/miq-redux/index.ts
@@ -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
};
}]);
5 changes: 5 additions & 0 deletions app/javascript/miq-redux/middleware.ts
@@ -0,0 +1,5 @@
import { Middleware } from 'redux';
Copy link
Member

Choose a reason for hiding this comment

The 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 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rename this file to redux-middleware.ts to reduce confusion.

(Still, this file is part of miq-redux pack which implicitly narrows down the context to Redux.)

Copy link
Member

Choose a reason for hiding this comment

The 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
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);
}]);
}
1 change: 1 addition & 0 deletions app/javascript/packs/miq-redux-common.js
@@ -0,0 +1 @@
require('miq-redux');
24 changes: 24 additions & 0 deletions app/javascript/typings/globals.d.ts
@@ -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;

}
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No redux dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No redux dependency?

redux is pulled in through ng-redux as transitive dependency, see here.

ng-redux@^3.5.2 relies on redux@^3.7.2 and let's say we add dependency on redux@^4.x this might break ng-redux expecting version 3.x APIs. This is why I didn't include redux dependency here.

"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