angular-loading-bar icon indicating copy to clipboard operation
angular-loading-bar copied to clipboard

Split the interceptor into a separate service that can be called from outside

Open asharayev opened this issue 8 years ago • 7 comments

Hi!

The problem I'm facing is that I need to add my own 'loading' steps into the common loading bar. The case is about rendering stage, which takes much more time in our project compared to data loading via http requests.

The idea would be to re-factor the current interceptor module and extract the logic working with requests counters into a separate service. This would allow us to just call some methods like cfp.loadingBarService.requestStarted() and then cfp.loadingBarService.requestCompleted(), and those methods would be called from the interceptor internally as well as from external code, to combine loading of HTTP request and custom 'loading' events that don't work as HTTP requests.

The interceptor code in this case would be very small - almost all code with state variables like reqsTotal, etc. would be moved into the new service. The interceptor would also call the service.

Currently I could use the existing service API, but that does not know about the number of pending http requests and as long as some http request completes, my custom progress would be reset or disappear completely.

Best regards, Alexey.

asharayev avatar Apr 03 '16 12:04 asharayev

I doubt this would benefit the majority of users, and for this reason I can say there is little chance such a change would make it into core.

Leaving @chieffancypants to close if you agree?

faceleg avatar Apr 04 '16 02:04 faceleg

Well, I have already fixed the issue in my code, had to copy the source code of your http interceptor and split it as I described. So it is not a problem for me - if you think this suggestion will not give any benefit to the users, then please close it.

By the way, maybe I would add another improvement suggestion to the behavior of progress bar if next requests are submitted while it is already "progressing" to avoid resetting the loading bar to earlier step that it has already progressed. Otherwise it looks quite odd in case the progress bar starts jumping to the left instead of progressing only to the right.

asharayev avatar Apr 04 '16 11:04 asharayev

I've been struggling with this issue as well. I implemented my own renderWatcherService that shows/hides the loading bar based on the $viewContentLoading/Loaded events, but I believe I am getting bit with the problem of an $http request completing and hiding the loading bar prematurely, before all of my $viewContentLoaded events have fired.

At the very least, there should be a way to make the loadingBarInterceptor optional.

@asharayev How did you disable the loadingBarInterceptor in order to use your own? Did you actually fork the code or figure out a way to monkey-patch it?

Edit: Nevermind, I see one can simply depend on 'cfp.loadingBar' in place of 'angular-loading-bar' to get the service without 'cfp.loadingBarInterceptor'.

hackel avatar Dec 05 '16 22:12 hackel

@asharayev I'd be interested in this code. I am about to do the same thing myself. I don't think this is useless. Many people will need to custom start/stop the bar based on internal events as well as $http events. Any way you can put it on pastebin as an example? Thanks.

otterslide avatar Sep 12 '17 16:09 otterslide

Hi,

Basically what I did was to write my own service that exports two methods: operationStarted and operationCompleted - started increments operations counter and shown loading bar while completed method decrements the counter and hides the loading bar if the counter reaches zero.

Inside my own http interceptor I simply call those methods on request and response events.

This is my customized loadingBarService, as you can see it's pretty straightforward:

/**
 * A service based on angular-loading-bar interceptor that extracts its logic of started/completed requests
 * counting and provides it to our own interceptor as well as to our custom code.
 *
 */

(function () {
    'use strict';

    angular
        .module('app.utilities')
        .factory('loadingBarService', loadingBarService);

    /* @ngInject */
    function loadingBarService($log, $q, $timeout, cfpLoadingBar) {

        /**
         * The total number of requests made
         */
        var reqsTotal = 0;

        /**
         * The number of requests completed (either successfully or not)
         */
        var reqsCompleted = 0;

        /**
         * The amount of time spent fetching before showing the loading bar
         */
        var latencyThreshold = cfpLoadingBar.latencyThreshold;

        /**
         * $timeout handle for latencyThreshold
         */
        var startTimeout;

        var service = {
            operationStarted: operationStarted,
            operationCompleted: operationCompleted
        };

        return service;

        /**
         * calls cfpLoadingBar.complete() which removes the
         * loading bar from the DOM.
         */
        function setComplete() {
            $timeout.cancel(startTimeout);
            cfpLoadingBar.complete();
            reqsCompleted = 0;
            reqsTotal = 0;
        }

        function operationStarted() {
            if (reqsTotal === 0) {
                startTimeout = $timeout(function () {
                    cfpLoadingBar.start();
                }, latencyThreshold);
                cfpLoadingBar.set(0);
            }
            reqsTotal++;
        }

        function operationCompleted() {
            reqsCompleted++;
            if (reqsCompleted >= reqsTotal) {
                setComplete();
            } else {
                cfpLoadingBar.set(reqsCompleted / reqsTotal);
            }
        }
    }
})();

