diff --git a/.gitignore b/.gitignore index c3152f4..b1916f7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ es coverage *.tgz examples/**/dist +.idea diff --git a/README.md b/README.md index b4a3b4a..028957e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ For Angular 2 see [ng2-redux](https://github.com/wbuchwalter/ng2-redux). - [API](#api) - [Dependency Injectable Middleware](#dependency-injectable-middleware) - [Routers](#routers) +- [Config](#config) - [Using DevTools](#using-devtools) - [Additional Resources](#additional-resources) @@ -188,6 +189,29 @@ $ngReduxProvider.createStoreWith(reducers, [thunk, 'myInjectableMiddleware']); Middlewares passed as **string** will then be resolved throught angular's injector. +## Config + +### Debouncing the digest +You can debounce the digest triggered by store modification (usefull in huge apps with a lot of store modifications) by passing a config parameter to the `ngReduxProvider`. + +```javascript +import angular from 'angular'; + +angular.module('ngapplication').config(($ngReduxProvider) => { + 'ngInject'; + + // eslint-disable-next-line + $ngReduxProvider.config.debounce = { + wait: 100, + maxWait: 500, + }; +}); +``` + +This will debounce the digest for 100ms with a maximum delay time of 500ms. Every store modification within this time will be handled by this delayed digest. + +[lodash.debounce](https://lodash.com/docs/4.17.4#debounce) is used for the debouncing. + ## Routers See [redux-ui-router](https://github.com/neilff/redux-ui-router) to make ng-redux and UI-Router work together.
See [ng-redux-router](https://github.com/amitport/ng-redux-router) to make ng-redux and angular-route work together. diff --git a/package.json b/package.json index b0f974a..dc5b5f2 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "babel-runtime": "^6.26.0", "invariant": "^2.2.2", "lodash.curry": "^4.1.1", + "lodash.debounce": "^4.0.8", "lodash.isfunction": "^3.0.8", "lodash.isobject": "^3.0.2", "lodash.isplainobject": "^4.0.6", diff --git a/src/components/digestMiddleware.js b/src/components/digestMiddleware.js index ae7752d..98718c6 100644 --- a/src/components/digestMiddleware.js +++ b/src/components/digestMiddleware.js @@ -1,7 +1,15 @@ -export default function digestMiddleware($rootScope) { +import debounce from 'lodash.debounce'; + +export default function digestMiddleware($rootScope, debounceConfig) { + let debouncedFunction = (expr) => { + $rootScope.$evalAsync(expr); + }; + if(debounceConfig && debounceConfig.wait && debounceConfig.wait > 0) { + debouncedFunction = debounce(debouncedFunction, debounceConfig.wait, { maxWait: debounceConfig.maxWait }); + } return store => next => action => { - const res = next(action); - $rootScope.$evalAsync(res); - return res; + const res = next(action); + debouncedFunction(res); + return res; }; } diff --git a/src/components/ngRedux.js b/src/components/ngRedux.js index 9c6f087..e6f059d 100644 --- a/src/components/ngRedux.js +++ b/src/components/ngRedux.js @@ -41,6 +41,13 @@ export default function ngReduxProvider() { _initialState = initialState || {}; }; + this.config = { + debounce: { + wait: undefined, + maxWait: undefined, + }, + }; + this.$get = ($injector) => { const resolveMiddleware = middleware => isString(middleware) ? $injector.get(middleware) @@ -71,13 +78,13 @@ export default function ngReduxProvider() { } // digestMiddleware needs to be the last one. - resolvedMiddleware.push(digestMiddleware($injector.get('$rootScope'))); + resolvedMiddleware.push(digestMiddleware($injector.get('$rootScope'), this.config.debounce)); // combine middleware into a store enhancer. const middlewares = applyMiddleware(...resolvedMiddleware); // compose enhancers with middleware and create store. - const store = createStore(_reducer, _initialState, compose(...resolvedStoreEnhancer, middlewares)); + const store = createStore(_reducer, _initialState, compose(middlewares, ...resolvedStoreEnhancer)); return assign({}, store, { connect: Connector(store) }); }; diff --git a/test/components/digestMiddleware.spec.js b/test/components/digestMiddleware.spec.js new file mode 100644 index 0000000..5fa67e9 --- /dev/null +++ b/test/components/digestMiddleware.spec.js @@ -0,0 +1,64 @@ +import expect from 'expect'; +import sinon from 'sinon'; +import digestMiddleware from '../../src/components/digestMiddleware'; + + +describe('digestMiddleware', () => { + + it('Should debounce the $evalAsync function if debounce is enabled', (done) => { + const $evalAsync = sinon.spy(); + const $rootScope = { + $evalAsync, + }; + const firstAction = 1; + const secondAction = 2; + const debounceConfig = { + wait: 10, + }; + const next = sinon.spy((action) => (action)); + const middleware = digestMiddleware($rootScope, debounceConfig); + middleware()(next)(firstAction); + setTimeout(() => { + middleware()(next)(secondAction); + }, 1); + setTimeout(() => { + expect($evalAsync.calledOnce).toBe(true); + expect(next.calledTwice).toBe(true); + expect(next.firstCall.calledWithExactly(firstAction)).toBe(true); + expect(next.secondCall.calledWithExactly(secondAction)).toBe(true); + expect($evalAsync.firstCall.calledWithExactly(secondAction)).toBe(true); + done(); + }, debounceConfig.wait + 10); + + }); + + it('Should not debounce the $evalAsync function if debounce is disabled', () => { + const disabledDebounceConfigs = [ + null, + undefined, + {}, + { wait: 0 }, + ]; + disabledDebounceConfigs.forEach(() => { + const $evalAsync = sinon.spy(); + const $rootScope = { + $evalAsync, + }; + const firstAction = 1; + const secondAction = 2; + const debounceConfig = {}; + + const next = sinon.spy((action) => (action)); + const middleware = digestMiddleware($rootScope, debounceConfig); + middleware()(next)(firstAction); + middleware()(next)(secondAction); + expect($evalAsync.calledTwice).toBe(true); + expect(next.calledTwice).toBe(true); + expect(next.firstCall.calledWithExactly(firstAction)).toBe(true); + expect(next.secondCall.calledWithExactly(secondAction)).toBe(true); + expect($evalAsync.firstCall.calledWithExactly(firstAction)).toBe(true); + expect($evalAsync.secondCall.calledWithExactly(secondAction)).toBe(true); + }); + }); + +});