Skip to content

Commit e3167be

Browse files
committed
Added mapDispatchToScope + propertyKey
1 parent f1065ef commit e3167be

File tree

8 files changed

+205
-102
lines changed

8 files changed

+205
-102
lines changed

examples/counter/components/counter.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ export default function counter() {
1313

1414
class CounterController {
1515

16-
constructor($ngRedux) {
17-
$ngRedux.connect(state => ({counter: state.counter}), this);
18-
19-
let {increment, decrement, incrementIfOdd, incrementAsync} = bindActionCreators(CounterActions, $ngRedux.dispatch);
20-
this.increment = increment;
21-
this.decrement = decrement;
22-
this.incrementIfOdd = incrementIfOdd;
23-
this.incrementAsync = incrementAsync;
16+
constructor($ngRedux, $scope) {
17+
$ngRedux.connect($scope, this.mapStateToScope, CounterActions, 'vm');
2418
}
2519

20+
// Which part of the Redux global state does our component want to receive on $scope?
21+
mapStateToScope(state) {
22+
return {
23+
counter: state.counter
24+
};
25+
}
2626
}

examples/counter/devTools.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { persistState } from 'redux-devtools';
2+
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
3+
import React, { Component } from 'react';
4+
5+
angular.module('counter')
6+
.run(($ngRedux, $rootScope) => {
7+
React.render(
8+
<App store={ $ngRedux }/>,
9+
document.getElementById('devTools')
10+
);
11+
//Hack to reflect state changes when disabling/enabling actions via the monitor
12+
$ngRedux.subscribe(_ => {
13+
setTimeout($rootScope.$apply, 100);
14+
});
15+
});
16+
17+
18+
class App extends Component {
19+
render() {
20+
return (
21+
<div>
22+
<DebugPanel top right bottom>
23+
<DevTools store={ this.props.store } monitor = { LogMonitor } />
24+
</DebugPanel>
25+
</div>
26+
);
27+
}
28+
}
29+
30+

examples/counter/index.js

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,10 @@ import 'ng-redux';
33
import rootReducer from './reducers';
44
import thunk from 'redux-thunk';
55
import counter from './components/counter';
6-
import { devTools, persistState } from 'redux-devtools';
7-
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
8-
import React, { Component } from 'react';
6+
import { devTools } from 'redux-devtools';
97

108
angular.module('counter', ['ngRedux'])
119
.config(($ngReduxProvider) => {
1210
$ngReduxProvider.createStoreWith(rootReducer, [thunk], [devTools()]);
1311
})
14-
.directive('ngrCounter', counter)
15-
//------- DevTools specific code ----
16-
.run(($ngRedux, $rootScope) => {
17-
React.render(
18-
<App store={ $ngRedux }/>,
19-
document.getElementById('devTools')
20-
);
21-
//Hack to reflect state changes when disabling/enabling actions via the monitor
22-
$ngRedux.subscribe(_ => {
23-
setTimeout($rootScope.$apply, 100);
24-
});
25-
});
26-
27-
28-
class App extends Component {
29-
render() {
30-
return (
31-
<div>
32-
<DebugPanel top right bottom>
33-
<DevTools store={ this.props.store } monitor = { LogMonitor } />
34-
</DebugPanel>
35-
</div>
36-
);
37-
}
38-
}
39-
40-
12+
.directive('ngrCounter', counter);

examples/counter/webpack.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
55
module.exports = {
66
entry: [
77
'webpack/hot/dev-server',
8-
'./index.js'
8+
'./index.js',
9+
//Remove the following line to remove devTools
10+
'./devTools.js'
911
],
1012
output: {
1113
path: path.join(__dirname, 'dist'),

src/components/connector.js

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,64 @@
11
import shallowEqual from '../utils/shallowEqual';
2+
import wrapActionCreators from '../utils/wrapActionCreators';
23
import invariant from 'invariant';
34
import _ from 'lodash';
45

5-
export default function Connector(store, $injector) {
6-
return (selector, scope) => {
6+
export default function Connector(store) {
7+
return (scope, mapStateToScope, mapDispatchToScope = {}, propertyKey) => {
78

8-
invariant(scope && _.isFunction(scope.$on) && _.isFunction(scope.$destroy), 'The scope parameter passed to connect must be an instance of $scope.');
9+
invariant(
10+
scope && _.isFunction(scope.$on) && _.isFunction(scope.$destroy),
11+
'The scope parameter passed to connect must be an instance of $scope.'
12+
);
13+
invariant(
14+
_.isFunction(mapStateToScope),
15+
'mapStateToScope must be a Function. Instead received $s.', mapStateToScope
16+
);
17+
invariant(
18+
_.isPlainObject(mapDispatchToScope) || _.isFunction(mapDispatchToScope),
19+
'mapDispatchToScope must be a plain Object or a Function. Instead received $s.', mapDispatchToScope
20+
);
21+
22+
let slice = getStateSlice(store.getState(), mapStateToScope);
23+
let target = propertyKey ? scope[propertyKey] : scope;
24+
if(!target) {
25+
target = scope[propertyKey] = {};
26+
}
27+
28+
const finalMapDispatchToScope = _.isPlainObject(mapDispatchToScope) ?
29+
wrapActionCreators(mapDispatchToScope) :
30+
mapDispatchToScope;
931

1032
//Initial update
11-
let slice = getStateSlice(store.getState(), selector);
12-
_.assign(scope, slice);
33+
_.assign(target, slice, finalMapDispatchToScope(store.dispatch));
1334

14-
let unsubscribe = store.subscribe(() => {
15-
let nextSlice = getStateSlice(store.getState(), selector);
35+
subscribe(scope, store, () => {
36+
const nextSlice = getStateSlice(store.getState(), mapStateToScope);
1637
if (!shallowEqual(slice, nextSlice)) {
1738
slice = nextSlice;
18-
_.assign(scope, slice);
39+
_.assign(target, slice);
1940
}
2041
});
21-
22-
scope.$on('$destroy', () => {
23-
unsubscribe();
24-
});
42+
2543
}
2644
}
2745

28-
function getStateSlice(state, selector) {
29-
let slice = selector(state);
46+
function subscribe(scope, store, callback) {
47+
const unsubscribe = store.subscribe(callback);
48+
49+
scope.$on('$destroy', () => {
50+
unsubscribe();
51+
});
52+
}
53+
54+
function getStateSlice(state, mapStateToScope) {
55+
const slice = mapStateToScope(state);
56+
3057
invariant(
3158
_.isPlainObject(slice),
32-
'`selector` must return an object. Instead received %s.',
59+
'`mapStateToScope` must return an object. Instead received %s.',
3360
slice
34-
);
61+
);
62+
3563
return slice;
36-
}
64+
}

src/utils/wrapActionCreators.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { bindActionCreators } from 'redux';
2+
3+
export default function wrapActionCreators(actionCreators) {
4+
return dispatch => bindActionCreators(actionCreators, dispatch);
5+
}

test/components/connector.spec.js

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,92 @@
11
import expect from 'expect';
2-
import {createStore} from 'redux';
2+
import { createStore } from 'redux';
33
import Connector from '../../src/components/connector';
4+
import _ from 'lodash';
45

56
describe('Connector', () => {
6-
let store;
7-
let connect;
8-
let scopeStub;
9-
10-
beforeEach(() => {
11-
store = createStore((state, action) => ({
12-
foo: 'bar',
13-
baz: action.payload,
14-
anotherState: 12,
15-
childObject: {child: true}
16-
}));
17-
scopeStub = { $on: () => {}, $destroy: () => {}};
18-
connect = Connector(store);
19-
});
7+
let store;
8+
let connect;
9+
let scopeStub;
2010

21-
it('Should throw when not passed a $scope object as target', () => {
22-
expect(connect.bind(connect, () => ({}), () => {})).toThrow();
23-
expect(connect.bind(connect, () => ({}), 15)).toThrow();
24-
expect(connect.bind(connect, () => ({}), undefined)).toThrow();
25-
expect(connect.bind(connect, () => ({}), {})).toThrow();
11+
beforeEach(() => {
12+
store = createStore((state, action) => ({
13+
foo: 'bar',
14+
baz: action.payload
15+
}));
16+
scopeStub = {
17+
$on: () => { },
18+
$destroy: () => { }
19+
};
20+
connect = Connector(store);
21+
});
2622

27-
expect(connect.bind(connect, () => ({}), scopeStub)).toNotThrow();
28-
});
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();
2928

30-
it('Should throw when selector does not return a plain object as target', () => {
31-
expect(connect.bind(connect, state => state.foo, scopeStub)).toThrow();
29+
expect(connect.bind(connect, scopeStub, () => ({}))).toNotThrow();
3230
});
3331

34-
it('target should be extended with state once directly after creation', () => {
35-
connect(() => ({vm : {test: 1}}), scopeStub);
36-
expect(scopeStub.vm).toEqual({test: 1});
37-
});
32+
it('Should throw when selector does not return a plain object as target', () => {
33+
expect(connect.bind(connect, scopeStub, state => state.foo)).toThrow();
34+
});
3835

39-
it('Should update the target passed to connect when the store updates', () => {
40-
connect(state => state, scopeStub);
41-
store.dispatch({type: 'ACTION', payload: 0});
42-
expect(scopeStub.baz).toBe(0);
43-
store.dispatch({type: 'ACTION', payload: 1});
44-
expect(scopeStub.baz).toBe(1);
45-
});
36+
it('Should extend scope with selected state once directly after creation', () => {
37+
connect(
38+
scopeStub,
39+
() => ({
40+
vm: { test: 1 }
41+
}));
42+
43+
expect(scopeStub.vm).toEqual({ test: 1 });
44+
});
45+
46+
it('Should extend scope[propertyKey] if propertyKey is passed', () => {
47+
connect(
48+
scopeStub,
49+
() => ({ test: 1 }),
50+
() => { },
51+
'vm'
52+
);
53+
54+
expect(scopeStub.vm).toEqual({ test: 1 });
55+
});
56+
57+
it('Should update the scope passed to connect when the store updates', () => {
58+
connect(scopeStub, state => state);
59+
store.dispatch({ type: 'ACTION', payload: 0 });
60+
expect(scopeStub.baz).toBe(0);
61+
store.dispatch({ type: 'ACTION', payload: 1 });
62+
expect(scopeStub.baz).toBe(1);
63+
});
64+
65+
it('Should prevent unnecessary updates when state does not change (shallowly)', () => {
66+
connect(scopeStub, state => state);
67+
store.dispatch({ type: 'ACTION', payload: 5 });
68+
69+
expect(scopeStub.baz).toBe(5);
70+
71+
scopeStub.baz = 0;
72+
73+
//this should not replace our mutation, since the state didn't change
74+
store.dispatch({ type: 'ACTION', payload: 5 });
75+
76+
expect(scopeStub.baz).toBe(0);
77+
78+
});
79+
80+
it('Should extend scope with actionCreators', () => {
81+
connect(scopeStub, () => ({}), { ac1: () => { }, ac2: () => { } });
82+
expect(_.isFunction(scopeStub.ac1)).toBe(true);
83+
expect(_.isFunction(scopeStub.ac2)).toBe(true);
84+
});
85+
86+
it('Should provide dispatch to mapDispatchToScope when receiving a Function', () => {
87+
let receivedDispatch;
88+
connect(scopeStub, () => ({}), dispatch => { receivedDispatch = dispatch });
89+
expect(receivedDispatch).toBe(store.dispatch);
90+
});
4691

47-
//does that still makes sense?
48-
/*it('Should prevent unnecessary updates when state does not change (shallowly)', () => {
49-
let counter = 0;
50-
let callback = () => counter++;
51-
connect(state => ({baz: state.baz}), callback);
52-
store.dispatch({type: 'ACTION', payload: 0});
53-
store.dispatch({type: 'ACTION', payload: 0});
54-
store.dispatch({type: 'ACTION', payload: 1});
55-
expect(counter).toBe(3);
56-
});*/
57-
});
92+
});

test/utils/wrapActionCreators.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import expect from 'expect';
2+
import wrapActionCreators from '../../src/utils/wrapActionCreators';
3+
4+
describe('Utils', () => {
5+
describe('wrapActionCreators', () => {
6+
it('should return a function that wraps argument in a call to bindActionCreators', () => {
7+
8+
function dispatch(action) {
9+
return {
10+
dispatched: action
11+
};
12+
}
13+
14+
const actionResult = {an: 'action'};
15+
16+
const actionCreators = {
17+
action: () => actionResult
18+
};
19+
20+
const wrapped = wrapActionCreators(actionCreators);
21+
expect(wrapped).toBeA(Function);
22+
expect(() => wrapped(dispatch)).toNotThrow();
23+
expect(() => wrapped().action()).toThrow();
24+
25+
const bound = wrapped(dispatch);
26+
expect(bound.action).toNotThrow();
27+
expect(bound.action().dispatched).toBe(actionResult);
28+
29+
});
30+
});
31+
});

0 commit comments

Comments
 (0)