Repository: ajoslin/angular-promise-tracker Branch: master Commit: 99e0064952a9 Files: 12 Total size: 28.3 KB Directory structure: gitextract_3ebtioif/ ├── .editorconfig ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── README.md ├── bower.json ├── package.json ├── promise-tracker-http-interceptor.js ├── promise-tracker.js └── test/ ├── karma.conf.js └── unit/ ├── interceptor.spec.js └── provider.spec.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .gitignore ================================================ *.swp npm-debug.log .DS_Store demo bower_components node_modules dist ================================================ FILE: .travis.yml ================================================ --- language: node_js node_js: - 0.10 env: before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - npm install -g grunt-cli bower - npm link - bower install script: - grunt ================================================ FILE: Gruntfile.js ================================================ module.exports = function (grunt) { require('load-grunt-tasks')(grunt); grunt.initConfig({ dist: 'dist', pkgFile: 'bower.json', pkg: grunt.file.readJSON('bower.json'), watch: { scripts: { files: ['src/**/*.js', 'test/unit/**/*.js'], tasks: ['karma:watch:run'] }, gruntfile: { files: ['Gruntfile.js'], tasks: ['jshint'] } }, jshint: { all: ['Gruntfile.js', 'src/**/*.js'], options: { eqeqeq: true, globals: { angular: true } } }, clean: ['demo/**/*'], karma: { watch: { configFile: 'test/karma.conf.js', background: true, }, single: { configFile: 'test/karma.conf.js', singleRun: true, browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], } }, changelog: { options: { dest: 'CHANGELOG.md' } }, }); grunt.registerTask('dev', ['karma:watch', 'watch']); grunt.registerTask('default', ['jshint', 'test']); grunt.registerTask('test', ['karma:single']); }; ================================================ FILE: README.md ================================================ angular-promise-tracker ======================= > **Version**: 2.0 (note to users using version 1.x: upgrading has *many* breaking changes, see [the CHANGELOG](https://github.com/ajoslin/angular-promise-tracker/tree/master/CHANGELOG.md).) [](https://travis-ci.org/ajoslin/angular-promise-tracker) Small, feature filled library used to easily add spinners or general promise/request tracking to your angular app. * [Quick Start](#quick-start) * [API Documentation](#api-documentation) * [Changes](https://github.com/ajoslin/angular-promise-tracker/tree/master/CHANGELOG.md) * [License](#license) ## Quick Start The basic idea: each time we add one or more promises to an instance of a `promiseTracker`, that instance's `active()` method will return true until all added promises are resolved. A common use case is showing some sort of loading spinner while some http requests are loading. [Play with this example on plunkr](http://plnkr.co/edit/PrO2ou9b1uANbeGoX6eB?p=preview) ```sh $ bower install angular-promise-tracker ``` ```html
angular-promise-tracker by Andy Joslin is free of known copyright restrictions.
================================================
FILE: bower.json
================================================
{
"author": "Andy Joslin",
"name": "promise-tracker",
"description": "Easily add spinners or general request tracking to your angular app.",
"version": "2.1.0",
"homepage": "http://github.com/ajoslin/angular-promise-tracker",
"repository": {
"type": "git",
"url": "git://github.com/ajoslin/angular-promise-tracker"
},
"license": "Public Domain",
"main": "./promise-tracker.js",
"ignore": [
"CHANGELOG.md",
"Gruntfile.js",
"bower.json",
"bower_components",
"dist",
"node_modules",
"package.json",
"src",
"test"
],
"files": [
"promise-tracker.js",
"promise-tracker-http-interceptor.js"
],
"dependencies": {
"angular": ">=1.3.0"
},
"devDependencies": {
"angular-mocks": ">=1.3.0",
"angular-resource": ">=1.3.0"
}
}
================================================
FILE: package.json
================================================
{
"name": "angular-promise-tracker",
"version": "2.2.2",
"main": "promise-tracker.js",
"author": {
"name": "Andy Joslin"
},
"repository": {
"url": "git://github.com/ajoslin/angular-promise-tracker.git"
},
"homepage": "https://github.com/ajoslin/angular-promise-tracker",
"dependencies": {
"angular": ">=1.3.0"
},
"devDependencies": {
"load-grunt-tasks": "~0.2.1",
"semver": "~2.2.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt": "~0.4.2",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-uglify": "~0.2.7",
"karma-script-launcher": "~0.1.0",
"karma-chrome-launcher": "~0.1.2",
"karma-html2js-preprocessor": "~0.1.0",
"karma-firefox-launcher": "~0.1.2",
"karma-jasmine": "~0.1.5",
"requirejs": "~2.1.9",
"karma-requirejs": "~0.2.1",
"karma-coffee-preprocessor": "~0.1.1",
"karma-phantomjs-launcher": "~0.1.1",
"karma": "~0.10.8",
"grunt-karma": "~0.6.2",
"grunt-conventional-changelog": "~1.1.0"
}
}
================================================
FILE: promise-tracker-http-interceptor.js
================================================
/*
* promise-tracker - v2.1.0 - 2014-11-15
* http://github.com/ajoslin/angular-promise-tracker
* Created by Andy Joslin; Licensed under Public Domain
*/
(function() {
angular.module('ajoslin.promise-tracker')
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push(['$q', 'promiseTracker', function($q, promiseTracker) {
return {
request: function(config) {
if (config.tracker) {
if (!angular.isArray(config.tracker)) {
config.tracker = [config.tracker];
}
config.$promiseTrackerDeferred = config.$promiseTrackerDeferred || [];
angular.forEach(config.tracker, function(tracker) {
var deferred = tracker.createPromise();
config.$promiseTrackerDeferred.push(deferred);
});
}
return $q.when(config);
},
response: function(response) {
if (response.config && response.config.$promiseTrackerDeferred) {
angular.forEach(response.config.$promiseTrackerDeferred, function(deferred) {
deferred.resolve(response);
});
}
return $q.when(response);
},
responseError: function(response) {
if (response.config && response.config.$promiseTrackerDeferred) {
angular.forEach(response.config.$promiseTrackerDeferred, function(deferred) {
deferred.reject(response);
});
}
return $q.reject(response);
}
};
}]);
}]);
}());
================================================
FILE: promise-tracker.js
================================================
angular.module('ajoslin.promise-tracker', [])
.provider('promiseTracker', function() {
var trackers = {};
this.$get = ['$q', '$timeout', function($q, $timeout) {
function cancelTimeout(promise) {
if (promise) {
$timeout.cancel(promise);
}
}
return function PromiseTracker(options) {
options = options || {};
//Array of promises being tracked
var tracked = [];
var self = {};
//Allow an optional "minimum duration" that the tracker has to stay active for.
var minDuration = options.minDuration;
//Allow a delay that will stop the tracker from activating until that time is reached
var activationDelay = options.activationDelay;
var minDurationPromise;
var activationDelayPromise;
self.active = function() {
//Even if we have a promise in our tracker, we aren't active until delay is elapsed
if (activationDelayPromise) {
return false;
}
return tracked.length > 0;
};
self.tracking = function() {
//Even if we aren't active, we could still have a promise in our tracker
return tracked.length > 0;
};
self.trackingCount = function() {
return tracked.length;
};
self.destroy = self.cancel = function() {
minDurationPromise = cancelTimeout(minDurationPromise);
activationDelayPromise = cancelTimeout(activationDelayPromise);
for (var i=tracked.length-1; i>=0; i--) {
tracked[i].resolve();
}
tracked.length = 0;
};
//Create a promise that will make our tracker active until it is resolved.
// @return deferred - our deferred object that is being tracked
self.createPromise = function() {
var deferred = $q.defer();
tracked.push(deferred);
//If the tracker was just inactive and this the first in the list of
//promises, we reset our delay and minDuration
//again.
if (tracked.length === 1) {
if (activationDelay) {
activationDelayPromise = $timeout(function() {
activationDelayPromise = cancelTimeout(activationDelayPromise);
startMinDuration();
}, activationDelay);
} else {
startMinDuration();
}
}
deferred.promise.then(onDone(false), onDone(true));
return deferred;
function startMinDuration() {
if (minDuration) {
minDurationPromise = $timeout(angular.noop, minDuration);
}
}
//Create a callback for when this promise is done. It will remove our
//tracked promise from the array if once minDuration is complete
function onDone(isError) {
return function(value) {
(minDurationPromise || $q.when()).then(function() {
var index = tracked.indexOf(deferred);
tracked.splice(index, 1);
//If this is the last promise, cleanup the timeouts
//for activationDelay
if (tracked.length === 0) {
activationDelayPromise = cancelTimeout(activationDelayPromise);
}
});
};
}
};
self.addPromise = function(promise) {
if (Array.isArray(promise)) {
return $q.all(promise.map(self.addPromise));
}
promise = promise && (promise.$promise || promise) || {};
if (!promise.then) {
throw new Error("promiseTracker#addPromise expects a promise object!");
}
var deferred = self.createPromise();
//When given promise is done, resolve our created promise
//Allow $then for angular-resource objects
promise.then(function success(value) {
deferred.resolve(value);
return value;
}, function error(value) {
deferred.reject(value);
return $q.reject(value);
});
return deferred;
};
return self;
};
}];
});
================================================
FILE: test/karma.conf.js
================================================
// Karma configuration
// Generated on Mon Dec 23 2013 08:15:21 GMT-0500 (EST)
module.exports = function(config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '../',
// frameworks to use
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
'bower_components/angular-resource/angular-resource.js',
'promise-tracker.js',
'promise-tracker-http-interceptor.js',
'test/unit/**/*.js'
],
// list of files to exclude
exclude: [
],
// test results reporter to use
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
reporters: ['dots'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera (has to be installed with `npm install karma-opera-launcher`)
// - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
// - PhantomJS
// - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
browsers: ['Chrome'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};
================================================
FILE: test/unit/interceptor.spec.js
================================================
describe('http interceptor', function() {
beforeEach(module('ajoslin.promise-tracker'));
var http, promiseTracker, backend, q;
beforeEach(inject(function($http, _promiseTracker_, $httpBackend, $q) {
http = $http;
promiseTracker = _promiseTracker_;
$httpBackend.whenGET('/ok').respond(200);
$httpBackend.whenGET('/error').respond(404);
backend = $httpBackend;
q = $q;
}));
function digest() {
inject(function($rootScope) { $rootScope.$digest(); });
}
it('should add a promise to tracking with http config option', function() {
var tracker = promiseTracker();
var tracker2 = promiseTracker();
spyOn(tracker, 'createPromise').andCallThrough();
spyOn(tracker2, 'createPromise').andCallThrough();
http.get('/ok', { tracker: tracker });
digest();
expect(tracker.createPromise).toHaveBeenCalled();
tracker.createPromise.reset();
http.get('/ok', { tracker: [tracker,tracker2] });
digest();
expect(tracker.createPromise).toHaveBeenCalled();
expect(tracker2.createPromise).toHaveBeenCalled();
});
it('should resolve on good response', function(){
var tracker = promiseTracker();
var deferred = q.defer();
spyOn(tracker, 'createPromise').andCallFake(function() {
return deferred;
});
spyOn(deferred, 'resolve');
http.get('/ok', { tracker: tracker });
digest();
backend.flush();
expect(deferred.resolve).toHaveBeenCalled();
expect(deferred.resolve.mostRecentCall.args[0].status).toBe(200);
});
it('should reject on error response', function(){
var tracker = promiseTracker();
var deferred = q.defer();
spyOn(tracker, 'createPromise').andCallFake(function() {
return deferred;
});
spyOn(deferred, 'reject');
http.get('/error', { tracker: tracker });
digest();
backend.flush();
expect(deferred.reject).toHaveBeenCalled();
expect(deferred.reject.mostRecentCall.args[0].status).toBe(404);
});
});
================================================
FILE: test/unit/provider.spec.js
================================================
describe('promiseTracker provider', function() {
beforeEach(module('ajoslin.promise-tracker'));
var promiseTracker, timeout, q;
beforeEach(inject(function(_promiseTracker_, $timeout, $q) {
promiseTracker = _promiseTracker_;
timeout = $timeout;
q = $q;
}));
function digest() {
inject(function($rootScope) { $rootScope.$digest(); });
}
it('should create a tracker with api', function() {
var tracker = new promiseTracker();
expect(typeof tracker.addPromise).toBe('function');
expect(typeof tracker.createPromise).toBe('function');
expect(typeof tracker.destroy).toBe('function');
expect(typeof tracker.cancel).toBe('function');
});
it('should create a tracker even if no `new`', function() {
var tracker = promiseTracker();
expect(typeof tracker.addPromise).toBe('function');
});
it('should not be active by default', function() {
expect(promiseTracker().active()).toBe(false);
});
it('should not be tracking by default', function() {
expect(promiseTracker().tracking()).toBe(false);
});
describe('addPromise', function() {
it('should error with object', function() {
expect(function() { promiseTracker().addPromise({}); }).toThrow();
});
it('should error with deferred', function() {
expect(function() { promiseTracker().addPromise(q.defer()); }).toThrow();
});
it('should not error with then, $then, $promise.then', function() {
promiseTracker().addPromise(q.defer().promise);
promiseTracker().addPromise({ $promise: q.defer().promise } );
});
it('should return promise from createPromise', function() {
var tracker = promiseTracker();
var promise = q.defer().promise;
var created = q.defer();
spyOn(tracker, 'createPromise').andCallFake(function() {
return created;
});
var ret = tracker.addPromise(promise);
expect(ret).toBe(created);
});
it('should resolve returned promise when passed in promise is resolved', function() {
var tracker = promiseTracker();
var deferred = q.defer();
var trackerPromise = tracker.addPromise(deferred.promise);
spyOn(trackerPromise, 'resolve');
deferred.resolve(1);
digest();
expect(trackerPromise.resolve).toHaveBeenCalledWith(1);
});
it('should reject returned promise when passed in promise is rejected', function() {
var tracker = promiseTracker();
var deferred = q.defer();
var trackerPromise = tracker.addPromise(deferred.promise);
spyOn(trackerPromise, 'reject');
deferred.reject(2);
digest();
expect(trackerPromise.reject).toHaveBeenCalledWith(2);
});
it('should start tracking with then, $then, $promise.then', function() {
var tracker = promiseTracker();
tracker.addPromise(q.defer().promise);
expect(tracker.tracking()).toBe(true);
tracker = promiseTracker();
tracker.addPromise(q.defer().promise);
expect(tracker.tracking()).toBe(true);
tracker = promiseTracker();
tracker.addPromise({ $promise: q.defer().promise });
expect(tracker.tracking()).toBe(true);
});
});
describe('createPromise', function() {
it('should return a deferred', function() {
expect(promiseTracker().createPromise().promise.then).toBeTruthy();
});
it('should set active to true when promise is added', function() {
var tracker = promiseTracker();
tracker.createPromise();
expect(tracker.active()).toBe(true);
});
it('should set active to true when promises are added', function() {
var tracker = promiseTracker();
tracker.createPromise();
tracker.createPromise();
expect(tracker.active()).toBe(true);
});
it('should set active to false when promises are added and resolved/rejected', function() {
var tracker = promiseTracker();
var p1 = tracker.createPromise();
var p2 = tracker.createPromise();
expect(tracker.active()).toBe(true);
p1.resolve();
digest();
expect(tracker.active()).toBe(true);
p2.reject();
digest();
expect(tracker.active()).toBe(false);
});
it('should set tracking to true when promise is added', function() {
var tracker = promiseTracker();
tracker.createPromise();
expect(tracker.tracking()).toBe(true);
});
it('should set tracking to true when promises are added', function() {
var tracker = promiseTracker();
tracker.createPromise();
tracker.createPromise();
expect(tracker.tracking()).toBe(true);
});
it('should set tracking to false when promises are added and resolved/rejected', function() {
var tracker = promiseTracker();
var p1 = tracker.createPromise();
var p2 = tracker.createPromise();
expect(tracker.tracking()).toBe(true);
p1.resolve();
digest();
expect(tracker.tracking()).toBe(true);
p2.reject();
digest();
expect(tracker.tracking()).toBe(false);
});
});
it('cancel should deactivate and resolve all promises', function() {
var tracker = promiseTracker();
var p1 = tracker.createPromise();
expect(tracker.active()).toBe(true);
spyOn(p1, 'resolve');
tracker.cancel();
expect(p1.resolve).toHaveBeenCalled();
expect(tracker.active()).toBe(false);
expect(tracker.tracking()).toBe(false);
});
it('destroy should be cancel', function() {
var tracker = promiseTracker();
expect(tracker.destroy).toBe(tracker.cancel);
});
describe('activationDelay', function() {
it('should not be active() until delay is over', function() {
var tracker = promiseTracker({ activationDelay: 1000 });
tracker.createPromise();
//Should not be active due to delay
expect(tracker.active()).toBe(false);
tracker.createPromise();
expect(tracker.active()).toBe(false);
//Flush, it should be active
timeout.flush();
expect(tracker.active()).toBe(true);
});
it('should be tracking irrespective of the activation delay', function() {
var tracker = promiseTracker({ activationDelay: 1000 });
tracker.createPromise();
//Should be tracking
expect(tracker.tracking()).toBe(true);
tracker.createPromise();
expect(tracker.tracking()).toBe(true);
//Flush, it should be tracking
timeout.flush();
expect(tracker.tracking()).toBe(true);
});
});
describe('minDuration', function() {
it('should be active() for at least minDuration', function() {
var tracker = promiseTracker({ minDuration: 1000 });
var p1 = tracker.createPromise();
expect(tracker.active()).toBe(true);
p1.resolve();
digest();
//Should still be active until minDuration timeout elapses
expect(tracker.active()).toBe(true);
timeout.flush();
expect(tracker.active()).toBe(false);
});
it('should not deactivate if there is still another promise active', function() {
var tracker = promiseTracker({ minDuration: 1000 });
var p1 = tracker.createPromise();
expect(tracker.active()).toBe(true);
p1.resolve();
digest();
//Should still be active until minDuration timeout elapses
expect(tracker.active()).toBe(true);
var p2 = tracker.createPromise();
timeout.flush();
expect(tracker.active()).toBe(true);
p2.resolve();
digest();
expect(tracker.active()).toBe(false);
});
it('should be tracking for at least minDuration', function() {
var tracker = promiseTracker({ minDuration: 1000 });
var p1 = tracker.createPromise();
expect(tracker.tracking()).toBe(true);
p1.resolve();
digest();
//Should still be tracking until minDuration timeout elapses
expect(tracker.tracking()).toBe(true);
timeout.flush();
expect(tracker.tracking()).toBe(false);
});
it('should continue tracking if there is still another promise active', function() {
var tracker = promiseTracker({ minDuration: 1000 });
var p1 = tracker.createPromise();
expect(tracker.tracking()).toBe(true);
p1.resolve();
digest();
//Should still be tracking until minDuration timeout elapses
expect(tracker.tracking()).toBe(true);
var p2 = tracker.createPromise();
timeout.flush();
expect(tracker.tracking()).toBe(true);
p2.resolve();
digest();
expect(tracker.tracking()).toBe(false);
});
});
describe('minDuration + activationDelay', function() {
it('should delay, be active, wait until duration, then be inactive', function() {
var tracker = promiseTracker({ minDuration: 500, activationDelay: 250 });
var p1 = tracker.createPromise();
expect(tracker.active()).toBe(false);
timeout.flush();
expect(tracker.active()).toBe(true);
p1.resolve();
digest();
expect(tracker.active()).toBe(true);
timeout.flush();
expect(tracker.active()).toBe(false);
});
it('should delay, be tracking, wait until duration, then be not tracking', function() {
var tracker = promiseTracker({ minDuration: 500, activationDelay: 250 });
expect(tracker.tracking()).toBe(false);
var p1 = tracker.createPromise();
expect(tracker.tracking()).toBe(true);
timeout.flush();
expect(tracker.tracking()).toBe(true);
p1.resolve();
digest();
expect(tracker.tracking()).toBe(true);
timeout.flush();
expect(tracker.tracking()).toBe(false);
});
});
});