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' + }] + } +};