Lazy loading Angular views and controllers on page scroll

TikaL13 picture TikaL13 · Dec 5, 2013 · Viewed 14.8k times · Source

I have a microsite that is utilizing Laravel and Angular. It's a one page microsite that is responsive and is broken into 5 sections. I would like to lazy load them to cut down on loading all at once.

<body ng-app>

 <div id="wrapper">
  <section id="intro">1</section>
  <section id="Second">2</section>
  <section id="Third">3</section>
  <section id="Fourth">4</section>
  <section id="Fifth">5</section>
 </div>

</body>

I'm looking to load 1 & 2 on page load then as you scroll down the page load the other view with a nice fade in and then load its interactive items.

Answer

m59 picture m59 · Dec 6, 2013

In this case it is probably not necessary (or efficient) to lazy load your controllers, but it can be done.

There are many things to tackle here, so I'm going to handle it in sections.

Lazy-loading views on scroll (animated).

Live demo here (click).

Markup:

<div class="container">
  <section
    ng-repeat="section in loadedSections"
    ng-include="section+'.html'"
    scroll-load
    scroll-load-from="sections"
    scroll-load-to="loadedSections"
    ng-animate="{enter:'section-animate-enter'}"
  ></section>
</div>

Animation CSS:

.section-animate-enter {
  -webkit-transition: 1.5s linear all;
    transition: 1.5s linear all;
    opacity: 0;
    left: 100%;
}
.section-animate-enter.section-animate-enter-active {
    opacity: 1;
    left: 0;
}

Angular logic:

app.controller('myCtrl', function($scope) {
  $scope.sections = ['top','mid','bottom']; //html files to load (top.html, etc)
  $scope.loadedSections = [$scope.sections[0]]; //loaded html files
});

app.directive('scrollLoad', function($compile) {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      var to = scope[attrs.scrollLoadTo]; //$scope.loadedSections
      var from = scope[attrs.scrollLoadFrom]; //$scope.sections

      $window = angular.element(window);
      $window.bind('scroll', function(event) {
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
        var scrollPos = scrollTop + document.documentElement.clientHeight;
        var elemBottom = element[0].offsetTop + element.height();
        if (scrollPos >= elemBottom) { //scrolled to bottom of scrollLoad element
          $window.unbind(event); //this listener is no longer needed.
          if (to.length < from.length) { //if there are still elements to load
            //use $apply because we're in the window event context
            scope.$apply(to.push(from[to.length])); //add next section
          }
        }
      });
    }
  };
});

Lazy-loading CONTROLLERS and views on scroll (animated).

Live demo here (click).

Markup:

<div class="container">
  <!-- the "lazy" directive will get the controller first, then add ng-include -->
  <section
    ng-repeat="section in loadedSections"
    lazy="section"
    scroll-load
    scroll-load-from="sections"
    scroll-load-to="loadedSections"
    ng-animate="{enter:'section-animate-enter'}"
  ></section>
</div>

Angular Logic:

var $appControllerProvider; //see below

var app = angular.module('myApp', []);

app.config(function($controllerProvider) {
  $appControllerProvider = $controllerProvider; //cache this so that we can lazy load controllers
});

app.controller('myCtrl', function($scope) {
  $scope.sections = ['top','mid','bottom']; //html files to load (top.html, etc)
  $scope.loadedSections = [$scope.sections[0]]; //loaded html files
});

app.directive('scrollLoad', function($compile) {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      var to = scope[attrs.scrollLoadTo]; //$scope.loadedSections
      var from = scope[attrs.scrollLoadFrom]; //$scope.sections

      $window = angular.element(window);
      $window.bind('scroll', function(event) {
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
        var scrollPos = scrollTop + document.documentElement.clientHeight;
        var elemBottom = element[0].offsetTop + element.height();
        if (scrollPos >= elemBottom) { //scrolled to bottom of scrollLoad element
          $window.unbind(event); //this listener is no longer needed.
          if (to.length < from.length) { //if there are still elements to load
            //use $apply because we're in the window event context
            scope.$apply(to.push(from[to.length])); //add next section
          }
        }
      });
    }
  };
});

app.factory('myService', function($http, $q) {
  return {
    getController: function(fileName) {
      return $http.get(fileName+'.js').then(function(response) {
        return response.data;
      });
    }
  }
});

app.directive('lazy', function(myService, $compile, $q) {
  /* I store the directive in a variable then return it later
   * so that I can abstract directive logic into other functions below */
  var directiveReturn = {
    restrict: 'A',
    link: function(scope, element, attrs) {
      var loadName = scope.$eval(attrs.lazy);

      //this is straightforward - see the "addScript" function for explanation
      myService.getController(loadName).then(function(js) {
        return addScript(loadName, js, scope);
      }).then(function() {
        //the controller has been lazy loaded into angular
        //now use "ng-include" to lazy load the view.
        var ngInc = angular.element('<span></span>')
          .attr('ng-include', "'"+loadName+".html'")
          .attr('ng-controller', loadName+'Ctrl');
          element.append(ngInc);
          $compile(ngInc)(scope);
      });
    } //link
  }; //directive return

  /*
   * This is the magic.
   */
  var scriptPromises = {};
  function addScript(loadName, js, scope) {
    if (!scriptPromises[loadName]) { //if this controller hasn't already been loaded
      var deferred = $q.defer();
      //cache promise (which caches the controller when resolved)
      scriptPromises[loadName] = deferred.promise;

      //inject controller into a script tag
      var script = document.createElement('script');
      script.src = 'data:text/javascript,' + encodeURI(js);
      script.onload = function() {
        //this is how you lazy load a controller
        $appControllerProvider.register(loadName, window[loadName+'Ctrl']);
        //now that the controller is registered with angular, resolve the promise
        //then, it is safe to add markup that uses this controller with ng-controller
        scope.$apply(deferred.resolve());
      };
      //when this script loads, the controller will be registered and promise is resolved
      document.body.appendChild(script);
      return deferred.promise;
    }
    else { //controller already loaded
      return scriptPromises[loadName]; //use cached controller
    }
  }
  return directiveReturn;
});