Search code examples
javascriptangularjsangularjs-directiveangularjs-scopestellar.js

Angular.js directive using stellar.js used multiple times - only the first one works


I wrote an angular.js directive which uses stellar.js for parallax effects. When I call the same directive multiple times one after another, stellar.js will only work on the first one. I read about $.stellar('refresh'); which should re-initialize stellar after injecting elements in the DOM, but this had no effect for me. Also calling stellar on the window had no effect.

Here is the code of my directive:

angular.module('ClientApp')
    .directive('parallaxModule', function () {
        return {
            templateUrl: 'views/parallax-module.html',
            restrict: 'E',
            scope: {
                data: '='
            },
            controller: function() {
               $(function(){
                   $(this).stellar({
                        horizontalScrolling: false,
                        verticalScrolling: true,
                        verticalOffset: 50
                   });
                });
            }
        };
    });

This is how I use the directive:

<parallax-module data="sectionData.para"></parallax-module>
<parallax-module data="sectionData.para"></parallax-module>

And here's my template:

<div class="parallax-module" data-stellar-background-ratio="0.2">
    <div class="wrapper" data-stellar-ratio="2.5">
        <div class="title">{{data.title}}</div>
        <div class="caption">{{data.caption}}</div>
    </div>
</div>

Here's a plunker: http://plnkr.co/edit/TJbPL3dhSsiitZQWm9Qe?p=preview


Solution

  • $timeout works because the stellar plugin is supposed to be applied, not to each parallax element, but to the scrolling container. In the example in the docs it is applied to the window: $(window).stellar();

    And so, with $timeout, the code "waits" the end of the digest queue and when it is executed, all the directives are loaded. Unfortunately, it is executed for each directive unnecessarily.

    The solution is to apply the directive to the container. Then it becomes as simple as:

    .directive("parallaxContainer", function(){
      return {
        link: function(scope, element){
          element.stellar({
              horizontalScrolling: false,
              verticalScrolling: true,
              verticalOffset: 0
            });
        }
      }
    })
    

    This solution relies on there being a stellar-compatible element (with data-stellar-background-ratio, for example) in that container:

    <body parallax-container>
      <div data-stellar-background-ratio="0.2">
        ...
      </div>
    
      <div data-stellar-background-ratio="0.2">
        ...
      </div>
    </body>
    

    plunker

    EDIT:

    The above code would not work if the parallax elements are loaded dynamically. This complicates things a bit, but not much. In this arrangement, we'd need the container directive responsible for triggering the plugin, and the actual parallax elements directives that would register themselves with the container and notify the container when they have loaded.

    The element directive could use ng-include to get a notification when content has loaded. This directives uses require: "^parallaxContainer".

    app.directive('parallaxModule', function() {
        return {
          template: '<div ng-include="\'parallax-module.html\'" onload="notifyLoaded()"></div>',
          restrict: 'E',
          scope: {
            data: '='
          },
          require: "^parallaxContainer",
          link: {
            pre: function(scope, element, attrs, parallaxContainer){
              parallaxContainer.registerElement();
              scope.notifyLoaded = parallaxContainer.notifyElementLoaded;
            }
          }
        };
      })
    

    The parallaxContainer directives adds a controller that exposes the methods to register and notify. And it triggers the plugin when all the children have loaded

    app.directive("parallaxContainer", function() {
        var parallaxElementCount = 0;
        var parallaxElementLoaded = 0;
        return {
          controller: function($element){
    
            this.registerElement = function(){
              parallaxElementCount++;
            };
    
            this.notifyElementLoaded = function(){
              parallaxElementLoaded++;
    
              if (parallaxElementCount !== 0 && parallaxElementCount === parallaxElementLoaded){
    
                $element.stellar({
                  horizontalScrolling: false,
                  verticalScrolling: true,
                  verticalOffset: 0
                });
              }
            }
          },
        }
      })
    

    plunker

    Note that stellar doesn't work (for some reason - perhaps there is some info on this in stellar docs) when it is activated more than once, so once triggered, loading more child elements won't change anything.