├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── build ├── angularCancelOnNavigateModule.js └── angularCancelOnNavigateModule.min.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── src ├── angularCancelOnNavigateModule.js ├── httpPendingRequestsService.js └── httpRequestTimeoutInterceptor.js └── test ├── angularCancelOnNavigateModule.js ├── httpPendingRequestsServiceSpec.js └── httpRequestTimeoutInterceptorSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bower_components/ 3 | node_modules/ 4 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Albert Brand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularJS angular-cancel-on-navigate module 2 | 3 | Provides drop-in functionality that cancels HTTP requests when 4 | the browser's location is changed. 5 | 6 | ## Installation 7 | 8 | Prerequisites: an existing AngularJS project of course. 9 | 10 | Using Bower: 11 | 12 | ```sh 13 | bower install -S angular-cancel-on-navigate 14 | ``` 15 | 16 | Or, manually download `build/angularCancelOnNavigateModule.js` (or the 17 | minified version) and place it somewhere in your project. 18 | 19 | Then, add the script include in your HTML which of course should point to the right location: 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | Add the module dependency to the main app module declaration: 26 | 27 | ```js 28 | angular 29 | .module('myApp', [ 30 | ... 31 | 'angularCancelOnNavigateModule' 32 | ]) 33 | ``` 34 | 35 | ## Configuration 36 | 37 | When the module is included, all $http requests are automatically intercepted and 38 | a cancel promise is registered when the request its timeout is not specified 39 | explicitly. If you want to exclude certain $http calls from this behavior, 40 | either provide an explicit timeout, or set the `noCancelOnRouteChange` property 41 | to `true` in the $http options. 42 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cancel-on-navigate", 3 | "version": "0.1.0", 4 | "description": "AngularJS module that cancels HTTP requests on location change (navigation)", 5 | "main": "src/angularCancelOnNavigateModule.js", 6 | "homepage": "https://github.com/AlbertBrand/angular-cancel-on-navigate", 7 | "authors": [ 8 | "Albert Brand " 9 | ], 10 | "keywords": [ 11 | "angular", 12 | "js", 13 | "angularjs", 14 | "http", 15 | "cancel", 16 | "location", 17 | "change", 18 | "route", 19 | "listener" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ], 29 | "devDependencies": { 30 | "angular": "~1.3.0", 31 | "angular-mocks": "~1.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /build/angularCancelOnNavigateModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-cancel-on-navigate - AngularJS module that cancels HTTP requests on location change (navigation) 3 | * @version v0.1.0 4 | * @link https://github.com/AlbertBrand/angular-cancel-on-navigate 5 | * @license MIT 6 | */ 7 | angular 8 | .module('angularCancelOnNavigateModule', []) 9 | .config(function($httpProvider) { 10 | $httpProvider.interceptors.push('HttpRequestTimeoutInterceptor'); 11 | }) 12 | .run(function ($rootScope, HttpPendingRequestsService) { 13 | $rootScope.$on('$locationChangeSuccess', function (event, newUrl, oldUrl) { 14 | if (newUrl != oldUrl) { 15 | HttpPendingRequestsService.cancelAll(); 16 | } 17 | }) 18 | }); 19 | 20 | angular.module('angularCancelOnNavigateModule') 21 | .service('HttpPendingRequestsService', function ($q) { 22 | var cancelPromises = []; 23 | 24 | function newTimeout() { 25 | var cancelPromise = $q.defer(); 26 | cancelPromises.push(cancelPromise); 27 | return cancelPromise.promise; 28 | } 29 | 30 | function cancelAll() { 31 | angular.forEach(cancelPromises, function (cancelPromise) { 32 | cancelPromise.promise.isGloballyCancelled = true; 33 | cancelPromise.resolve(); 34 | }); 35 | cancelPromises.length = 0; 36 | } 37 | 38 | return { 39 | newTimeout: newTimeout, 40 | cancelAll: cancelAll 41 | }; 42 | }); 43 | 44 | angular.module('angularCancelOnNavigateModule') 45 | .factory('HttpRequestTimeoutInterceptor', function ($q, HttpPendingRequestsService) { 46 | return { 47 | request: function (config) { 48 | config = config || {}; 49 | if (config.timeout === undefined && !config.noCancelOnRouteChange) { 50 | config.timeout = HttpPendingRequestsService.newTimeout(); 51 | } 52 | return config; 53 | }, 54 | 55 | responseError: function (response) { 56 | if (response.config.timeout.isGloballyCancelled) { 57 | return $q.defer().promise; 58 | } 59 | return $q.reject(response); 60 | } 61 | }; 62 | }); 63 | -------------------------------------------------------------------------------- /build/angularCancelOnNavigateModule.min.js: -------------------------------------------------------------------------------- 1 | angular.module("angularCancelOnNavigateModule",[]).config(function(e){e.interceptors.push("HttpRequestTimeoutInterceptor")}).run(function(e,n){e.$on("$locationChangeSuccess",function(e,t,o){t!=o&&n.cancelAll()})}),angular.module("angularCancelOnNavigateModule").service("HttpPendingRequestsService",function(e){function n(){var n=e.defer();return o.push(n),n.promise}function t(){angular.forEach(o,function(e){e.promise.isGloballyCancelled=!0,e.resolve()}),o.length=0}var o=[];return{newTimeout:n,cancelAll:t}}),angular.module("angularCancelOnNavigateModule").factory("HttpRequestTimeoutInterceptor",function(e,n){return{request:function(e){return e=e||{},void 0!==e.timeout||e.noCancelOnRouteChange||(e.timeout=n.newTimeout()),e},responseError:function(n){return n.config.timeout.isGloballyCancelled?e.defer().promise:e.reject(n)}}}); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | concat = require('gulp-concat'), 3 | uglify = require('gulp-uglify'), 4 | rename = require('gulp-rename'), 5 | jscs = require('gulp-jscs'), 6 | jshint = require('gulp-jshint'), 7 | header = require('gulp-header'), 8 | pkg = require('./package.json'); 9 | 10 | var scriptsGlob = 'src/**/*.js'; 11 | var banner = ['/**', 12 | ' * <%= pkg.name %> - <%= pkg.description %>', 13 | ' * @version v<%= pkg.version %>', 14 | ' * @link <%= pkg.homepage %>', 15 | ' * @license <%= pkg.license %>', 16 | ' */', 17 | ''].join('\n'); 18 | 19 | gulp.task('lint', function () { 20 | gulp.src(scriptsGlob) 21 | .pipe(jshint()); 22 | }); 23 | 24 | gulp.task('default', ['lint'], function () { 25 | gulp.src(scriptsGlob) 26 | .pipe(concat('angularCancelOnNavigateModule.js')) 27 | .pipe(header(banner, { pkg : pkg } )) 28 | .pipe(gulp.dest('build/')) 29 | 30 | .pipe(uglify()) 31 | .pipe(rename({ extname: '.min.js' })) 32 | .pipe(gulp.dest('build/')); 33 | }); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Jan 26 2015 21:32:40 GMT+0100 (CET) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | '../bower_components/angular/angular.js', 19 | '../bower_components/angular-mocks/angular-mocks.js', 20 | 'src/**/*.js', 21 | 'test/**/*Spec.js' 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [], 27 | 28 | 29 | // preprocess matching files before serving them to the browser 30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 31 | preprocessors: {}, 32 | 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress' 36 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 37 | reporters: ['progress'], 38 | 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: true, 55 | 56 | 57 | // start these browsers 58 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 59 | browsers: ['PhantomJS'], 60 | 61 | 62 | // Continuous Integration mode 63 | // if true, Karma captures browsers, runs the tests and exits 64 | singleRun: false 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cancel-on-navigate", 3 | "version": "0.1.0", 4 | "description": "AngularJS module that cancels HTTP requests on location change (navigation)", 5 | "main": "src/angularCancelOnNavigateModule.js", 6 | "homepage": "https://github.com/AlbertBrand/angular-cancel-on-navigate", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/AlbertBrand/angular-cancel-on-navigate.git" 13 | }, 14 | "keywords": [ 15 | "angular", 16 | "js", 17 | "angularjs", 18 | "http", 19 | "cancel", 20 | "location", 21 | "change", 22 | "route", 23 | "listener" 24 | ], 25 | "author": "Albert Brand ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/AlbertBrand/angular-cancel-on-navigate/issues" 29 | }, 30 | "devDependencies": { 31 | "karma": "~0.12.0", 32 | "karma-jasmine": "~0.3.0", 33 | "karma-phantomjs-launcher": "~0.1.0" 34 | }, 35 | "dependencies": { 36 | "gulp": "^3.8.10", 37 | "gulp-concat": "^2.4.3", 38 | "gulp-header": "^1.2.2", 39 | "gulp-jshint": "^1.9.2", 40 | "gulp-rename": "^1.2.0", 41 | "gulp-uglify": "^1.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/angularCancelOnNavigateModule.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('angularCancelOnNavigateModule', []) 3 | .config(function($httpProvider) { 4 | $httpProvider.interceptors.push('HttpRequestTimeoutInterceptor'); 5 | }) 6 | .run(function ($rootScope, HttpPendingRequestsService) { 7 | $rootScope.$on('$locationChangeSuccess', function (event, newUrl, oldUrl) { 8 | if (newUrl != oldUrl) { 9 | HttpPendingRequestsService.cancelAll(); 10 | } 11 | }) 12 | }); 13 | -------------------------------------------------------------------------------- /src/httpPendingRequestsService.js: -------------------------------------------------------------------------------- 1 | angular.module('angularCancelOnNavigateModule') 2 | .service('HttpPendingRequestsService', function ($q) { 3 | var cancelPromises = []; 4 | 5 | function newTimeout() { 6 | var cancelPromise = $q.defer(); 7 | cancelPromises.push(cancelPromise); 8 | return cancelPromise.promise; 9 | } 10 | 11 | function cancelAll() { 12 | angular.forEach(cancelPromises, function (cancelPromise) { 13 | cancelPromise.promise.isGloballyCancelled = true; 14 | cancelPromise.resolve(); 15 | }); 16 | cancelPromises.length = 0; 17 | } 18 | 19 | return { 20 | newTimeout: newTimeout, 21 | cancelAll: cancelAll 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /src/httpRequestTimeoutInterceptor.js: -------------------------------------------------------------------------------- 1 | angular.module('angularCancelOnNavigateModule') 2 | .factory('HttpRequestTimeoutInterceptor', function ($q, HttpPendingRequestsService) { 3 | return { 4 | request: function (config) { 5 | config = config || {}; 6 | if (config.timeout === undefined && !config.noCancelOnRouteChange) { 7 | config.timeout = HttpPendingRequestsService.newTimeout(); 8 | } 9 | return config; 10 | }, 11 | 12 | responseError: function (response) { 13 | if (response.config.timeout.isGloballyCancelled) { 14 | return $q.defer().promise; 15 | } 16 | return $q.reject(response); 17 | } 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /test/angularCancelOnNavigateModule.js: -------------------------------------------------------------------------------- 1 | describe('The angularCancelOnNavigateModule', function () { 2 | 3 | var 4 | http, 5 | httpBackend, 6 | location, 7 | rootScope, 8 | successFn, 9 | errorFn; 10 | 11 | beforeEach(module('angularCancelOnNavigateModule')); 12 | 13 | beforeEach(inject(function ($http, $httpBackend, $location, $rootScope) { 14 | http = $http; 15 | httpBackend = $httpBackend; 16 | location = $location; 17 | rootScope = $rootScope; 18 | successFn = jasmine.createSpy('success'); 19 | errorFn = jasmine.createSpy('error'); 20 | })); 21 | 22 | function doGet() { 23 | http.get('/abc').success(successFn).error(errorFn); 24 | } 25 | 26 | it('should succeed normally without route change', function () { 27 | httpBackend.when('GET', '/abc').respond(); 28 | doGet(); 29 | httpBackend.flush(); 30 | 31 | expect(successFn).toHaveBeenCalled(); 32 | expect(errorFn).not.toHaveBeenCalled(); 33 | }); 34 | 35 | it('should fail normally without route change', function () { 36 | httpBackend.when('GET', '/abc').respond(404); 37 | doGet(); 38 | httpBackend.flush(); 39 | 40 | expect(successFn).not.toHaveBeenCalled(); 41 | expect(errorFn).toHaveBeenCalled(); 42 | }); 43 | 44 | it('should not succeed or fail with route change', function () { 45 | httpBackend.when('GET', '/abc').respond(); 46 | doGet(); 47 | location.path('/def'); 48 | rootScope.$digest(); 49 | httpBackend.verifyNoOutstandingRequest(); 50 | 51 | expect(successFn).not.toHaveBeenCalled(); 52 | expect(errorFn).not.toHaveBeenCalled(); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/httpPendingRequestsServiceSpec.js: -------------------------------------------------------------------------------- 1 | describe('The HttpPendingRequestsService', function () { 2 | 3 | var httpPendingRequestsService, 4 | qMock; 5 | 6 | beforeEach(module('angularCancelOnNavigateModule')); 7 | 8 | beforeEach(function () { 9 | module(function ($provide) { 10 | qMock = jasmine.createSpyObj('$q', ['defer', 'reject']); 11 | $provide.value('$q', qMock); 12 | }) 13 | }); 14 | 15 | beforeEach(inject(function (HttpPendingRequestsService) { 16 | httpPendingRequestsService = HttpPendingRequestsService; 17 | })); 18 | 19 | it('should return a new promise when creating a timeout', function () { 20 | var promiseMock = 'promise'; 21 | qMock.defer.and.returnValue({ promise: promiseMock }); 22 | 23 | var result = httpPendingRequestsService.newTimeout(); 24 | 25 | expect(result).toBe(promiseMock); 26 | }); 27 | 28 | it('should resolve all promises when cancelling', function () { 29 | var resolveSpy1 = jasmine.createSpy(), 30 | resolveSpy2 = jasmine.createSpy(), 31 | promiseMock1 = { promise: {}, resolve: resolveSpy1 }, 32 | promiseMock2 = { promise: {}, resolve: resolveSpy2 }; 33 | qMock.defer.and.returnValue(promiseMock1); 34 | httpPendingRequestsService.newTimeout(); 35 | qMock.defer.and.returnValue(promiseMock2); 36 | httpPendingRequestsService.newTimeout(); 37 | 38 | httpPendingRequestsService.cancelAll(); 39 | 40 | expect(resolveSpy1).toHaveBeenCalledWith(); 41 | expect(promiseMock1.promise.isGloballyCancelled).toBe(true); 42 | expect(resolveSpy2).toHaveBeenCalledWith(); 43 | expect(promiseMock2.promise.isGloballyCancelled).toBe(true); 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/httpRequestTimeoutInterceptorSpec.js: -------------------------------------------------------------------------------- 1 | describe('The httpRequestTimeoutInterceptor', function () { 2 | 3 | var httpPendingRequestsServiceMock, 4 | httpRequestTimeoutInterceptor, 5 | qMock, 6 | responseMock = { 7 | config: { 8 | timeout: {} 9 | } 10 | }, 11 | responseMockWithTimeout = { 12 | config: { 13 | timeout: { 14 | isGloballyCancelled: true 15 | } 16 | } 17 | }; 18 | 19 | beforeEach(module('angularCancelOnNavigateModule')); 20 | 21 | beforeEach(function () { 22 | module(function ($provide) { 23 | qMock = jasmine.createSpyObj('$q', ['defer', 'reject']); 24 | httpPendingRequestsServiceMock = jasmine.createSpyObj('HttpPendingRequestsServiceMock', ['newTimeout']); 25 | $provide.value('$q', qMock); 26 | $provide.value('HttpPendingRequestsService', httpPendingRequestsServiceMock); 27 | }) 28 | }); 29 | 30 | beforeEach(inject(function (HttpRequestTimeoutInterceptor) { 31 | httpRequestTimeoutInterceptor = HttpRequestTimeoutInterceptor; 32 | })); 33 | 34 | it('should set a cancel promise on the config', function () { 35 | var promiseMock = {}; 36 | httpPendingRequestsServiceMock.newTimeout.and.returnValue(promiseMock); 37 | 38 | var result = httpRequestTimeoutInterceptor.request({}); 39 | 40 | expect(httpPendingRequestsServiceMock.newTimeout).toHaveBeenCalledWith(); 41 | expect(result.timeout).toBe(promiseMock); 42 | }); 43 | 44 | it('should not set a cancel promise on the config when request already has timeout', function () { 45 | var result = httpRequestTimeoutInterceptor.request({ timeout: 10 }); 46 | 47 | expect(httpPendingRequestsServiceMock.newTimeout).not.toHaveBeenCalled(); 48 | expect(result.timeout).toBe(10); 49 | }); 50 | 51 | it('should not set a cancel promise on the config when request sets noCancelOnRouteChange', function () { 52 | var result = httpRequestTimeoutInterceptor.request({ noCancelOnRouteChange: true }); 53 | 54 | expect(httpPendingRequestsServiceMock.newTimeout).not.toHaveBeenCalled(); 55 | expect(result.timeout).toBeUndefined(); 56 | }); 57 | 58 | it('should return a normal response when not cancelled', function () { 59 | httpRequestTimeoutInterceptor.responseError(responseMock); 60 | 61 | expect(qMock.reject).toHaveBeenCalledWith(responseMock); 62 | }); 63 | 64 | it('should return a new promise response when cancelled', function () { 65 | qMock.defer.and.returnValue({}); 66 | 67 | httpRequestTimeoutInterceptor.responseError(responseMockWithTimeout); 68 | 69 | expect(qMock.defer).toHaveBeenCalledWith(); 70 | }); 71 | 72 | }); 73 | --------------------------------------------------------------------------------