diff --git a/examples/async-immutable/.babelrc b/examples/async-immutable/.babelrc
new file mode 100644
index 0000000..c13c5f6
--- /dev/null
+++ b/examples/async-immutable/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015"]
+}
diff --git a/examples/async-immutable/actions/asyncService.js b/examples/async-immutable/actions/asyncService.js
new file mode 100644
index 0000000..10f0793
--- /dev/null
+++ b/examples/async-immutable/actions/asyncService.js
@@ -0,0 +1,67 @@
+import * as types from '../constants/ActionTypes';
+
+function selectReddit(reddit) {
+ return {
+ type: types.SELECT_REDDIT,
+ reddit
+ };
+}
+
+function invalidateReddit(reddit) {
+ return {
+ type: types.INVALIDATE_REDDIT,
+ reddit
+ };
+}
+
+function requestPosts(reddit) {
+ return {
+ type: types.REQUEST_POSTS,
+ reddit
+ };
+}
+
+function receivePosts(reddit, json) {
+ return {
+ type: types.RECEIVE_POSTS,
+ reddit: reddit,
+ posts: json.data.children.map(child => child.data),
+ receivedAt: Date.now()
+ };
+}
+
+export default function asyncService($http) {
+ function fetchPosts(reddit) {
+ return dispatch => {
+ dispatch(requestPosts(reddit));
+ return $http.get(`http://www.reddit.com/r/${reddit}.json`)
+ .then(response => response.data)
+ .then(json => dispatch(receivePosts(reddit, json)));
+ };
+ }
+
+ function shouldFetchPosts(state, reddit) {
+ const posts = state.postsByReddit.get(reddit);
+ if (!posts) {
+ return true;
+ }
+ if (posts.get('isFetching')) {
+ return false;
+ }
+ return posts.get('didInvalidate');
+ }
+
+ function fetchPostsIfNeeded(reddit) {
+ return (dispatch, getState) => {
+ if (shouldFetchPosts(getState(), reddit)) {
+ return dispatch(fetchPosts(reddit));
+ }
+ };
+ }
+
+ return {
+ selectReddit,
+ invalidateReddit,
+ fetchPostsIfNeeded
+ };
+}
diff --git a/examples/async-immutable/components/picker.html b/examples/async-immutable/components/picker.html
new file mode 100644
index 0000000..a882c7e
--- /dev/null
+++ b/examples/async-immutable/components/picker.html
@@ -0,0 +1,7 @@
+
+ {{picker.value}}
+
+
diff --git a/examples/async-immutable/components/picker.js b/examples/async-immutable/components/picker.js
new file mode 100644
index 0000000..efd072c
--- /dev/null
+++ b/examples/async-immutable/components/picker.js
@@ -0,0 +1,17 @@
+export default function picker() {
+ return {
+ restrict: 'E',
+ controllerAs: 'picker',
+ controller: PickerController,
+ template: require('./picker.html'),
+ scope: {
+ options: '=',
+ value: '=',
+ onChange: '='
+ },
+ bindToController: true
+ };
+}
+
+class PickerController {
+}
diff --git a/examples/async-immutable/components/posts.html b/examples/async-immutable/components/posts.html
new file mode 100644
index 0000000..f8dfd85
--- /dev/null
+++ b/examples/async-immutable/components/posts.html
@@ -0,0 +1,3 @@
+
diff --git a/examples/async-immutable/components/posts.js b/examples/async-immutable/components/posts.js
new file mode 100644
index 0000000..03d07a8
--- /dev/null
+++ b/examples/async-immutable/components/posts.js
@@ -0,0 +1,15 @@
+export default function posts() {
+ return {
+ restrict: 'E',
+ controllerAs: 'posts',
+ controller: PostsController,
+ template: require('./posts.html'),
+ scope: {
+ posts: '=',
+ },
+ bindToController: true
+ };
+}
+
+class PostsController {
+}
diff --git a/examples/async-immutable/constants/ActionTypes.js b/examples/async-immutable/constants/ActionTypes.js
new file mode 100644
index 0000000..1f7729a
--- /dev/null
+++ b/examples/async-immutable/constants/ActionTypes.js
@@ -0,0 +1,4 @@
+export const REQUEST_POSTS = 'REQUEST_POSTS';
+export const RECEIVE_POSTS = 'RECEIVE_POSTS';
+export const SELECT_REDDIT = 'SELECT_REDDIT';
+export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT';
diff --git a/examples/async-immutable/containers/app.html b/examples/async-immutable/containers/app.html
new file mode 100644
index 0000000..ade6cd2
--- /dev/null
+++ b/examples/async-immutable/containers/app.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Last updated at {{ app.lastUpdated | date:'mediumTime' }}.
+
+
+ Refresh
+
+
+
Loading...
+
Empty.
+
+
+
+
diff --git a/examples/async-immutable/containers/app.js b/examples/async-immutable/containers/app.js
new file mode 100644
index 0000000..fb8c7fd
--- /dev/null
+++ b/examples/async-immutable/containers/app.js
@@ -0,0 +1,55 @@
+import { getPostsTojs, getIsFetching } from '../selectors'
+
+export default function app() {
+ return {
+ restrict: 'E',
+ controllerAs: 'app',
+ controller: AppController,
+ template: require('./app.html'),
+ scope: {}
+ };
+}
+
+class AppController {
+
+ constructor($ngRedux, AsyncActions, $scope) {
+ const unsubscribe = $ngRedux.connect(this.mapStateToThis, AsyncActions)((selectedState, actions) => {
+ this.componentWillReceiveStateAndActions(selectedState, actions);
+ Object.assign(this, selectedState, actions);
+ });
+ this.options = ['angularjs', 'frontend'];
+ this.handleChange = this.handleChange.bind(this);
+ this.handleRefreshClick = this.handleRefreshClick.bind(this);
+
+ this.fetchPostsIfNeeded(this.selectedReddit);
+ $scope.$on('$destroy', unsubscribe);
+ }
+
+ componentWillReceiveStateAndActions(nextState, nextActions) {
+ if (nextState.selectedReddit !== this.selectedReddit) {
+ nextActions.fetchPostsIfNeeded(nextState.selectedReddit);
+ }
+ }
+
+ handleChange(nextReddit) {
+ this.selectReddit(nextReddit);
+ }
+
+ handleRefreshClick() {
+ this.invalidateReddit(this.selectedReddit);
+ this.fetchPostsIfNeeded(this.selectedReddit);
+ }
+
+ mapStateToThis(state) {
+ const { selectedReddit, postsByReddit } = state;
+
+ return {
+ selectedReddit,
+ // Use selectors here
+ posts: getPostsTojs(state),
+ isFetching: getIsFetching(state),
+ // Or get value from the state directly without selectors
+ lastUpdated: postsByReddit.get(selectedReddit) && postsByReddit.get(selectedReddit).get('lastUpdated')
+ };
+ }
+}
diff --git a/examples/async-immutable/index.html b/examples/async-immutable/index.html
new file mode 100644
index 0000000..77b2d7d
--- /dev/null
+++ b/examples/async-immutable/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ {%= o.htmlWebpackPlugin.options.title %}
+
+
+
+
+
+
+
diff --git a/examples/async-immutable/index.js b/examples/async-immutable/index.js
new file mode 100644
index 0000000..84060de
--- /dev/null
+++ b/examples/async-immutable/index.js
@@ -0,0 +1,19 @@
+//import 'babel-core/polyfill';
+import angular from 'angular';
+import ngRedux from 'ng-redux';
+import thunk from 'redux-thunk';
+import createLogger from 'redux-logger';
+import rootReducer from './reducers';
+import asyncService from './actions/asyncService';
+import app from './containers/app';
+import picker from './components/picker';
+import posts from './components/posts';
+
+angular.module('async', [ngRedux])
+ .config(($ngReduxProvider) => {
+ $ngReduxProvider.createStoreWith(rootReducer, [thunk, createLogger()]);
+ })
+ .service('AsyncActions', asyncService)
+ .directive('ngrAsync', app)
+ .directive('ngrPicker', picker)
+ .directive('ngrPosts', posts);
diff --git a/examples/async-immutable/package.json b/examples/async-immutable/package.json
new file mode 100644
index 0000000..a521297
--- /dev/null
+++ b/examples/async-immutable/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "ng-redux-async-immutable-example",
+ "version": "0.0.0",
+ "description": "ng-redux async immutable example",
+ "scripts": {
+ "start": "webpack-dev-server --content-base dist/ --inline"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/angular-redux/ng-redux.git"
+ },
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/angular-redux/ng-redux/issues"
+ },
+ "homepage": "https://github.com/angular-redux/ng-redux#readme",
+ "dependencies": {
+ "angular": "^1.4.4",
+ "babel-core": "^6.8.0",
+ "babel-loader": "^6.2.4",
+ "babel-preset-es2015": "^6.6.0",
+ "immutable": "^3.8.1",
+ "ng-redux": "^3.0.0",
+ "redux": "^3.0.0",
+ "redux-logger": "^2.0.2",
+ "redux-thunk": "^1.0.0",
+ "reselect": "^3.0.1"
+ },
+ "devDependencies": {
+ "babel-core": "^6.9.1",
+ "babel-loader": "^6.2.4",
+ "html-loader": "^0.3.0",
+ "html-webpack-plugin": "^2.30.1",
+ "webpack": "^1.13.1",
+ "webpack-dev-server": "^1.14.1"
+ }
+}
diff --git a/examples/async-immutable/reducers/index.js b/examples/async-immutable/reducers/index.js
new file mode 100644
index 0000000..5b69d3d
--- /dev/null
+++ b/examples/async-immutable/reducers/index.js
@@ -0,0 +1,60 @@
+import { combineReducers } from 'redux';
+import {
+ SELECT_REDDIT, INVALIDATE_REDDIT,
+ REQUEST_POSTS, RECEIVE_POSTS
+} from '../constants/ActionTypes';
+import { fromJS } from 'immutable'
+
+
+function selectedReddit(state = 'angularjs', action) {
+ switch (action.type) {
+ case SELECT_REDDIT:
+ return action.reddit;
+ default:
+ return state;
+ }
+}
+
+function posts(state = fromJS({
+ isFetching: false,
+ didInvalidate: false,
+ items: []
+}), action) {
+ switch (action.type) {
+ case INVALIDATE_REDDIT:
+ return state.set('didInvalidate', true);
+ case REQUEST_POSTS:
+ return state.mergeDeep(fromJS({
+ isFetching: true,
+ didInvalidate: false,
+ }));
+ case RECEIVE_POSTS:
+ var updatedState = state.mergeDeep(fromJS({
+ isFetching: false,
+ didInvalidate: false,
+ items: action.posts,
+ lastUpdated: action.receivedAt
+ }));
+ return updatedState;
+ default:
+ return state;
+ }
+}
+
+function postsByReddit(state = fromJS({}), action) {
+ switch (action.type) {
+ case INVALIDATE_REDDIT:
+ case RECEIVE_POSTS:
+ case REQUEST_POSTS:
+ return state.set(action.reddit, posts(state.get(action.reddit), action));
+ default:
+ return state;
+ }
+}
+
+const rootReducer = combineReducers({
+ postsByReddit,
+ selectedReddit
+});
+
+export default rootReducer;
diff --git a/examples/async-immutable/selectors/index.js b/examples/async-immutable/selectors/index.js
new file mode 100644
index 0000000..0f7324f
--- /dev/null
+++ b/examples/async-immutable/selectors/index.js
@@ -0,0 +1,26 @@
+import { createSelector } from 'reselect'
+
+function getPostsState(state) {
+ return state.postsByReddit.get(state.selectedReddit);
+}
+
+const getPostList = createSelector(
+ [getPostsState],
+ (postsState) => {
+ return postsState && postsState.get('items');
+ }
+);
+
+export const getPostsTojs = createSelector(
+ [getPostList],
+ (postList) => {
+ return (postList && postList.toJS()) || [];
+ }
+);
+
+export const getIsFetching = createSelector(
+ [getPostsState],
+ (postsState) => {
+ return postsState && postsState.get('isFetching')
+ }
+);
diff --git a/examples/async-immutable/store/configureStore.js b/examples/async-immutable/store/configureStore.js
new file mode 100644
index 0000000..bef5a6f
--- /dev/null
+++ b/examples/async-immutable/store/configureStore.js
@@ -0,0 +1,23 @@
+import { createStore, applyMiddleware } from 'redux';
+import thunkMiddleware from 'redux-thunk';
+import loggerMiddleware from 'redux-logger';
+import rootReducer from '../reducers';
+
+const createStoreWithMiddleware = applyMiddleware(
+ thunkMiddleware,
+ loggerMiddleware
+)(createStore);
+
+export default function configureStore(initialState) {
+ const store = createStoreWithMiddleware(rootReducer, initialState);
+
+ if (module.hot) {
+ // Enable Webpack hot module replacement for reducers
+ module.hot.accept('../reducers', () => {
+ const nextRootReducer = require('../reducers');
+ store.replaceReducer(nextRootReducer);
+ });
+ }
+
+ return store;
+}
diff --git a/examples/async-immutable/webpack.config.js b/examples/async-immutable/webpack.config.js
new file mode 100644
index 0000000..19355dc
--- /dev/null
+++ b/examples/async-immutable/webpack.config.js
@@ -0,0 +1,33 @@
+var path = require('path');
+var webpack = require('webpack');
+var HtmlWebpackPlugin = require('html-webpack-plugin');
+
+module.exports = {
+ devtool: 'cheap-module-eval-source-map',
+ entry: [
+ './index.js'
+ ],
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'bundle.js'
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'ngRedux Async',
+ template: './index.html',
+ inject: 'body'
+ }),
+ new webpack.NoErrorsPlugin()
+ ],
+ module: {
+ loaders: [{
+ test: /\.js$/,
+ loaders: ['babel'],
+ exclude: /node_modules/,
+ },
+ {
+ test: /\.html$/,
+ loader: 'html'
+ }]
+ }
+};