asharayev avatar Sep 13 '17 07:09 asharayev

@asharayev Thanks for sharing that.

I ended up doing my own version yesterday. I moved the entire interceptor in a service that also has the start/stop methods. It was a bit difficult to get $http defaults outside of .config while using uiRouter, but $injector worked for that. I think code in .config() is not a good way of coding in the first place, since .config() is for configuring not for big chunks of logic, but in this case Angular makes it difficult to take that code out. This is what I came up with. It's working for me. Simply inject 'CfpInteceptorService' into any other service and call addRequest() and doneRequest()


var loadingBarModule = angular.module('cfp.loadingBar', []);

/**
 * loadingBarInterceptor service
 *
 * Registers itself as an Angular interceptor and listens for XHR requests.
 */
angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar'])
    .config(['$httpProvider', function($httpProvider) {

        $httpProvider.interceptors.push('CfpInteceptorService');
    }]);



loadingBarModule.service('CfpInteceptorService', ['$q', '$cacheFactory', '$timeout', '$rootScope', '$log', '$injector', 'cfpLoadingBar',
    function($q, $cacheFactory, $timeout, $rootScope, $log, $injector, cfpLoadingBar) {

        var $http;

        var service = this;

        /**
         * The total number of requests made
         */
        var reqsTotal = 0;

        /**
         * The number of requests completed (either successfully or not)
         */
        var reqsCompleted = 0;

        /**
         * The amount of time spent fetching before showing the loading bar
         */
        var latencyThreshold = cfpLoadingBar.latencyThreshold;

        /**
         * $timeout handle for latencyThreshold
         */
        var startTimeout;


        /**
         * calls cfpLoadingBar.complete() which removes the
         * loading bar from the DOM.
         */
        function setComplete() {
            $timeout.cancel(startTimeout);
            cfpLoadingBar.complete();
            reqsCompleted = 0;
            reqsTotal = 0;
        }


        var getDefaults = function() {
            if (!$http) {
                $http = $injector.get("$http");
            }
            return $http.defaults;
        }

        /**
         * Determine if the response has already been cached
         * @param  {Object}  config the config option from the request
         * @return {Boolean} retrns true if cached, otherwise false
         */
        function isCached(config) {
            var cache;
            var defaultCache = $cacheFactory.get('$http');
            var defaults = getDefaults();

            // Choose the proper cache source. Borrowed from angular: $http service
            if ((config.cache || defaults.cache) && config.cache !== false &&
                (config.method === 'GET' || config.method === 'JSONP')) {
                cache = angular.isObject(config.cache) ? config.cache :
                    angular.isObject(defaults.cache) ? defaults.cache :
                    defaultCache;
            }

            var cached = cache !== undefined ?
                cache.get(config.url) !== undefined : false;

            if (config.cached !== undefined && cached !== config.cached) {
                return config.cached;
            }
            config.cached = cached;
            return cached;
        }

        service.addRequest = function() {

            if (reqsTotal === 0) {
                startTimeout = $timeout(function() {
                    cfpLoadingBar.start();
                }, latencyThreshold);
            }
            reqsTotal++;
            cfpLoadingBar.set(reqsCompleted / reqsTotal);
        }


        service.doneRequest = function() {
            reqsCompleted++;
            if (reqsCompleted >= reqsTotal) {
                setComplete();
            } else {
                cfpLoadingBar.set(reqsCompleted / reqsTotal);
            }
        }

        service.request = function(config) {
            // Check to make sure this request hasn't already been cached and that
            // the requester didn't explicitly ask us to ignore this request:
            if (!config.ignoreLoadingBar && !isCached(config)) {
                $rootScope.$broadcast('cfpLoadingBar:loading', {
                    url: config.url
                });

                service.addRequest();
            }
            return config;
        };

        service.response = function(response) {

            if (!response || !response.config) {
                $log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
                return response;
            }

            if (!response.config.ignoreLoadingBar && !isCached(response.config)) {
                service.doneRequest();
            }
            return response;
        }

        service.responseError = function(rejection) {
            if (!rejection || !rejection.config) {
                $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
                return $q.reject(rejection);
            }

            if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) {
                reqsCompleted++;
                if (reqsCompleted >= reqsTotal) {
                    $rootScope.$broadcast('cfpLoadingBar:loaded', {
                        url: rejection.config.url,
                        result: rejection
                    });
                    setComplete();
                } else {
                    cfpLoadingBar.set(reqsCompleted / reqsTotal);
                }
            }
            return $q.reject(rejection);
        };
    }
]);

otterslide avatar Sep 13 '17 13:09 otterslide

@otterslide Awesome job! That really helped me a lot.

stefanDeveloper avatar Mar 05 '21 08:03 stefanDeveloper