0

I'm trying to get better understanding of how to code in Angular properly and am writing my own custom module to better understand how things are supposed to work.

I have an HTML markup consisting of images and a directive i.e.

<img ng-if="!post.tooBig" mydirective resize="0" ng-src="/img/@{{post.Content}}">

My directive is here:

.directive('mydirective', [
           '$animate','$mediaStack',
  function($animate,$mediaStack) {
  return {
    restrict: 'A',
    compile: function(tElement, tAttrs) {
        return loader;
    }
  };

    function loader(scope, element, attrs) {
      var source = attrs.ngSrc;
      var tooLoadBig = parseInt(attrs.resize);
      if(tooLoadBig){
        var bigImage = new Image();
        bigImage.src = source.replace("small.",".");
      }
      }
}])

The idea is this: if the image has small appended to its filename, I know it is a thumbnail. I want to load it's big version (same file without the appended small) in the background so it is ready for a lightbox.

This works fine as is, but the problem is because I'm doing all the work in the compile, when I set bigImage.src = source.replace("small.","."); it fires off right away, and if I have many small images on the page, it causes the page to slow down because of all the loading that is going on.

I want to therefore use $q to make it so that it will load one image at a time.

So move

 var bigImage = new Image();
 bigImage.src = source.replace("small.",".");

Into a promise. Is it best practice to do this in the directive? My understanding is it wouldn't make sense to do so and that I should use a service, but I'm not sure how to do that. I could play around with it more but I was hoping someone with more experience could instruct me as to best-practices for something like this and a code sample of a similar workflow, thank you.

Edits:

Directive:

.directive('mydirective', [
           '$animate','$mediaStack',
  function($animate,$mediaStack) {
  return {
    restrict: 'A',
    compile: function(tElement, tAttrs) {
        return loader;
    }
  };

    function loader(scope, element, attrs) {
      var source = attrs.ngSrc;
      var tooLoadBig = parseInt(attrs.resize);
      if(tooLoadBig){
        /*var bigImage = new Image();
        bigImage.src = source.replace("small.",".");*/
        $mediaStack.load(source);
      }
      }
}])

My service:

.factory('$mediaStack', [
             '$animate', '$timeout', '$document', '$compile', '$rootScope',
             '$q',
             '$injector',
    function($animate ,  $timeout ,  $document ,  $compile ,  $rootScope ,
              $q,
              $injector) {
      var OPENED_MEDIA_CLASS = 'media-open';
      var theImages = [];
      $mediaStack = {};
      $mediaStack.load = function(source){
          theImages.push(source);
      };
      $mediaStack.loadRequest   = function(theImages) {

                          deferred.resolve(function(){
                          var bigImage = new Image();
                          bigImage.src = theImages[0].replace("small.",".");
                          });
                          return deferred.promise;
                        }

                        /*var promise = asyncGreet('Robin Hood');
                        promise.then(function(greeting) {
                          alert('Success: ' + greeting);
                        }, function(reason) {
                          alert('Failed: ' + reason);
                        }, function(update) {
                          alert('Got notification: ' + update);
                        });*/
                        //above from docs
      }
      return $mediaStack;
    }])

This works in that it gets the image urls into an array in the service, so I have all the images in the array, how do I use $q properly based off that array. I got started but most of the $mediaStack.loadRequest is based off the $q documentation, I'm not sure how to use it effectively in this case.

EDIT 2:

