From 98668cfb22a0df56e3c7ce63f15154fa81648030 Mon Sep 17 00:00:00 2001 From: wbuchwalter Date: Fri, 7 Aug 2015 17:46:59 -0400 Subject: [PATCH 1/3] API modification for 0.3.0 --- README.md | 16 +++++----- src/components/connector.js | 64 ------------------------------------- src/components/ngRedux.js | 41 ++++++++++++++++++++++++ src/components/provider.js | 15 --------- src/index.js | 4 +-- src/utils/hashCode.js | 11 +++++++ 6 files changed, 61 insertions(+), 90 deletions(-) delete mode 100644 src/components/connector.js create mode 100644 src/components/ngRedux.js delete mode 100644 src/components/provider.js create mode 100644 src/utils/hashCode.js diff --git a/README.md b/README.md index 213880d..0e83ff3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -48,18 +48,18 @@ angular.module('app', ['ngRedux']) controllerAs: 'vm', controller: TodoLoaderController, template: "
{{todo.text}}
", - + [...] }; } - -class TodoLoaderController { + +class TodoLoaderController { constructor(reduxConnector) { this.todos = []; reduxConnector.connect(state => state.todos, todos => this.todos = todos); } - + [...] } ``` @@ -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(); } @@ -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); ``` diff --git a/src/components/connector.js b/src/components/connector.js deleted file mode 100644 index 001ab8b..0000000 --- a/src/components/connector.js +++ /dev/null @@ -1,64 +0,0 @@ -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 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); - } -} \ No newline at end of file diff --git a/src/components/ngRedux.js b/src/components/ngRedux.js new file mode 100644 index 0000000..ee812fc --- /dev/null +++ b/src/components/ngRedux.js @@ -0,0 +1,41 @@ +import isFunction from '../utils/isFunction'; +import hashCode from '../utils/hashCode'; +import invariant from 'invariant'; +import shallowEqual from '../utils/shallowEqual'; +import isPlainObject from '../utils/isPlainObject'; + +export default function ngReduxProvider() { + let reduxStore = undefined; + this.setReduxStore = store => reduxStore = store; + + this.$get = () => { + 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 + ); + + let params = null; + let unsubscribe = reduxStore.subscribe(() => { + let nextParams = selectors.map(selector => selector(reduxStore.getState())); + if(params === null || params.every((param, index) => param !== nextParams[index])) { + callback(params); + params = nextParams; + } + }); + + return unsubscribe; + }, + getStore() { + return reduxStore; + } + } + } +} + diff --git a/src/components/provider.js b/src/components/provider.js deleted file mode 100644 index c742803..0000000 --- a/src/components/provider.js +++ /dev/null @@ -1,15 +0,0 @@ -export default function ngReduxProvider() { - let reduxStoreInstance = undefined; - this.setReduxStore = store => reduxStoreInstance = store; - this.$get = () => new NgRedux(reduxStoreInstance); -} - -class NgRedux { - constructor(store){ - this.reduxStore = store; - } - - getStore() { - return this.reduxStore; - } -} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 2abc786..1aebff5 100644 --- a/src/index.js +++ b/src/index.js @@ -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; \ No newline at end of file diff --git a/src/utils/hashCode.js b/src/utils/hashCode.js new file mode 100644 index 0000000..806c975 --- /dev/null +++ b/src/utils/hashCode.js @@ -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; +} \ No newline at end of file From b59f98d62372ac6045257407c8dc73244b9bec64 Mon Sep 17 00:00:00 2001 From: William Buchwalter Date: Mon, 10 Aug 2015 01:01:58 -0400 Subject: [PATCH 2/3] New api, with tests updated --- src/components/connector.js | 35 +++++++++++++ src/components/ngRedux.js | 34 +------------ src/utils/shallowEqual.js | 3 +- test/components/connector.spec.js | 81 ++++++++++++------------------- 4 files changed, 68 insertions(+), 85 deletions(-) create mode 100644 src/components/connector.js diff --git a/src/components/connector.js b/src/components/connector.js new file mode 100644 index 0000000..cbc635f --- /dev/null +++ b/src/components/connector.js @@ -0,0 +1,35 @@ +import isFunction from '../utils/isFunction'; +import invariant from 'invariant'; + +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; + } + } +} \ No newline at end of file diff --git a/src/components/ngRedux.js b/src/components/ngRedux.js index ee812fc..1071da5 100644 --- a/src/components/ngRedux.js +++ b/src/components/ngRedux.js @@ -1,41 +1,11 @@ -import isFunction from '../utils/isFunction'; -import hashCode from '../utils/hashCode'; -import invariant from 'invariant'; -import shallowEqual from '../utils/shallowEqual'; -import isPlainObject from '../utils/isPlainObject'; +import Connector from './connector'; export default function ngReduxProvider() { let reduxStore = undefined; this.setReduxStore = store => reduxStore = store; this.$get = () => { - 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 - ); - - let params = null; - let unsubscribe = reduxStore.subscribe(() => { - let nextParams = selectors.map(selector => selector(reduxStore.getState())); - if(params === null || params.every((param, index) => param !== nextParams[index])) { - callback(params); - params = nextParams; - } - }); - - return unsubscribe; - }, - getStore() { - return reduxStore; - } - } + return Connector(reduxStore); } } diff --git a/src/utils/shallowEqual.js b/src/utils/shallowEqual.js index 9a87c45..e99773a 100644 --- a/src/utils/shallowEqual.js +++ b/src/utils/shallowEqual.js @@ -20,5 +20,4 @@ } return true; - } - \ No newline at end of file + } \ No newline at end of file diff --git a/test/components/connector.spec.js b/test/components/connector.spec.js index 9c6f8a3..988ab7e 100644 --- a/test/components/connector.spec.js +++ b/test/components/connector.spec.js @@ -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); }); From e4e3b95e992854edebcc70e91cf5259cb867972e Mon Sep 17 00:00:00 2001 From: William Buchwalter Date: Mon, 10 Aug 2015 01:02:56 -0400 Subject: [PATCH 3/3] version updated --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e159098..491889c 100644 --- a/package.json +++ b/package.json @@ -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": {