Skip to content

Commit 2c7b197

Browse files
committed
Merge pull request #17 from wbuchwalter/bc-2.0.0
Breaking changes 2.0.0
2 parents b088352 + 8912e7d commit 2c7b197

File tree

9 files changed

+123
-114
lines changed

9 files changed

+123
-114
lines changed

README.md

+22-8
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,21 @@ angular.module('app', ['ngRedux'])
4242

4343
#### Usage
4444

45+
*Using controllerAs syntax*
4546
```JS
4647
import * as CounterActions from '../actions/counter';
4748

4849
class CounterController {
4950
constructor($ngRedux, $scope) {
50-
/* ngRedux will merge the requested state's slice and actions onto the $scope,
51+
/* ngRedux will merge the requested state's slice and actions onto this,
5152
you don't need to redefine them in your controller */
5253

53-
$ngRedux.connect($scope, this.mapStateToScope, CounterActions);
54+
let unsubscribe = $ngRedux.connect(this.mapStateToTarget, CounterActions)(this);
55+
$scope.$on('$destroy', unsubscribe);
5456
}
5557

5658
// Which part of the Redux global state does our component want to receive on $scope?
57-
mapStateToScope(state) {
59+
mapStateToTarget(state) {
5860
return {
5961
counter: state.counter
6062
};
@@ -84,17 +86,29 @@ Creates the Redux store, and allow `connect()` to access it.
8486
* [`storeEnhancers`] \(*Function[]*): Optional, this will be used to create the store, in most cases you don't need to pass anything, see [Store Enhancer official documentation.](http://rackt.github.io/redux/docs/Glossary.html#store-enhancer)
8587

8688

87-
### `connect([scope], [mapStateToScope], [mapDispatchToScope])`
89+
### `connect([scope], [mapStateToTarget], [mapDispatchToTarget])([target])`
8890

8991
Connects an Angular component to Redux.
9092

9193
#### Arguments
92-
* [`scope`] \(*Object*): The `$scope` of your controller.
93-
* [`mapStateToScope`] \(*Function*): connect will subscribe to Redux store updates. Any time it updates, mapStateToTarget will be called. Its result must be a plain object, and it will be merged into `target`.
94-
* [`mapDispatchToScope`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged into your component `$scope`. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use the [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.).
94+
* [`mapStateToTarget`] \(*Function*): connect will subscribe to Redux store updates. Any time it updates, mapStateToTarget will be called. Its result must be a plain object, and it will be merged into `target`.
95+
* [`mapDispatchToTarget`] \(*Object* or *Function*): Optional. If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged onto `target`. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use the [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.).
96+
97+
*You then need to invoke the function a second time, with `target` as parameter:*
98+
* [`target`] \(*Object* or *Function*): If passed an object, the results of `mapStateToTarget` and `mapDispatchToTarget` will be merged onto it. If passed a function, the function will receive the results of `mapStateToTarget` and `mapDispatchToTarget` as parameters.
99+
100+
e.g:
101+
```JS
102+
connect(this.mapState, this.mapDispatch)(this);
103+
//Or
104+
connect(this.mapState, this.mapDispatch)((selectedState, actions) => {/* ... */});
105+
```
106+
95107

96108
#### Remarks
97-
* As `$scope` is passed to `connect`, ngRedux will listen to the `$destroy` event and unsubscribe the change listener itself, you don't need to keep track of your subscribtions.
109+
* The `mapStateToTarget` function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. It is often called a selector. Use reselect to efficiently compose selectors and compute derived data.
110+
111+
98112

99113
### Store API
100114
All of redux's store methods (i.e. `dispatch`, `subscribe` and `getState`) are exposed by $ngRedux and can be accessed directly. For example:

examples/counter/components/counter.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export default function counter() {
1313
class CounterController {
1414

1515
constructor($ngRedux, $scope) {
16-
$ngRedux.connect($scope, this.mapStateToScope, CounterActions, 'vm');
16+
const unsubscribe = $ngRedux.connect(this.mapStateToScope, CounterActions)(this);
17+
$scope.$on('$destroy', unsubscribe);
1718
}
1819

1920
// Which part of the Redux global state does our component want to receive on $scope?

examples/counter/package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@
2222
"html-loader": "^0.3.0",
2323
"html-webpack-plugin": "^1.6.1",
2424
"react": "^0.13.3",
25-
"redux-devtools": "^1.1.1",
2625
"webpack": "^1.11.0",
2726
"webpack-dev-server": "^1.10.1"
2827
},
2928
"dependencies": {
3029
"angular": "^1.4.4",
31-
"ng-redux": "^1.0.0-rc.2",
32-
"redux": "^1.0.1",
30+
"ng-redux": "2.0.0",
31+
"redux": "^2.0.0",
3332
"redux-thunk": "^0.1.0"
3433
}
3534
}

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ng-redux",
3-
"version": "1.0.0-rc.4",
3+
"version": "2.0.0",
44
"description": "Redux bindings for Angular.js",
55
"main": "./lib/index.js",
66
"scripts": {
@@ -23,14 +23,15 @@
2323
"babel-loader": "^5.3.2",
2424
"expect": "^1.8.0",
2525
"mocha": "^2.2.5",
26+
"sinon": "^1.16.1",
2627
"webpack": "^1.10.5"
2728
},
2829
"peerDependencies": {
29-
"redux": "^1.0.0"
30+
"redux": "^2.0.0"
3031
},
3132
"dependencies": {
3233
"invariant": "^2.1.0",
3334
"lodash": "^3.10.1",
34-
"redux": "^1.0.1"
35+
"redux": "^2.0.0"
3536
}
3637
}

src/components/connector.js

+41-31
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,63 @@
11
import shallowEqual from '../utils/shallowEqual';
22
import wrapActionCreators from '../utils/wrapActionCreators';
3-
import findControllerAsKey from '../utils/findControllerAsKey';
43
import invariant from 'invariant';
54
import _ from 'lodash';
65

6+
const defaultMapStateToTarget = () => ({});
7+
const defaultMapDispatchToTarget = dispatch => ({dispatch});
8+
79
export default function Connector(store) {
8-
return (scope, mapStateToScope, mapDispatchToScope = {}) => {
10+
return (mapStateToTarget, mapDispatchToTarget) => {
11+
12+
const finalMapStateToTarget = mapStateToTarget || defaultMapStateToTarget;
13+
14+
const finalMapDispatchToTarget = _.isPlainObject(mapDispatchToTarget) ?
15+
wrapActionCreators(mapDispatchToTarget) :
16+
mapDispatchToTarget || defaultMapDispatchToTarget;
917

1018
invariant(
11-
scope && _.isFunction(scope.$on) && _.isFunction(scope.$destroy),
12-
'The scope parameter passed to connect must be an instance of $scope.'
13-
);
14-
invariant(
15-
_.isFunction(mapStateToScope),
16-
'mapStateToScope must be a Function. Instead received $s.', mapStateToScope
19+
_.isFunction(finalMapStateToTarget),
20+
'mapStateToTarget must be a Function. Instead received $s.', finalMapStateToTarget
1721
);
22+
1823
invariant(
19-
_.isPlainObject(mapDispatchToScope) || _.isFunction(mapDispatchToScope),
20-
'mapDispatchToScope must be a plain Object or a Function. Instead received $s.', mapDispatchToScope
24+
_.isPlainObject(finalMapDispatchToTarget) || _.isFunction(finalMapDispatchToTarget),
25+
'mapDispatchToTarget must be a plain Object or a Function. Instead received $s.', finalMapDispatchToTarget
2126
);
2227

23-
const propertyKey = findControllerAsKey(scope);
28+
let slice = getStateSlice(store.getState(), finalMapStateToTarget);
29+
30+
const boundActionCreators = finalMapDispatchToTarget(store.dispatch);
31+
32+
return (target) => {
2433

25-
let slice = getStateSlice(store.getState(), mapStateToScope);
26-
let target = propertyKey ? scope[propertyKey] : scope;
34+
invariant(
35+
_.isFunction(target) || _.isObject(target),
36+
'The target parameter passed to connect must be a Function or a plain object.'
37+
);
2738

28-
const finalMapDispatchToScope = _.isPlainObject(mapDispatchToScope) ?
29-
wrapActionCreators(mapDispatchToScope) :
30-
mapDispatchToScope;
39+
//Initial update
40+
updateTarget(target, slice, boundActionCreators);
3141

32-
//Initial update
33-
_.assign(target, slice, finalMapDispatchToScope(store.dispatch));
42+
const unsubscribe = store.subscribe(() => {
43+
const nextSlice = getStateSlice(store.getState(), finalMapStateToTarget);
44+
if (!shallowEqual(slice, nextSlice)) {
45+
slice = nextSlice;
46+
updateTarget(target, slice, boundActionCreators);
47+
}
48+
});
49+
return unsubscribe;
50+
}
3451

35-
subscribe(scope, store, () => {
36-
const nextSlice = getStateSlice(store.getState(), mapStateToScope);
37-
if (!shallowEqual(slice, nextSlice)) {
38-
slice = nextSlice;
39-
_.assign(target, slice);
40-
}
41-
});
4252
}
4353
}
4454

45-
function subscribe(scope, store, callback) {
46-
const unsubscribe = store.subscribe(callback);
47-
48-
scope.$on('$destroy', () => {
49-
unsubscribe();
50-
});
55+
function updateTarget(target, StateSlice, dispatch) {
56+
if(_.isFunction(target)) {
57+
target(StateSlice, dispatch);
58+
} else {
59+
_.assign(target, StateSlice, dispatch);
60+
}
5161
}
5262

5363
function getStateSlice(state, mapStateToScope) {

src/components/ngRedux.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function ngReduxProvider() {
3838
}
3939
}
4040

41-
let finalCreateStore = _storeEnhancers ? compose(..._storeEnhancers, createStore) : createStore;
41+
let finalCreateStore = _storeEnhancers ? compose(..._storeEnhancers)(createStore) : createStore;
4242

4343
//digestMiddleware needs to be the last one.
4444
resolvedMiddleware.push(digestMiddleware($injector.get('$rootScope')));

src/utils/findControllerAsKey.js

-11
This file was deleted.

test/components/connector.spec.js

+51-35
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,96 @@
11
import expect from 'expect';
2+
let sinon = require('sinon');
23
import { createStore } from 'redux';
34
import Connector from '../../src/components/connector';
45
import _ from 'lodash';
56

67
describe('Connector', () => {
78
let store;
89
let connect;
9-
let scopeStub;
10+
let targetObj;
11+
let defaultState;
1012

1113
beforeEach(() => {
12-
store = createStore((state, action) => ({
14+
defaultState = {
1315
foo: 'bar',
14-
baz: action.payload
15-
}));
16-
scopeStub = {
17-
$on: () => {},
18-
$destroy: () => {}
16+
baz: -1
1917
};
18+
store = createStore((state = defaultState, action) => {
19+
return {...state, baz: action.payload};
20+
});
21+
targetObj = {};
2022
connect = Connector(store);
2123
});
2224

23-
it('Should throw when not passed a $scope object', () => {
24-
expect(connect.bind(connect, () => { }, () => ({}))).toThrow();
25-
expect(connect.bind(connect, 15, () => ({}))).toThrow();
26-
expect(connect.bind(connect, undefined, () => ({}))).toThrow();
27-
expect(connect.bind(connect, {}, () => ({}))).toThrow();
25+
it('Should throw when target is not a Function or a plain object', () => {
26+
expect(connect(() => ({})).bind(connect, 15)).toThrow();
27+
expect(connect(() => ({})).bind(connect, undefined)).toThrow();
28+
expect(connect(() => ({})).bind(connect, 'test')).toThrow();
29+
30+
expect(connect(() => ({})).bind(connect, {})).toNotThrow();
31+
expect(connect(() => ({})).bind(connect, () => {})).toNotThrow();
2832

29-
expect(connect.bind(connect, scopeStub, () => ({}))).toNotThrow();
3033
});
3134

32-
it('Should throw when selector does not return a plain object as target', () => {
33-
expect(connect.bind(connect, scopeStub, state => state.foo)).toThrow();
35+
it('Should throw when selector does not return a plain object', () => {
36+
expect(connect.bind(connect, state => state.foo)).toThrow();
3437
});
3538

36-
it('Should extend scope with selected state once directly after creation', () => {
37-
connect(
38-
scopeStub,
39+
it('Should extend target (Object) with selected state once directly after creation', () => {
40+
connect(
3941
() => ({
4042
vm: { test: 1 }
41-
}));
43+
}))(targetObj);
4244

43-
expect(scopeStub.vm).toEqual({ test: 1 });
45+
expect(targetObj.vm).toEqual({ test: 1 });
4446
});
4547

46-
it('Should update the scope passed to connect when the store updates', () => {
47-
connect(scopeStub, state => state);
48+
it('Should update the target (Object) passed to connect when the store updates', () => {
49+
connect(state => state)(targetObj);
4850
store.dispatch({ type: 'ACTION', payload: 0 });
49-
expect(scopeStub.baz).toBe(0);
50-
store.dispatch({ type: 'ACTION', payload: 1 });
51-
expect(scopeStub.baz).toBe(1);
51+
expect(targetObj.baz).toBe(0);
52+
store.dispatch({ type: 'ACTION', payload: 7 });
53+
expect(targetObj.baz).toBe(7);
5254
});
5355

5456
it('Should prevent unnecessary updates when state does not change (shallowly)', () => {
55-
connect(scopeStub, state => state);
57+
connect(state => state)(targetObj);
5658
store.dispatch({ type: 'ACTION', payload: 5 });
5759

58-
expect(scopeStub.baz).toBe(5);
60+
expect(targetObj.baz).toBe(5);
5961

60-
scopeStub.baz = 0;
62+
targetObj.baz = 0;
6163

6264
//this should not replace our mutation, since the state didn't change
6365
store.dispatch({ type: 'ACTION', payload: 5 });
6466

65-
expect(scopeStub.baz).toBe(0);
67+
expect(targetObj.baz).toBe(0);
68+
69+
});
6670

71+
it('Should extend target (object) with actionCreators', () => {
72+
connect(() => ({}), { ac1: () => { }, ac2: () => { } })(targetObj);
73+
expect(_.isFunction(targetObj.ac1)).toBe(true);
74+
expect(_.isFunction(targetObj.ac2)).toBe(true);
6775
});
6876

69-
it('Should extend scope with actionCreators', () => {
70-
connect(scopeStub, () => ({}), { ac1: () => { }, ac2: () => { } });
71-
expect(_.isFunction(scopeStub.ac1)).toBe(true);
72-
expect(_.isFunction(scopeStub.ac2)).toBe(true);
77+
it('Should return an unsubscribing function', () => {
78+
const unsubscribe = connect(state => state)(targetObj);
79+
store.dispatch({ type: 'ACTION', payload: 5 });
80+
81+
expect(targetObj.baz).toBe(5);
82+
83+
unsubscribe();
84+
85+
store.dispatch({ type: 'ACTION', payload: 7 });
86+
87+
expect(targetObj.baz).toBe(5);
88+
7389
});
7490

75-
it('Should provide dispatch to mapDispatchToScope when receiving a Function', () => {
91+
it('Should provide dispatch to mapDispatchToTarget when receiving a Function', () => {
7692
let receivedDispatch;
77-
connect(scopeStub, () => ({}), dispatch => { receivedDispatch = dispatch });
93+
connect(() => ({}), dispatch => { receivedDispatch = dispatch })(targetObj);
7894
expect(receivedDispatch).toBe(store.dispatch);
7995
});
8096

test/utils/findControllerAs.spec.js

-21
This file was deleted.

0 commit comments

Comments
 (0)