Lazy loading Angular views and controllers on page scroll
Asked Answered
C

1

6

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.

Catoptrics answered 5/12, 2013 at 20:52 Comment(0)
I
12

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;
});
Iniquity answered 6/12, 2013 at 0:51 Comment(10)
@m59... thank you for this explanation. I did though have a problem viewing the demo that you created.Catoptrics
@Catoptrics the demo is working great for me. Plunkr does occasionally have some loading issues, but it should generally be fine. I use Chrome to be safe.Iniquity
What would be a good and productive folder structure for this type of build? I'm using Laravel as the back-end so these sections are actually blade templates. I'm guessing that with this approach using Laravel's blade templating system wouldn't be ideal here.Catoptrics
@Catoptrics you can make it look however you want! Just think on how you can best abstract details. You might want not load the files as directly as I am, and instead abstract that away to something else...you could even setup a server operation to handle template things for you like $http.get('templates-api/get/me/this); and everything in between...Iniquity
One last question. If I wanted to change the URL as I scroll down to these other sections is that feature doable with this set up?Catoptrics
@Catoptrics Sure, you can do anything you want. There are probably a few ways to do it...you might just want to use the $location service to add #section-name to the url. Some options might interfere with your routing, so I can't say for sure what would be the best thing to do.Iniquity
@m59... I guess that was an easy question to answer :). Would you know of any good online examples of this. The ones of seen don't really touch on this specific function.Catoptrics
Can we use the same thing on changing routes? As if we want to go to another page, and load this. How can this be done?Battleplane
@Iniquity solution not working in firefox even jsfiddle you can try it's not workingDross
@Sandeepvashisth The reason is that body.scrollTop is not cross platform. documentElement.scrollTop is the spec, but it isn't supported in Chrome for some reason. I updated the code and demos to support both.Iniquity

© 2022 - 2024 — McMap. All rights reserved.