├── .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 |
--------------------------------------------------------------------------------