My service as is:

    .factory('$mediaStack', [
                 '$animate', '$timeout', '$document', '$compile', '$rootScope',
                 '$q',
                 '$injector',
        function($animate ,  $timeout ,  $document ,  $compile ,  $rootScope ,
                  $q,
                  $injector) {
           var OPENED_MEDIA_CLASS = 'media-open';
      var theImages = [];
      var theLoadedImages = [];
      var thePromises = [];
      var loading = false;
      $mediaStack = {};
      $mediaStack.load = function(source){
          theImages.push(source);
          var mainDeferred = $q.defer();
         if(loading)
         {
             thePromises.push(mainDeferred.promise);
             console.log(thePromises);
             $mediaStack.myRaceFn(thePromises).then(function() { 
             console.log("Fire!");
             loading=true;
            $mediaStack.loadRequest(theImages[0]).then(function(bigImage){ 
          console.log(bigImage);
            theImages.shift();
            theLoadedImages.push(bigImage);
            loading = false;
          mainDeferred.resolve(); 
        });
      });
         }
         if(!loading)
         {
            loading = true;
            $mediaStack.loadRequest(theImages[0]).then(function(bigImage){
            console.log(bigImage);
            theImages.shift();
            theLoadedImages.push(bigImage);
            loading = false;
            mainDeferred.resolve();         
            });

         }
           }
      $mediaStack.loadRequest = function(source){
                          var deferred = $q.defer();
                          var bigImage = new Image();
                          bigImage.src = source.replace("small.",".");
                          bigImage.onload = function() {
                          deferred.resolve(bigImage);
                          }
                           return deferred.promise;
      }
      //.race implementation in old angular
      $mediaStack.myRaceFn = function (promises){
   return $q(function(resolve, reject) { 
     promises.forEach(function(promise) {
       console.log(promise);
       promise.then(resolve, reject);
     });
   });
}
//.race implementation in old angular
      return $mediaStack;
        }])

I'm very close, I see an array of promises but I never get to the point of being able to fire them again, so I have 10 images, and I get an array of 9 promises. How do I fire them?

I expected it would happen around here:

