Skip to content

Commit b44079a

Browse files
author
Hang
committed
Example of using immutable.js and reselect
1. Duplicate the ‘async’ example and upgrade the html-webpack-plugin. 2. Refactor the code to use Immutablejs and reselect.
1 parent 3291e1b commit b44079a

File tree

16 files changed

+403
-0
lines changed

16 files changed

+403
-0
lines changed

examples/async-immutable/.babelrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["es2015"]
3+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as types from '../constants/ActionTypes';
2+
3+
function selectReddit(reddit) {
4+
return {
5+
type: types.SELECT_REDDIT,
6+
reddit
7+
};
8+
}
9+
10+
function invalidateReddit(reddit) {
11+
return {
12+
type: types.INVALIDATE_REDDIT,
13+
reddit
14+
};
15+
}
16+
17+
function requestPosts(reddit) {
18+
return {
19+
type: types.REQUEST_POSTS,
20+
reddit
21+
};
22+
}
23+
24+
function receivePosts(reddit, json) {
25+
return {
26+
type: types.RECEIVE_POSTS,
27+
reddit: reddit,
28+
posts: json.data.children.map(child => child.data),
29+
receivedAt: Date.now()
30+
};
31+
}
32+
33+
export default function asyncService($http) {
34+
function fetchPosts(reddit) {
35+
return dispatch => {
36+
dispatch(requestPosts(reddit));
37+
return $http.get(`http://www.reddit.com/r/${reddit}.json`)
38+
.then(response => response.data)
39+
.then(json => dispatch(receivePosts(reddit, json)));
40+
};
41+
}
42+
43+
function shouldFetchPosts(state, reddit) {
44+
const posts = state.postsByReddit.get(reddit);
45+
if (!posts) {
46+
return true;
47+
}
48+
if (posts.get('isFetching')) {
49+
return false;
50+
}
51+
return posts.get('didInvalidate');
52+
}
53+
54+
function fetchPostsIfNeeded(reddit) {
55+
return (dispatch, getState) => {
56+
if (shouldFetchPosts(getState(), reddit)) {
57+
return dispatch(fetchPosts(reddit));
58+
}
59+
};
60+
}
61+
62+
return {
63+
selectReddit,
64+
invalidateReddit,
65+
fetchPostsIfNeeded
66+
};
67+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<span>
2+
<h1>{{picker.value}}</h1>
3+
<select ng-options="option for option in picker.options"
4+
ng-model="picker.value"
5+
ng-change="picker.onChange(picker.value)">
6+
</select>
7+
</span>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export default function picker() {
2+
return {
3+
restrict: 'E',
4+
controllerAs: 'picker',
5+
controller: PickerController,
6+
template: require('./picker.html'),
7+
scope: {
8+
options: '=',
9+
value: '=',
10+
onChange: '='
11+
},
12+
bindToController: true
13+
};
14+
}
15+
16+
class PickerController {
17+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<ul>
2+
<li ng-repeat="post in posts.posts">{{post.title}}</li>
3+
</ul>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function posts() {
2+
return {
3+
restrict: 'E',
4+
controllerAs: 'posts',
5+
controller: PostsController,
6+
template: require('./posts.html'),
7+
scope: {
8+
posts: '=',
9+
},
10+
bindToController: true
11+
};
12+
}
13+
14+
class PostsController {
15+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const REQUEST_POSTS = 'REQUEST_POSTS';
2+
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
3+
export const SELECT_REDDIT = 'SELECT_REDDIT';
4+
export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<div>
2+
<ngr-picker value="app.selectedReddit"
3+
on-change="app.handleChange"
4+
options="app.options">
5+
</ngr-picker>
6+
<p>
7+
<span ng-show="app.lastUpdated">
8+
Last updated at {{ app.lastUpdated | date:'mediumTime' }}.
9+
</span>
10+
<a href="#"
11+
ng-show="!app.isFetching"
12+
ng-click="app.handleRefreshClick()">
13+
Refresh
14+
</a>
15+
</p>
16+
<h2 ng-show="app.isFetching && app.posts.length === 0">Loading...</h2>
17+
<h2 ng-show="!app.isFetching && app.posts.length === 0">Empty.</h2>
18+
<div ng-show="app.posts.length > 0"
19+
ng-style="{ opacity: app.isFetching ? 0.5 : 1 }">
20+
<ngr-posts posts="app.posts"></ngr-posts>
21+
</div>
22+
</div>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getPostsTojs, getIsFetching } from '../selectors'
2+
3+
export default function app() {
4+
return {
5+
restrict: 'E',
6+
controllerAs: 'app',
7+
controller: AppController,
8+
template: require('./app.html'),
9+
scope: {}
10+
};
11+
}
12+
13+
class AppController {
14+
15+
constructor($ngRedux, AsyncActions, $scope) {
16+
const unsubscribe = $ngRedux.connect(this.mapStateToThis, AsyncActions)((selectedState, actions) => {
17+
this.componentWillReceiveStateAndActions(selectedState, actions);
18+
Object.assign(this, selectedState, actions);
19+
});
20+
this.options = ['angularjs', 'frontend'];
21+
this.handleChange = this.handleChange.bind(this);
22+
this.handleRefreshClick = this.handleRefreshClick.bind(this);
23+
24+
this.fetchPostsIfNeeded(this.selectedReddit);
25+
$scope.$on('$destroy', unsubscribe);
26+
}
27+
28+
componentWillReceiveStateAndActions(nextState, nextActions) {
29+
if (nextState.selectedReddit !== this.selectedReddit) {
30+
nextActions.fetchPostsIfNeeded(nextState.selectedReddit);
31+
}
32+
}
33+
34+
handleChange(nextReddit) {
35+
this.selectReddit(nextReddit);
36+
}
37+
38+
handleRefreshClick() {
39+
this.invalidateReddit(this.selectedReddit);
40+
this.fetchPostsIfNeeded(this.selectedReddit);
41+
}
42+
43+
mapStateToThis(state) {
44+
const { selectedReddit, postsByReddit } = state;
45+
46+
return {
47+
selectedReddit,
48+
// Use selectors here
49+
posts: getPostsTojs(state),
50+
isFetching: getIsFetching(state),
51+
// Or get value from the state directly without selectors
52+
lastUpdated: postsByReddit.get(selectedReddit) && postsByReddit.get(selectedReddit).get('lastUpdated')
53+
};
54+
}
55+
}

examples/async-immutable/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
5+
<title>{%= o.htmlWebpackPlugin.options.title %}</title>
6+
</head>
7+
<body>
8+
<div ng-app="async">
9+
<ngr-async></ngr-async>
10+
</div>
11+
</body>
12+
</html>

examples/async-immutable/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//import 'babel-core/polyfill';
2+
import angular from 'angular';
3+
import ngRedux from 'ng-redux';
4+
import thunk from 'redux-thunk';
5+
import createLogger from 'redux-logger';
6+
import rootReducer from './reducers';
7+
import asyncService from './actions/asyncService';
8+
import app from './containers/app';
9+
import picker from './components/picker';
10+
import posts from './components/posts';
11+
12+
angular.module('async', [ngRedux])
13+
.config(($ngReduxProvider) => {
14+
$ngReduxProvider.createStoreWith(rootReducer, [thunk, createLogger()]);
15+
})
16+
.service('AsyncActions', asyncService)
17+
.directive('ngrAsync', app)
18+
.directive('ngrPicker', picker)
19+
.directive('ngrPosts', posts);

examples/async-immutable/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "ng-redux-async-immutable-example",
3+
"version": "0.0.0",
4+
"description": "ng-redux async immutable example",
5+
"scripts": {
6+
"start": "webpack-dev-server --content-base dist/ --inline"
7+
},
8+
"repository": {
9+
"type": "git",
10+
"url": "https://github.com/angular-redux/ng-redux.git"
11+
},
12+
"license": "MIT",
13+
"bugs": {
14+
"url": "https://github.com/angular-redux/ng-redux/issues"
15+
},
16+
"homepage": "https://github.com/angular-redux/ng-redux#readme",
17+
"dependencies": {
18+
"angular": "^1.4.4",
19+
"babel-core": "^6.8.0",
20+
"babel-loader": "^6.2.4",
21+
"babel-preset-es2015": "^6.6.0",
22+
"immutable": "^3.8.1",
23+
"ng-redux": "^3.0.0",
24+
"redux": "^3.0.0",
25+
"redux-logger": "^2.0.2",
26+
"redux-thunk": "^1.0.0",
27+
"reselect": "^3.0.1"
28+
},
29+
"devDependencies": {
30+
"babel-core": "^6.9.1",
31+
"babel-loader": "^6.2.4",
32+
"html-loader": "^0.3.0",
33+
"html-webpack-plugin": "^2.30.1",
34+
"webpack": "^1.13.1",
35+
"webpack-dev-server": "^1.14.1"
36+
}
37+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { combineReducers } from 'redux';
2+
import {
3+
SELECT_REDDIT, INVALIDATE_REDDIT,
4+
REQUEST_POSTS, RECEIVE_POSTS
5+
} from '../constants/ActionTypes';
6+
import { fromJS } from 'immutable'
7+
8+
9+
function selectedReddit(state = 'angularjs', action) {
10+
switch (action.type) {
11+
case SELECT_REDDIT:
12+
return action.reddit;
13+
default:
14+
return state;
15+
}
16+
}
17+
18+
function posts(state = fromJS({
19+
isFetching: false,
20+
didInvalidate: false,
21+
items: []
22+
}), action) {
23+
switch (action.type) {
24+
case INVALIDATE_REDDIT:
25+
return state.set('didInvalidate', true);
26+
case REQUEST_POSTS:
27+
return state.mergeDeep(fromJS({
28+
isFetching: true,
29+
didInvalidate: false,
30+
}));
31+
case RECEIVE_POSTS:
32+
var updatedState = state.mergeDeep(fromJS({
33+
isFetching: false,
34+
didInvalidate: false,
35+
items: action.posts,
36+
lastUpdated: action.receivedAt
37+
}));
38+
return updatedState;
39+
default:
40+
return state;
41+
}
42+
}
43+
44+
function postsByReddit(state = fromJS({}), action) {
45+
switch (action.type) {
46+
case INVALIDATE_REDDIT:
47+
case RECEIVE_POSTS:
48+
case REQUEST_POSTS:
49+
return state.set(action.reddit, posts(state.get(action.reddit), action));
50+
default:
51+
return state;
52+
}
53+
}
54+
55+
const rootReducer = combineReducers({
56+
postsByReddit,
57+
selectedReddit
58+
});
59+
60+
export default rootReducer;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createSelector } from 'reselect'
2+
3+
function getPostsState(state) {
4+
return state.postsByReddit.get(state.selectedReddit);
5+
}
6+
7+
const getPostList = createSelector(
8+
[getPostsState],
9+
(postsState) => {
10+
return postsState && postsState.get('items');
11+
}
12+
);
13+
14+
export const getPostsTojs = createSelector(
15+
[getPostList],
16+
(postList) => {
17+
return (postList && postList.toJS()) || [];
18+
}
19+
);
20+
21+
export const getIsFetching = createSelector(
22+
[getPostsState],
23+
(postsState) => {
24+
return postsState && postsState.get('isFetching')
25+
}
26+
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createStore, applyMiddleware } from 'redux';
2+
import thunkMiddleware from 'redux-thunk';
3+
import loggerMiddleware from 'redux-logger';
4+
import rootReducer from '../reducers';
5+
6+
const createStoreWithMiddleware = applyMiddleware(
7+
thunkMiddleware,
8+
loggerMiddleware
9+
)(createStore);
10+
11+
export default function configureStore(initialState) {
12+
const store = createStoreWithMiddleware(rootReducer, initialState);
13+
14+
if (module.hot) {
15+
// Enable Webpack hot module replacement for reducers
16+
module.hot.accept('../reducers', () => {
17+
const nextRootReducer = require('../reducers');
18+
store.replaceReducer(nextRootReducer);
19+
});
20+
}
21+
22+
return store;
23+
}

0 commit comments

Comments
 (0)