skip to Main Content

I am using ui-router for my angular app. I have some views that contain images. I would like the angular-loading-bar to track also when these images are finished loading. Or more generally, until the view has finished rendering.

When I stumbled upon that post on how to do SEO with angularJS, it indicated that we must create static html snapshots of every “page” and serve them to search engine bots so that they can be crawled. To do that, we must ourselves crawl over all pages in our application with a headless browser and store the html into a file that will be served. But, with angular having to load views over ajax and all, we must wait for our page to be loaded before storing what the headless browser html. Otherwise we get empty html with empty views.

I have written a small script that can check for the ready status of our views. When the $rootScope.status property equals ‘ready’, I know I can store my headless browser’s html as it has finished loading.

var app = angular.module("app", ["ui.router", 'ngAnimate','angular-loading-bar','angular-images-loaded','angular-google-analytics']);

app.run(['$rootScope', function($rootScope){

    $rootScope.loadingCount = 0;
    $rootScope.changeSuccess = false;

    $rootScope.ready = function(){
        $rootScope.loadingCount--;
        if (($rootScope.loadingCount == 0) && ($rootScope.changeSuccess == true)){
            $rootScope.status = 'ready';
        }
    };

    $rootScope.loading = function(){
        $rootScope.loadingCount++;
        $rootScope.status = 'loading';
    };

    $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
        $rootScope.loadingCount = 0;
        $rootScope.changeSuccess = false;
    });

    $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
        $rootScope.changeSuccess = true;
    });

    $rootScope.$on("$viewContentLoading", function(){
        $rootScope.loadingCount++;
    });

    $rootScope.$on("$viewContentLoading", function(){
        $rootScope.loadingCount--;
    });

}]);

Then, in each of our controllers, we must call

$rootScope.loading();

and when the controller is ready

$rootScope.ready()

With this, we’ll be able to check if all our controllers have rendered their views. It’s not super elegant for now, but it does the job.

This script can be well integrated with angular-loading-bar as it tracks the readiness of the overall application. The progress bar could be an indicator of that progress. The drawback of this is that it has conflicts with the natural behaviour of angular-loading-bar tracking XHR requests.

For example, in my controllers I use this :

app.controller("WorksController", [

    "$scope", "cfpLoadingBar",

    function ($scope, cfpLoadingBar) {
        cfpLoadingBar.start();
        $scope.imgLoadedEvents = {
            always: function () {
                cfpLoadingBar.complete();
            }
        };
    }
]);

This code should be migrated right in the $rootScope script that tracks the readiness of the views.

$rootScope.$watch('status', function(newValue, oldValue){
    if (newValue == 'loading'){ cfpLoadingBar.start() }
    else if (newValue == 'ready') { cfpLoadingBar.complete() }
})

Though, angular-progress-bar still works in the background. I let active the XHR interceptor. Though, if an XHR requests is completed before the image has loaded, the progress bar disappears even if the views have not finished. As well, if an image has loaded before the XHR request is completed, the progress bar disappears.

How can I integrate the XHR interception capabilities of the angular-loading-bar with this view readiness interception capabilities?

4

Answers


  1. Chosen as BEST ANSWER

    As @anid-monsur specified, impossible to do without modifying the loading-bar code. Though, I wanted to keep the initial functionality of angular-loading-bar so I followed a different path that suggested by him.

    Instead of removing the hide-show code, I added custom event handlers like these in the angular-loading-bar interceptor :

    $rootScope.$on("cfpLoadingBar:customRequest", function(event, args){
        if (!args) {
          args = {};
        }
        if (!args.url){
          args.url = 'custom';
        }
        loading(args);
      });
    
      $rootScope.$on("cfpLoadingBar:customResponse", function(event, args){
        if (!args) {
          args = {};
        }
        if (!args.config){
          args.config = {};
        }
        if (!args.config.url){
          args.config.url = 'custom';
        }
        response(args);
      }); 
    

    Check the code here

    So, when my ui-router changes state I can write :

    app.run(['$rootScope', 'cfpLoadingBar', function($rootScope, cfpLoadingBar){
    
        $rootScope.$broadcast("cfpLoadingBar:customRequest");
    
        $rootScope.ready = function(){
            $rootScope.$broadcast("cfpLoadingBar:customResponse");
        };
    
        $rootScope.loading = function(){
            $rootScope.$broadcast("cfpLoadingBar:customRequest");
        };
    
        $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
            $rootScope.$broadcast("cfpLoadingBar:customRequest");
        });
    
        $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
            $rootScope.$broadcast("cfpLoadingBar:customResponse");
        });
    
    }]);
    

    Notice the added $rootScope.ready() and $rootScope.loading()

    They are used when a controller has a deferred loading process

    For example, with angular-imagesloaded :

    app.controller("WorksController", [
        "$scope", '$rootScope',
    
        function ($scope, $rootScope) {
    
            $rootScope.loading();
    
            $scope.imgLoadedEvents = {
                done: function () {
                    $rootScope.ready();
                }
            };
    
        }
    ]);
    

    This way, I can still use the native interceptor provided by angular-loading-bar and register my own "custom" requests.


  2. This isn’t possible to do without modifying angular-loading-bar’s code because the $http interceptor does not have any kind of API you can hook into.

    You should fork the project on Github and modify the cfb.loadingBarInterceptor. Basically what you’ll need to do is remove the code that hides and shows the loading bar. Leave the code in that broadcasts events on $rootScope. This is what you’ll hook into.

      return {
        '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});            
          }
          return config;
        },
    
        '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)) {
            $rootScope.$broadcast('cfpLoadingBar:loaded', {url: response.config.url, result: response});
          }
          return response;
        },
    
        '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)) {
            $rootScope.$broadcast('cfpLoadingBar:loaded', {url: rejection.config.url, result: rejection});
          }
          return $q.reject(rejection);
        }
      };
    

    Now, in your run block, simply add these lines at the bottom:

    app.run(['$rootScope', function($rootScope){
      // Previous code ...
    
      $rootScope.$on("cfbLoadingBar:loaded", $rootScope.ready);
      $rootScope.$on("cfbLoadingBar:loading", $rootScope.loading);
    
    });
    

    In case it’s not clear what this does, this uses the events from your new $http interceptor which results in ready and loading being called in the same way that your controllers do.

    Login or Signup to reply.
  3. The blogpost you are referring to is legacy.

    Google will index your single page application without any html snapshots.


    If you want to create Snapshots (e.g. for use of canonical tag which is not supported yet!) you should NOT do this manually but integrate a task in your build process. With Grunt/Gulp this is very simple and can be done in 10 minutes. There is no need to specify a “ready” Event – phantomjs will recognize when your script is done.

    The proper way would be to move the function that loads your images in your states resolve function. Then you dont have to worry about sending ready events to phantom. 😉 To use this with a loading bar:

      $rootScope.$on('$stateChangeStart', function () {
                ngProgress.start();
    
                  // or your:  cfpLoadingBar.start();
    
            });
      $rootScope.$on('$stateChangeSuccess', function () {
            ngProgress.complete();
    
                   // or your:  cfpLoadingBar.complete();
    
            });
    
    Login or Signup to reply.
  4. This issue is related to the Plugin as Anid Monsur said. You can solve the issue just upgrading angular-loading-bar to latest version or at least 7.0.1+.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search