Skip to content

Breaking changes 2.0.0 #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 2, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,21 @@ angular.module('app', ['ngRedux'])

#### Usage

*Using controllerAs syntax*
```JS
import * as CounterActions from '../actions/counter';

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

$ngRedux.connect($scope, this.mapStateToScope, CounterActions);
let unsubscribe = $ngRedux.connect(this.mapStateToTarget, CounterActions)(this);
$scope.$on('$destroy', unsubscribe);
}

// Which part of the Redux global state does our component want to receive on $scope?
mapStateToScope(state) {
mapStateToTarget(state) {
return {
counter: state.counter
};
Expand Down Expand Up @@ -84,17 +86,29 @@ Creates the Redux store, and allow `connect()` to access it.
* [`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)


### `connect([scope], [mapStateToScope], [mapDispatchToScope])`
### `connect([scope], [mapStateToTarget], [mapDispatchToTarget])([target])`

Connects an Angular component to Redux.

#### Arguments
* [`scope`] \(*Object*): The `$scope` of your controller.
* [`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`.
* [`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.).
* [`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`.
* [`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.).

*You then need to invoke the function a second time, with `target` as parameter:*
* [`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.

e.g:
```JS
connect(this.mapState, this.mapDispatch)(this);
//Or
connect(this.mapState, this.mapDispatch)((selectedState, actions) => {/* ... */});
```


#### Remarks
* 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.
* 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.



### Store API
All of redux's store methods (i.e. `dispatch`, `subscribe` and `getState`) are exposed by $ngRedux and can be accessed directly. For example:
Expand Down
3 changes: 2 additions & 1 deletion examples/counter/components/counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default function counter() {
class CounterController {

constructor($ngRedux, $scope) {
$ngRedux.connect($scope, this.mapStateToScope, CounterActions, 'vm');
const unsubscribe = $ngRedux.connect(this.mapStateToScope, CounterActions)(this);
$scope.$on('$destroy', unsubscribe);
}

// Which part of the Redux global state does our component want to receive on $scope?
Expand Down
5 changes: 2 additions & 3 deletions examples/counter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@
"html-loader": "^0.3.0",
"html-webpack-plugin": "^1.6.1",
"react": "^0.13.3",
"redux-devtools": "^1.1.1",
"webpack": "^1.11.0",
"webpack-dev-server": "^1.10.1"
},
"dependencies": {
"angular": "^1.4.4",
"ng-redux": "^1.0.0-rc.2",
"redux": "^1.0.1",
"ng-redux": "2.0.0",
"redux": "^2.0.0",
"redux-thunk": "^0.1.0"
}
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ng-redux",
"version": "1.0.0-rc.4",
"version": "2.0.0",
"description": "Redux bindings for Angular.js",
"main": "./lib/index.js",
"scripts": {
Expand All @@ -23,14 +23,15 @@
"babel-loader": "^5.3.2",
"expect": "^1.8.0",
"mocha": "^2.2.5",
"sinon": "^1.16.1",
"webpack": "^1.10.5"
},
"peerDependencies": {
"redux": "^1.0.0"
"redux": "^2.0.0"
},
"dependencies": {
"invariant": "^2.1.0",
"lodash": "^3.10.1",
"redux": "^1.0.1"
"redux": "^2.0.0"
}
}
72 changes: 41 additions & 31 deletions src/components/connector.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,63 @@
import shallowEqual from '../utils/shallowEqual';
import wrapActionCreators from '../utils/wrapActionCreators';
import findControllerAsKey from '../utils/findControllerAsKey';
import invariant from 'invariant';
import _ from 'lodash';

const defaultMapStateToTarget = () => ({});
const defaultMapDispatchToTarget = dispatch => ({dispatch});

export default function Connector(store) {
return (scope, mapStateToScope, mapDispatchToScope = {}) => {
return (mapStateToTarget, mapDispatchToTarget) => {

const finalMapStateToTarget = mapStateToTarget || defaultMapStateToTarget;

const finalMapDispatchToTarget = _.isPlainObject(mapDispatchToTarget) ?
wrapActionCreators(mapDispatchToTarget) :
mapDispatchToTarget || defaultMapDispatchToTarget;

invariant(
scope && _.isFunction(scope.$on) && _.isFunction(scope.$destroy),
'The scope parameter passed to connect must be an instance of $scope.'
);
invariant(
_.isFunction(mapStateToScope),
'mapStateToScope must be a Function. Instead received $s.', mapStateToScope
_.isFunction(finalMapStateToTarget),
'mapStateToTarget must be a Function. Instead received $s.', finalMapStateToTarget
);

invariant(
_.isPlainObject(mapDispatchToScope) || _.isFunction(mapDispatchToScope),
'mapDispatchToScope must be a plain Object or a Function. Instead received $s.', mapDispatchToScope
_.isPlainObject(finalMapDispatchToTarget) || _.isFunction(finalMapDispatchToTarget),
'mapDispatchToTarget must be a plain Object or a Function. Instead received $s.', finalMapDispatchToTarget
);

const propertyKey = findControllerAsKey(scope);
let slice = getStateSlice(store.getState(), finalMapStateToTarget);

const boundActionCreators = finalMapDispatchToTarget(store.dispatch);

return (target) => {

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

const finalMapDispatchToScope = _.isPlainObject(mapDispatchToScope) ?
wrapActionCreators(mapDispatchToScope) :
mapDispatchToScope;
//Initial update
updateTarget(target, slice, boundActionCreators);

//Initial update
_.assign(target, slice, finalMapDispatchToScope(store.dispatch));
const unsubscribe = store.subscribe(() => {
const nextSlice = getStateSlice(store.getState(), finalMapStateToTarget);
if (!shallowEqual(slice, nextSlice)) {
slice = nextSlice;
updateTarget(target, slice, boundActionCreators);
}
});
return unsubscribe;
}

subscribe(scope, store, () => {
const nextSlice = getStateSlice(store.getState(), mapStateToScope);
if (!shallowEqual(slice, nextSlice)) {
slice = nextSlice;
_.assign(target, slice);
}
});
}
}

function subscribe(scope, store, callback) {
const unsubscribe = store.subscribe(callback);

scope.$on('$destroy', () => {
unsubscribe();
});
function updateTarget(target, StateSlice, dispatch) {
if(_.isFunction(target)) {
target(StateSlice, dispatch);
} else {
_.assign(target, StateSlice, dispatch);
}
}

function getStateSlice(state, mapStateToScope) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ngRedux.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function ngReduxProvider() {
}
}

let finalCreateStore = _storeEnhancers ? compose(..._storeEnhancers, createStore) : createStore;
let finalCreateStore = _storeEnhancers ? compose(..._storeEnhancers)(createStore) : createStore;

//digestMiddleware needs to be the last one.
resolvedMiddleware.push(digestMiddleware($injector.get('$rootScope')));
Expand Down
11 changes: 0 additions & 11 deletions src/utils/findControllerAsKey.js

This file was deleted.

86 changes: 51 additions & 35 deletions test/components/connector.spec.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,96 @@
import expect from 'expect';
let sinon = require('sinon');
import { createStore } from 'redux';
import Connector from '../../src/components/connector';
import _ from 'lodash';

describe('Connector', () => {
let store;
let connect;
let scopeStub;
let targetObj;
let defaultState;

beforeEach(() => {
store = createStore((state, action) => ({
defaultState = {
foo: 'bar',
baz: action.payload
}));
scopeStub = {
$on: () => {},
$destroy: () => {}
baz: -1
};
store = createStore((state = defaultState, action) => {
return {...state, baz: action.payload};
});
targetObj = {};
connect = Connector(store);
});

it('Should throw when not passed a $scope object', () => {
expect(connect.bind(connect, () => { }, () => ({}))).toThrow();
expect(connect.bind(connect, 15, () => ({}))).toThrow();
expect(connect.bind(connect, undefined, () => ({}))).toThrow();
expect(connect.bind(connect, {}, () => ({}))).toThrow();
it('Should throw when target is not a Function or a plain object', () => {
expect(connect(() => ({})).bind(connect, 15)).toThrow();
expect(connect(() => ({})).bind(connect, undefined)).toThrow();
expect(connect(() => ({})).bind(connect, 'test')).toThrow();

expect(connect(() => ({})).bind(connect, {})).toNotThrow();
expect(connect(() => ({})).bind(connect, () => {})).toNotThrow();

expect(connect.bind(connect, scopeStub, () => ({}))).toNotThrow();
});

it('Should throw when selector does not return a plain object as target', () => {
expect(connect.bind(connect, scopeStub, state => state.foo)).toThrow();
it('Should throw when selector does not return a plain object', () => {
expect(connect.bind(connect, state => state.foo)).toThrow();
});

it('Should extend scope with selected state once directly after creation', () => {
connect(
scopeStub,
it('Should extend target (Object) with selected state once directly after creation', () => {
connect(
() => ({
vm: { test: 1 }
}));
}))(targetObj);

expect(scopeStub.vm).toEqual({ test: 1 });
expect(targetObj.vm).toEqual({ test: 1 });
});

it('Should update the scope passed to connect when the store updates', () => {
connect(scopeStub, state => state);
it('Should update the target (Object) passed to connect when the store updates', () => {
connect(state => state)(targetObj);
store.dispatch({ type: 'ACTION', payload: 0 });
expect(scopeStub.baz).toBe(0);
store.dispatch({ type: 'ACTION', payload: 1 });
expect(scopeStub.baz).toBe(1);
expect(targetObj.baz).toBe(0);
store.dispatch({ type: 'ACTION', payload: 7 });
expect(targetObj.baz).toBe(7);
});

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

expect(scopeStub.baz).toBe(5);
expect(targetObj.baz).toBe(5);

scopeStub.baz = 0;
targetObj.baz = 0;

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

expect(scopeStub.baz).toBe(0);
expect(targetObj.baz).toBe(0);

});

it('Should extend target (object) with actionCreators', () => {
connect(() => ({}), { ac1: () => { }, ac2: () => { } })(targetObj);
expect(_.isFunction(targetObj.ac1)).toBe(true);
expect(_.isFunction(targetObj.ac2)).toBe(true);
});

it('Should extend scope with actionCreators', () => {
connect(scopeStub, () => ({}), { ac1: () => { }, ac2: () => { } });
expect(_.isFunction(scopeStub.ac1)).toBe(true);
expect(_.isFunction(scopeStub.ac2)).toBe(true);
it('Should return an unsubscribing function', () => {
const unsubscribe = connect(state => state)(targetObj);
store.dispatch({ type: 'ACTION', payload: 5 });

expect(targetObj.baz).toBe(5);

unsubscribe();

store.dispatch({ type: 'ACTION', payload: 7 });

expect(targetObj.baz).toBe(5);

});

it('Should provide dispatch to mapDispatchToScope when receiving a Function', () => {
it('Should provide dispatch to mapDispatchToTarget when receiving a Function', () => {
let receivedDispatch;
connect(scopeStub, () => ({}), dispatch => { receivedDispatch = dispatch });
connect(() => ({}), dispatch => { receivedDispatch = dispatch })(targetObj);
expect(receivedDispatch).toBe(store.dispatch);
});

Expand Down
21 changes: 0 additions & 21 deletions test/utils/findControllerAs.spec.js

This file was deleted.