$mediaStack.myRaceFn(thePromises).then(function() { 
console.log("Fire!");
loading=true;
$mediaStack.loadRequest(theImages[0]).then(function(bigImage){ 

But I never get that console.log().

Instead I get console messages in $mediaStack.myRaceFn() showing me each promise (9 at the end) but they just sit there? I'm missing something.

I think I may be resolving mainDeferred too early...

Summer Developer
  • 2,056
  • 7
  • 31
  • 68

2 Answers2

1

I think, that your hunch is good. You need a service to handle requests for your images. Remember that browsers are limited to send multiple concurrent requests to single server/proxy (Max parallel http connections in a browser?)

In theory you could create a promise and expose it thru two way bindings or even drive loading with an attribute. Something like:

HTML:

<img mydirective="booleanValue" />

In mydirective:

$scope.$watch('mydirective', function (newVal, oldVal) {
  if (newVal === true) {
    //perform the load
  }
});

But I think, that a service would be better. You could decide what and when to load, from that single point. It wouldn't be limited to UI component, you could load bigger images not only from directives context. So in the service the essential code would be:

module.factory('$mediaStack', ['$q', function($q) { //first of all you don't need all of these dependencies, keep the list short
  var imgDict = {}; //Image dictionary where we're going to keep promises and loaded images
  var loadingNow = 0;

  function loadImage(img) {
    //Send request and return promise
    var deferred = $q.defer();
    var bigImage = new Image();

    bigImage.onload = function() {
      deferred.resolve(bigImage);
    }

    bigImage.src = source.replace("small.", ".");

    return deferred.promise;
  }

  var $mediaStack = function(img) {
    var deferred = $q.defer(); //the main deferred task

    if (loadingNow > 4) { //if we're loading 5 images or more, defer the task untill one of the Images is loaded
      var promises = []; //an array of promises, list of images which are loading now
      Object.keys(imgDict).forEach(function(key) {
        if (!(imgDict[key] instanceof Element)) { //Check if the item from dictionary is a promise or loaded image
          promises.push(imgDict[key]); //if it's a promise, add it to the list
        }
      });
      $q.race(promises).then(function() { //use the list, to create a race: when one of the promises resolves (image is loaded) we can fire our function again (we probably have less than 5 Images loading at the same time)
        $mediaStack(img).then(function(data) { //call the function again
          deferred.resolve(data); //as we return a promise form that function we have to resolve main deferred object
        });
      });
    }

    if (!(img in imgDict)) { //when the image is not loaded yet
      loadingNow++; //increase the number of images being loaded
      imgDict[img] = loadImage(img).then(function(imgDOM) { //and load the image
        imgDict[img] = imgDOM;
        deferred.resolve(imgDOM); //once it's loaded resolve the main deferred task
        loadingNow--;
      });
    } else {
      deferred.resolve(imgDict[img]);
    }

    return deferred.promise; //promise to the main deferred task
  }

  return $mediaStack;
}]);

The general idea behind promises and deferred tasks: Deferred object is a deferred task, which has a Promise parameter - a entry point of a callback, which is going to be invoked once the deferred task is completed.

Check $q docs for more: angular $q

And how to write a service: https://docs.angularjs.org/guide/services

What is Promise? The general idea and native API

Hope it helps.

Community
  • 1
  • 1
Oskar
  • 2,548
  • 1
  • 20
  • 22
  • @SummerDeveloper yeah, check out my `Service` function, that's basically what you have to do in order to limit number of requests. – Oskar Apr 01 '17 at 00:21
  • I don't really understand your code. Could you wrap service and directive parts? Also the `$q.race` part, if I had another promise somewhere would that trigger it? It seems separate from `deferred` – Summer Developer Apr 01 '17 at 00:39
  • @SummerDeveloper that's ok, it was late for me when I was adding the answer. I updated it, hopefully it will make more sense for you now. – Oskar Apr 01 '17 at 10:08
  • I'm getting close, but where are the promises added to either the `promises` array or the `imgDict ` object? I see where elements are added, not promises, so in my code I get errors because I expect promises. – Summer Developer Apr 01 '17 at 19:16
  • Thanks, this helped me figure it out! – Summer Developer Apr 01 '17 at 21:51
1

Marking Oskar as accepted because it basically allowed me to figure out a solution.

Here is what I actually implemented, however:

    .factory('$mediaStack', [
             '$animate', '$timeout', '$document', '$compile', '$rootScope',
             '$q',
             '$injector',
    function($animate ,  $timeout ,  $document ,  $compile ,  $rootScope ,
              $q,
              $injector) {
      var OPENED_MEDIA_CLASS = 'media-open';
      var theImages = [];
      var theLoadedImages = [];
      var thePromises = [];
      var loading = false;
      $mediaStack = {};
      $mediaStack.load = function(source){
          if(source)
          {
          theImages.push(source);
          }
          var mainDeferred = $q.defer();
         if(loading)
         {
             thePromises.push(mainDeferred.promise);
             console.log(thePromises);
             $mediaStack.myRaceFn(thePromises).then(function() { 
             console.log("Fire!");
             loading=true;
            $mediaStack.loadRequest(theImages[0]).then(function(bigImage){ 
          console.log(bigImage);
            theImages.shift();
            theLoadedImages.push(bigImage);
            loading = false;
          //mainDeferred.resolve(); 
        });
      });
         }
         if(!loading)
         {
            loading = true;
            $mediaStack.loadRequest(theImages[0]).then(function(bigImage){
            console.log(bigImage);
            theImages.shift();
            theLoadedImages.push(bigImage);
            loading = false;
            if(theImages.length>0)
            {
            $mediaStack.load();     
            };          
            });

         }
           }
      $mediaStack.loadRequest = function(source){
                          var deferred = $q.defer();
                          var bigImage = new Image();
                          bigImage.src = source.replace("small.",".");
                          bigImage.onload = function() {
                          deferred.resolve(bigImage);
                          }
                           return deferred.promise;
      }
      //.race implementation in old angular
      $mediaStack.myRaceFn = function (promises){
   return $q(function(resolve, reject) { 
     promises.forEach(function(promise) {
       console.log(promise);
       promise.then(resolve, reject);
     });
   });
}
//.race implementation in old angular
      return $mediaStack;
    }])
Summer Developer
  • 2,056
  • 7
  • 31
  • 68