diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..fa9a819
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+charset = utf-8
+max_line_length = 80
+indent_brace_style = 1TBS
+spaces_around_operators = true
+quote_type = auto
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..8c49fa2
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,53 @@
+{
+ "env": {
+ "browser": true,
+ "node": true,
+ "mocha": true,
+ "es6": true
+ },
+ "globals": {
+ "angular": true,
+ "moment": true,
+ "R": true,
+ "inject": true,
+ "Q": true,
+ "sinon": true,
+ "getService": true,
+ "expect": true,
+ "xit": true,
+ "dealoc": true,
+ "require": true
+ },
+ "rules": {
+ "camelcase": 2,
+ "curly": 2,
+ "brace-style": [2, "1tbs"],
+ "quotes": [2, "single"],
+ "semi": [2, "always"],
+ "object-curly-spacing": [2, "never"],
+ "array-bracket-spacing": [2, "never"],
+ "computed-property-spacing": [2, "never"],
+ "space-infix-ops": 2,
+ "space-after-keywords": [2, "always"],
+ "dot-notation": 2,
+ "eqeqeq": [2, "smart"],
+ "no-use-before-define": 2,
+ "no-redeclare": 2,
+ "no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
+ "no-floating-decimal": 2,
+ "no-spaced-func": 2,
+ "no-extra-parens": 1,
+ "no-underscore-dangle": 0,
+ "valid-jsdoc": 1,
+ "space-after-keywords": 2,
+ "max-len": [2, 120],
+ "no-use-before-define": [2, "nofunc"],
+ "no-warning-comments": 0,
+ "strict": 0,
+ "eol-last": 0,
+ "semi": 2
+ },
+ "ecmaFeatures": {
+ "modules": true
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9f05e53
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,82 @@
+# Logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Sass cache folder
+.sass-cache
+
+# Users Environment Variables
+.lock-wscript
+.idea/
+*~
+\#*#
+.#*
+*.keystore
+*.sw*
+.DS_Store
+._*
+Thumbs.db
+.cache
+*.sublime-project
+*.sublime-workspace
+*swp
+*swo
+*swn
+build
+dist
+temp
+.tmp
+node_modules
+bower_components
+jspm_packages
+.test
+www
+
+keys.md
+.env
+
+# Cordova
+platforms/
+plugins/
+
+# emacs
+.tern-port
+# -*- mode: gitignore; -*-
+*~
+\#*\#
+/.emacs.desktop
+/.emacs.desktop.lock
+*.elc
+auto-save-list
+tramp
+.\#*
+
+# Org-mode
+.org-id-locations
+*_archive
+
+# flymake-mode
+*_flymake.*
+
+# eshell files
+/eshell/history
+/eshell/lastdir
+
+# elpa packages
+/elpa/
+
+# reftex files
+*.rel
+
+# AUCTeX auto folder
+/auto/
+
+# cask packages
+.cask/
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..6c70de7
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ ng-redux-ui-router
+
+
+
+
+
+
diff --git a/example/index.js b/example/index.js
new file mode 100644
index 0000000..daa394e
--- /dev/null
+++ b/example/index.js
@@ -0,0 +1,114 @@
+import angular from 'angular';
+import uiRouter from 'angular-ui-router';
+
+import ngUiRouterMiddleware from '../src';
+import ngRedux from 'ng-redux';
+import * as redux from 'redux';
+import createLogger from 'redux-logger';
+
+const logger = createLogger({
+ level: 'info',
+ collapsed: true
+});
+
+import uiRouterState from '../src/router-state-reducer';
+
+const reducers = redux.combineReducers({
+ uiRouterState
+});
+
+import thunk from 'redux-thunk';
+
+export default angular
+ .module('app', [
+ uiRouter,
+ ngRedux,
+ ngUiRouterMiddleware
+ ])
+ .config(($urlRouterProvider, $stateProvider) => {
+ $urlRouterProvider.otherwise('/app');
+
+ $stateProvider
+ .state('app', {
+ url: '/app',
+ views: {
+ main: {
+ template: `
+ {{ globalState | json }}
+
+ `,
+ controller: ($scope, $ngRedux) => {
+ $scope.globalState = {};
+
+ $ngRedux.connect(state => state, state => $scope.globalState = state);
+ }
+ }
+ }
+ })
+ .state('app.child1', {
+ url: '/child1?hello?optional',
+ views: {
+ child: {
+ controller: ($scope, ngUiRouterActions) => {
+ $scope.goto = () => {
+ ngUiRouterActions.stateGo('app.child2');
+ }
+ },
+ template: `
+
+
Child View 1
+
+
+ `
+ }
+ }
+ })
+ .state('app.child2', {
+ url: '/child2',
+ views: {
+ child: {
+ controller: ($scope, ngUiRouterActions) => {
+ $scope.goto = () => {
+ ngUiRouterActions.stateGo('app.child1');
+ }
+
+ $scope.goWithReload = () => {
+ ngUiRouterActions.stateReload('app.child1');
+ }
+
+ $scope.goWithParams = () => {
+ ngUiRouterActions.stateGo('app.child1', {
+ hello: 'world',
+ optional: true
+ });
+ }
+ },
+ template: `
+
+
Child View 2
+
+
+
+
+ `
+ }
+ }
+ })
+ })
+ .config(($ngReduxProvider) => {
+ $ngReduxProvider.createStoreWith(reducers, ['ngUiRouterMiddleware', logger, thunk]);
+ })
+ .name;
diff --git a/example/package.json b/example/package.json
new file mode 100644
index 0000000..a165226
--- /dev/null
+++ b/example/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "angular-ui-router-redux",
+ "version": "0.1.0",
+ "main": "index.js",
+ "scripts": {
+ "start": "node server.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "devDependencies": {
+ "babel-core": "5.6.15",
+ "babel-loader": "^5.1.4",
+ "react-hot-loader": "^1.2.7",
+ "redux-batched-updates": "^0.1.0",
+ "webpack": "^1.9.11",
+ "webpack-dev-server": "^1.9.0"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "react-pure-component": "^0.1.0",
+ "react-redux": "^0.2.1",
+ "redux": "^1.0.0-alpha",
+ "redux-logger": "^1.0.4",
+ "redux-thunk": "^0.1.0"
+ }
+}
diff --git a/example/server.js b/example/server.js
new file mode 100644
index 0000000..627cbb3
--- /dev/null
+++ b/example/server.js
@@ -0,0 +1,18 @@
+var webpack = require('webpack');
+var WebpackDevServer = require('webpack-dev-server');
+var config = require('./webpack.config');
+
+new WebpackDevServer(webpack(config), {
+ publicPath: config.output.publicPath,
+ hot: true,
+ stats: {
+ colors: true
+ }
+})
+.listen(3000, 'localhost', function (err) {
+ if (err) {
+ console.log(err);
+ }
+
+ console.log('http://localhost:3000');
+});
diff --git a/example/styles.css b/example/styles.css
new file mode 100644
index 0000000..36dd9a1
--- /dev/null
+++ b/example/styles.css
@@ -0,0 +1,30 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+}
+
+main {
+ background-color: lightgray;
+}
+
+pre {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 300px;
+ bottom: 0;
+ background-color: black;
+ color: white;
+}
+
+.mainView {
+ margin-left: 400px;
+ min-height: 100vh;
+}
+
+.child-view {
+ background-color: white;
+ padding: 15px;
+ margin: 15px;
+}
diff --git a/example/webpack.config.js b/example/webpack.config.js
new file mode 100644
index 0000000..f89fc58
--- /dev/null
+++ b/example/webpack.config.js
@@ -0,0 +1,37 @@
+var path = require('path');
+var webpack = require('webpack');
+
+module.exports = {
+ devtool: 'eval',
+ entry: [
+ 'webpack-dev-server/client?http://localhost:3000',
+ 'webpack/hot/dev-server',
+ './index'
+ ],
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'bundle.js',
+ publicPath: '/static/'
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NoErrorsPlugin()
+ ],
+ resolve: {
+ extensions: ['', '.js'],
+ alias: {
+ 'react': path.join(__dirname, '..', '..', 'node_modules', 'react')
+ }
+ },
+ module: {
+ loaders: [{
+ test: /\.js$/,
+ loaders: ['babel'],
+ exclude: /node_modules/
+ }, {
+ test: /\.css?$/,
+ loaders: ['style', 'raw'],
+ exclude: /node_modules/
+ }]
+ }
+};
diff --git a/notes.md b/notes.md
new file mode 100644
index 0000000..2bb446c
--- /dev/null
+++ b/notes.md
@@ -0,0 +1,5 @@
+- Wondering if `toState` and `toParams` doesn't make sense in the context of the router state?
+ - `toParams` implies that the router is still going to a state
+ - Possibly rename to `currentParams` and `currentState`
+ - `fromState` and `fromParams` would become `prevState` and `prevParams`
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..77bff7d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "redux-ui-router-middleware",
+ "version": "0.1.0",
+ "description": "Redux middleware for use with Angular UI Router",
+ "main": "src/index.js",
+ "scripts": {
+ "lint": "node node_modules/.bin/eslint src",
+ "clean": "rm -rf lib",
+ "test": "npm run lint; NODE_ENV=test node node_modules/.bin/mocha --compilers js:babel/register --recursive --require src/__tests__/index.js src/**/*.test.js",
+ "test:live": "npm run lint; NODE_ENV=test node node_modules/.bin/mocha --compilers js:babel/register --recursive --require src/__tests__/index.js -w src/**/*.test.js",
+ "build": "node node_modules/.bin/babel src --out-dir lib"
+ },
+ "author": "Neil Fenton (neilff)",
+ "license": "ISC",
+ "dependencies": {
+ "angular": "^1.4.4",
+ "angular-ui-router": "^0.2.15"
+ },
+ "devDependencies": {
+ "babel-core": "5.6.15",
+ "babel-eslint": "^3.1.20",
+ "babel-loader": "^5.3.1",
+ "chai": "^3.0.0",
+ "eslint": "^0.24.0",
+ "mocha": "^2.2.5",
+ "node-libs-browser": "^0.5.2",
+ "redux": "1.0.0-alpha",
+ "sinon": "^1.15.4"
+ }
+}
diff --git a/src/action-types.js b/src/action-types.js
new file mode 100644
index 0000000..425bdab
--- /dev/null
+++ b/src/action-types.js
@@ -0,0 +1,17 @@
+// Exports the constants used for triggering transitions using Angular UI Router
+//
+// STATE_GO: Action for triggering a $state.go
+// STATE_RELOAD: Action for triggering a $state.reload
+// STATE_TRANSITION_TO: Action for triggering a $state.transitionTo
+//
+export const STATE_GO = '@@reduxUiRouter/stateGo';
+export const STATE_RELOAD = '@@reduxUiRouter/stateReload';
+export const STATE_TRANSITION_TO = '@@reduxUiRouter/transitionTo';
+
+// UI Router Events
+export const STATE_CHANGE_START = '@@reduxUiRouter/$stateChangeStart';
+export const STATE_CHANGE_SUCCESS = '@@reduxUiRouter/$stateChangeSuccess';
+export const STATE_CHANGE_ERROR = '@@reduxUiRouter/$stateChangeError';
+export const STATE_NOT_FOUND = '@@reduxUiRouter/$stateNotFound';
+export const VIEW_CONTENT_LOADING = '@@reduxUiRouter/$viewContentLoading';
+export const VIEW_CONTENT_LOADED = '@@reduxUiRouter/$viewContentLoaded';
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..2c196cf
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,15 @@
+import angular from 'angular';
+import uiRouter from 'angular-ui-router';
+
+import routerMiddleware from './router-middleware';
+import routerActions from './router-actions';
+import uiRouterListener from './router-listener';
+
+export default angular
+ .module('ng-ui-router-middleware', [
+ uiRouter
+ ])
+ .factory('ngUiRouterActions', routerActions)
+ .factory('ngUiRouterMiddleware', routerMiddleware)
+ .run(uiRouterListener)
+ .name;
diff --git a/src/router-actions.js b/src/router-actions.js
new file mode 100644
index 0000000..29bfb8a
--- /dev/null
+++ b/src/router-actions.js
@@ -0,0 +1,21 @@
+import onStateChangeStart from './state-change-start';
+import onStateChangeSuccess from './state-change-success';
+import stateGo from './state-go';
+import stateReload from './state-reload';
+import stateTransitionTo from './state-transition-to';
+
+import * as redux from 'redux';
+
+export default function routerActions($ngRedux) {
+ let actionCreator = {
+ onStateChangeStart,
+ onStateChangeSuccess,
+ stateGo,
+ stateReload,
+ stateTransitionTo
+ };
+
+ return redux.bindActionCreators(actionCreator, $ngRedux.getStore().dispatch);
+}
+
+routerActions.$inject = ['$ngRedux'];
diff --git a/src/router-listener.js b/src/router-listener.js
new file mode 100644
index 0000000..60d6f1d
--- /dev/null
+++ b/src/router-listener.js
@@ -0,0 +1,14 @@
+/**
+ * Listens for events emitted from Angular UI Router and fires redux events
+ *
+ * @param {object} $rootScope Dependency
+ * @param {object} ngUiRouterActions Dependency
+ * @param {object} accountSelectActions Dependency
+ * @return {undefined} undefined
+ */
+export default function RouterListener($rootScope, ngUiRouterActions) {
+ $rootScope.$on('$stateChangeStart', ngUiRouterActions.onStateChangeStart);
+ $rootScope.$on('$stateChangeSuccess', ngUiRouterActions.onStateChangeSuccess);
+}
+
+RouterListener.$inject = ['$rootScope', 'ngUiRouterActions'];
diff --git a/src/router-middleware.js b/src/router-middleware.js
new file mode 100644
index 0000000..05eb5ba
--- /dev/null
+++ b/src/router-middleware.js
@@ -0,0 +1,45 @@
+import {
+ STATE_GO,
+ STATE_RELOAD,
+ STATE_TRANSITION_TO,
+ STATE_CHANGE_START,
+ STATE_CHANGE_SUCCESS,
+ STATE_CHANGE_ERROR,
+ STATE_NOT_FOUND,
+ VIEW_CONTENT_LOADING,
+ VIEW_CONTENT_LOADED,
+} from './action-types';
+
+export default function routerMiddleware($state) {
+ return store => next => action => {
+ if (action.type === STATE_GO) {
+ let {
+ payload: {
+ to: to,
+ params: params,
+ options: options,
+ }
+ } = action;
+
+ return $state.go(to, params, options);
+ } else if (action.type === STATE_RELOAD) {
+ let { payload } = action;
+
+ return $state.reload(payload);
+ } else if (action.type === STATE_TRANSITION_TO) {
+ let {
+ payload: {
+ to: to,
+ params: params,
+ options: options,
+ }
+ } = action;
+
+ return $state.transitionTo(to, params, options);
+ } else {
+ next(action);
+ }
+ };
+}
+
+routerMiddleware.$inject = ['$state'];
diff --git a/src/router-state-reducer.js b/src/router-state-reducer.js
new file mode 100644
index 0000000..466a031
--- /dev/null
+++ b/src/router-state-reducer.js
@@ -0,0 +1,23 @@
+import { STATE_CHANGE_SUCCESS } from './action-types';
+
+const INITIAL_STATE = {
+ event: null,
+ toState: null,
+ toParams: null,
+ fromState: null,
+ fromParams: null
+};
+
+/**
+ * Reducer of STATE_CHANGE_SUCCESS actions. Returns a state object
+ * with { toState, toParams, fromState, fromParams }
+ *
+ * @param {Object} state - Previous state
+ * @param {Object} action - Action
+ * @return {Object} New state
+ */
+export default function routerStateReducer(state = INITIAL_STATE, action) {
+ return action.type === STATE_CHANGE_SUCCESS
+ ? action.payload
+ : state;
+}
diff --git a/src/state-change-start.js b/src/state-change-start.js
new file mode 100644
index 0000000..0779347
--- /dev/null
+++ b/src/state-change-start.js
@@ -0,0 +1,26 @@
+import { STATE_CHANGE_START } from './action-types';
+
+/**
+ * This action is triggered when a $stateChangeStart event is broadcast.
+ * Accepts a payload which matches the UI Router $stateChangeStart event.
+ *
+ * http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
+ *
+ * @param {Object} evt event details
+ * @param {Object} toState To state definition
+ * @param {Object} toParams To params object
+ * @param {Object} fromState From state definition
+ * @param {Object} fromParams From params object
+ * @return {Object} Action object
+ */
+export default function onStateChangeStart(evt, toState, toParams, fromState, fromParams) {
+ return {
+ type: STATE_CHANGE_START,
+ payload: {
+ toState,
+ toParams,
+ fromState,
+ fromParams
+ }
+ };
+}
diff --git a/src/state-change-success.js b/src/state-change-success.js
new file mode 100644
index 0000000..bd26004
--- /dev/null
+++ b/src/state-change-success.js
@@ -0,0 +1,26 @@
+import { STATE_CHANGE_SUCCESS } from './action-types';
+
+/**
+ * This action is triggered when a $stateChangeSuccess event is broadcast.
+ * Accepts a payload which matches the UI Router $stateChangeSuccess event.
+ *
+ * http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
+ *
+ * @param {Object} evt event details
+ * @param {Object} toState To state definition
+ * @param {Object} toParams To params object
+ * @param {Object} fromState From state definition
+ * @param {Object} fromParams From params object
+ * @return {Object} Action object
+ */
+export default function onStateChangeSuccess(evt, toState, toParams, fromState, fromParams) {
+ return {
+ type: STATE_CHANGE_SUCCESS,
+ payload: {
+ toState,
+ toParams,
+ fromState,
+ fromParams
+ }
+ };
+}
diff --git a/src/state-go.js b/src/state-go.js
new file mode 100644
index 0000000..0f530e6
--- /dev/null
+++ b/src/state-go.js
@@ -0,0 +1,19 @@
+import { STATE_GO } from './action-types';
+
+/**
+ * This action create will trigger a $state.go in the UiRouter Middleware.
+ * Accepts a payload which matches the UI Router $state.go function.
+ *
+ * http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
+ *
+ * @param {String} to - State name
+ * @param {Object} params - (optional) Parameters to send with state
+ * @param {Object} options - (optional) Options object
+ * @return {Object} Action object
+ */
+export default function stateGo(to, params, options) {
+ return {
+ type: STATE_GO,
+ payload: { to, params, options }
+ };
+}
diff --git a/src/state-reload.js b/src/state-reload.js
new file mode 100644
index 0000000..1f7fe37
--- /dev/null
+++ b/src/state-reload.js
@@ -0,0 +1,17 @@
+import { STATE_RELOAD } from './action-types';
+
+/**
+ * This action create will trigger a $state.reload in the UiRouter Middleware.
+ * Accepts a payload which matches the UI Router $state.reload function.
+ *
+ * http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
+ *
+ * @param {String} state - (optional) Root of the resolves to be re-resolved
+ * @return {Object} Action object
+ */
+export default function stateReload(state) {
+ return {
+ type: STATE_RELOAD,
+ payload: state
+ };
+}
diff --git a/src/state-transition-to.js b/src/state-transition-to.js
new file mode 100644
index 0000000..966f3fa
--- /dev/null
+++ b/src/state-transition-to.js
@@ -0,0 +1,19 @@
+import { STATE_TRANSITION_TO } from './action-types';
+
+/**
+ * This action create will trigger a $state.transitionTo in the UiRouter Middleware.
+ * Accepts a payload which matches the UI Router $state.transitionTo function.
+ *
+ * http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
+ *
+ * @param {String} to - State name
+ * @param {Object} toParams - (optional) Parameters to send with state
+ * @param {Object} options - (optional) Options object
+ * @return {Object} Action object
+ */
+export default function stateTransitionTo(to, toParams, options) {
+ return {
+ type: STATE_TRANSITION_TO,
+ payload: { to, toParams, options }
+ };
+}