Skip to content
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
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ require('ng-redux');
angular.module('app', ['ngRedux'])
.config(($ngReduxProvider) => {
let reducer = redux.combineReducers(reducers);
let store = redux.createStore(reducer);
let store = redux.createStore(reducer);
$ngReduxProvider.setReduxStore(store);
});
```
```

### Usage
```JS
Expand All @@ -48,18 +48,18 @@ angular.module('app', ['ngRedux'])
controllerAs: 'vm',
controller: TodoLoaderController,
template: "<div ng-repeat='todo in vm.todos'>{{todo.text}}</div>",

[...]
};
}
class TodoLoaderController {

class TodoLoaderController {

constructor(reduxConnector) {
this.todos = [];
reduxConnector.connect(state => state.todos, todos => this.todos = todos);
}

[...]
}
```
Expand Down Expand Up @@ -92,7 +92,7 @@ constructor(reduxConnector) {
this.disconnectTodos = reduxConnector.connect(state => state.todos, todos => this.todos = todos);
reduxConnector.connect(state => state.users, users => this.users = users);
}

disconnectSome() {
this.disconnectTodos();
}
Expand All @@ -104,7 +104,7 @@ disconnectSome() {
You don't need to create another service to get hold of Redux's store (although you can).
You can access the store via ```$ngRedux.getStore()```:

```JS
```JS
redux.bindActionCreators(actionCreator, $ngRedux.getStore().dispatch);
```

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ng-redux",
"version": "0.2.1",
"version": "0.3.0",
"description": "Redux bindings for Angular.js",
"main": "./lib/index.js",
"scripts": {
Expand Down
87 changes: 29 additions & 58 deletions src/components/connector.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,35 @@
import shallowEqual from '../utils/shallowEqual';
import isFunction from '../utils/isFunction';
import isPlainObject from '../utils/isPlainObject';
import invariant from 'invariant';

export default function connectorFactory($ngRedux) {
return {
connect: (select, target) => {
let connector = new Connector($ngRedux, select, target);
return connector.unsubscribe;
export default function Connector(store){
return {
connect: (selectors, callback) => {
if (!Array.isArray(selectors)) {
selectors = [selectors];
}

invariant(
isFunction(callback),
'The callback parameter passed to connect must be a Function. Instead received %s.',
typeof selector
);

//Initial update
let params = selectors.map(selector => selector(store.getState()));
callback(...params);

let unsubscribe = store.subscribe(() => {
let nextParams = selectors.map(selector => selector(store.getState()));
if(params === null || params.some((param, index) => param !== nextParams[index])) {
callback(...nextParams);
params = nextParams.slice(0);
}
});

return unsubscribe;
},
getStore() {
return store;
}
}
}

export class Connector {
constructor($ngRedux, selector, callback){

invariant(
isFunction(selector),
'The selector passed to connect must be a function. Instead received %s.',
typeof selector
);

invariant(
isFunction(callback),
'The callback passed to connect must be a function. Instead received %s.',
typeof callback
);

this.select = selector;
this.callback = callback;
this.reduxStore = $ngRedux.getStore();
this._sliceState = this.selectState(this.reduxStore.getState(), this.select);
//force a first update to initialize subscribing component
this.updateTarget(this.callback, this._sliceState);
this.unsubscribe = this.reduxStore.subscribe(this.onStoreChanged.bind(this));
}

onStoreChanged() {
let nextState = this.selectState(this.reduxStore.getState(), this.select);
if (!this.isSliceEqual(this._sliceState, nextState)) {
this.updateTarget(this.callback, nextState)
this._sliceState = {...nextState};
}
}

updateTarget(target, state){
target(state)
}

selectState(state, selector) {
let slice = selector(state);

return slice;
}

isSliceEqual(slice, nextSlice) {
const isRefEqual = slice === nextSlice;
if (isRefEqual || typeof slice !== 'object' || typeof nextSlice !== 'object') {
return isRefEqual;
}
return shallowEqual(slice, nextSlice);
}
}
11 changes: 11 additions & 0 deletions src/components/ngRedux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Connector from './connector';

export default function ngReduxProvider() {
let reduxStore = undefined;
this.setReduxStore = store => reduxStore = store;

this.$get = () => {
return Connector(reduxStore);
}
}

15 changes: 0 additions & 15 deletions src/components/provider.js

This file was deleted.

4 changes: 1 addition & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import connectorFactory from './components/connector';
import ngReduxProvider from './components/provider';
import ngReduxProvider from './components/ngRedux';

export default angular.module('ngRedux', [])
.provider('$ngRedux', ngReduxProvider)
.factory('reduxConnector', ['$ngRedux', '$rootScope', connectorFactory])
.name;
11 changes: 11 additions & 0 deletions src/utils/hashCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function hashCode(str){
var hash = 0;
if (str.length == 0) return hash;
for (i = 0; i < str.length; i++) {
var chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash = hash & hash; // Convert to 32bit integer
}

return hash;
}
3 changes: 1 addition & 2 deletions src/utils/shallowEqual.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,4 @@
}

return true;
}

}
81 changes: 30 additions & 51 deletions test/components/connector.spec.js
Original file line number Diff line number Diff line change
@@ -1,100 +1,79 @@
import expect from 'expect';
import {createStore} from 'redux';
import {Connector, default as connectorFactory} from '../../src/components/connector';
import {default as Connector, copyArray} from '../../src/components/connector';

describe('Connector', () => {
let store;
let ngRedux;
let connector;
beforeEach(() => {
store = createStore((state, action) => {
return {foo: 'bar', baz: action.payload};
return {foo: 'bar', baz: action.payload, anotherState: 12};
});
ngRedux = {
getStore: () => store
};
connector = Connector(store);
});

it('Should throw when not passed a function as callback', () => {
expect(() => new Connector(ngRedux, state => state, {})).toThrow();
});

it('Should throw when not passed a function as selector', () => {
expect(() => new Connector(ngRedux, {}, () => {})).toThrow();
expect(connector.connect.bind(connector, () => {}, undefined)).toThrow();
expect(connector.connect.bind(connector, () => {}, {})).toThrow();
expect(connector.connect.bind(connector, () => {}, 15)).toThrow();
});

it('Callback should be called once directly after creation to allow initialization', () => {
let counter = 0;
let callback = () => counter++;
let connector = new Connector(ngRedux, state => state, callback);
connector.connect(state => state, callback);
expect(counter).toBe(1);
});

it('Should call the function passed to connect when the store updates', () => {
let counter = 0;
let callback = () => counter++;
let connector = new Connector(ngRedux, state => state, callback);
connector.connect(state => state, callback);
store.dispatch({type: 'ACTION', payload: 0});
store.dispatch({type: 'ACTION', payload: 1});
expect(counter).toBe(3);
});

it('Should prevent unnecessary updates when state does not change', () => {
it('Should accept a function or an array of function as selector', () => {
let receivedState1, receivedState2;
connector.connect(state => state.foo, newState => receivedState1 = newState);
connector.connect([state => state.foo], newState => receivedState2 = newState);
expect(receivedState1).toBe('bar');
expect(receivedState1).toBe(receivedState2);
})

it('Should prevent unnecessary updates when state does not change (shallowly)', () => {
let counter = 0;
let callback = () => counter++;
let connector = new Connector(ngRedux, state => state, callback);
connector.connect(state => state.baz, callback);
store.dispatch({type: 'ACTION', payload: 0});
store.dispatch({type: 'ACTION', payload: 0});
store.dispatch({type: 'ACTION', payload: 0});
expect(counter).toBe(2);
store.dispatch({type: 'ACTION', payload: 1});
expect(counter).toBe(3);
});

it('Should pass the selected state as argument to the callback', () => {
let receivedState;
let connector = new Connector(ngRedux, state => state.foo, newState => receivedState = newState);
connector.connect(state => state.foo, newState => receivedState = newState);
store.dispatch({type: 'ACTION', payload: 1});
expect(receivedState).toBe('bar');
});

it('Should unsubscribe when disconnect is called', () => {
let counter = 0;
let callback = () => counter++;
let connector = new Connector(ngRedux, state => state, callback);
store.dispatch({type: 'ACTION', payload: 0});
connector.unsubscribe();
store.dispatch({type: 'ACTION', payload: 2});
expect(counter).toBe(2);
});

it('Factory: connect should create a new Connector', () => {
let api = connectorFactory(ngRedux);
let counter = 0;
let callback = () => counter++;
api.connect(state => state, callback);
store.dispatch({type: 'ACTION', payload: 0});
it('Should pass all the selected state as argument to the callback when provided an array of selectors', () => {
connector.connect([state => state.foo, state => state.anotherState],
(foo, anotherState) => {
expect(foo).toBe('bar');
expect(anotherState).toBe(12);
});
store.dispatch({type: 'ACTION', payload: 1});
store.dispatch({type: 'ACTION', payload: 2});
expect(counter).toBe(4);
});

it('Factory: should allow multiple Connector creation', () => {
let api = connectorFactory(ngRedux);
it('Should return an unsubscribing function', () => {
let counter = 0;
let callback = () => counter++;
api.connect(state => state, callback);
api.connect(state => state, callback);
store.dispatch({type: 'ACTION', payload: 0});
// 2 initialization + each connection responding once to the action = 4
expect(counter).toBe(4);
})

it('Factory: connect should return an unsubscribing function', () => {
let api = connectorFactory(ngRedux);
let counter = 0;
let callback = () => counter++;
let unsubscribe = api.connect(state => state, callback);
let unsubscribe = connector.connect(state => state, callback);
store.dispatch({type: 'ACTION', payload: 0});
unsubscribe();
store.dispatch({type: 'ACTION', payload: 1});
store.dispatch({type: 'ACTION', payload: 2});
expect(counter).toBe(2);
});
Expand Down