jQuery Swiper script to run after Ng-Repeat elements are loaded

AndreaM16 picture AndreaM16 · May 21, 2015 · Viewed 8k times · Source

I'm making a web app using AngularJS, jQuery, HTML, CSS and Bootstrap, and I would like to pick some image links from my JSON that is located in an Apache2 server and use them in order to render those images in my main page. I would also like to swipe them like in a carousel. To make that work, I'm trying to use iDangero.us Swiper.

When I pick my images with 3 separated divs, I have no problems. I get my images and then I can normally swipe them as I want. I do it like shown below:

Main.html:

<div ng-init="initGetRequestMain()">

  <div class="swiper-slide" ng-click="swipers()" 
              style="{{data.background1}}"></div>
  <div class="swiper-slide" ng-click="swipers()" 
              style="{{data.background2}}"></div>
  <div class="swiper-slide" ng-click="swipers()" 
              style="{{data.background3}}"></div>

   <script src="scripts/custom/main/swipers.js"></script>
</div>

I use Swiper to swipe from one image to another, and it seems to work as it should. It's a jQuery plugin, and you can see some demos at this link.

Swipers.js:

angular.module('swipers', [])
       .controller('',[ 
        $(document).ready(function (){
           var swiper = new Swiper('.swiper-container',{
           direction: 'horizontal',
           pagination: '.swiper-pagination',
           paginationClickable: true
       })
})]);

Json:

"background1":"background-image: url(images/img1.jpg)",
"background2":"background-image: url(images/img2.jpg)",
"background3":"background-image: url(images/img3.jpg)"

mainController.js:

  myApp.controller('MainController', ["$scope","$http",                
                  function($scope,$http){

      $scope.initGetRequestMain = function(){

       $http.get('http://localhost/main.json').success(function(data){

         $scope.data=data;
       })
      }
  }]);

The problem is that when I try to use ng-repeat instead of 3 separated divs, I can't see them anymore and my Swiper script triggers before they are fully loaded. I have no errors in my console or in my JSON (validated with JSONLint). Below, I added 2 screenshots of my output in both situations.

Working with 3 separated divs:

Working with 3 separated divs

Not working with ng-repeat:

Not working with ng-repeat

This is the code where I try to make ng-repeat work keeping the same controller and the same Swiper script as before:

Main.html:

  <div ng-init="initGetRequestMain()">

       <div ng-repeat="slide in data.slides" isLoaded="">
           <div class="swiper-slide" style="{{slide.background}}" 
                       ng-click="swipers()"></div>
       </div>

    <script type="text/javascript-lazy" src="scripts/custom/main/swipers.js"></script>
  </div>

mainJson.json:

"slides":[
            {"background":"background-image:url('images/img1.jpg')"},
            {"background":"background-image:url('images/img2.jpg')"},
            {"background":"background-image: url('images/img3.jpg')"}
],

In order to get my images loaded before triggering the script, I'm trying to use 2 custom directives.

isLoaded tells me when the last ng-repeat element is loaded and sets pageIsLoaded = true;:

myApp.directive('isLoaded', function (){

   return{
     scope:true,
     restrict: 'A', //Attribute type
     link: function (scope, elements, arguments){ 

        if (scope.$last === true) {
            scope.pageIsReady = true;
            console.log('page Is Ready!');
         }
     }   
   }
})

gettingTheScript waits for pageIsLoaded = true; and loads the script:

myApp.directive('src', function (){

     return{
       scope:true,
       restrict: 'A', //Attribute type
       link: function (scope, elements, arguments){

            scope.$on(pageIsReady===true, function(){
                   if (attr.type === 'text/javascript-lazy'){

                       scope.scriptLink = arguments.src;
                    }
             })
         },
         replace: true, //replaces our element 
         template: '{{scriptLink}}'
       }
  })   

They do not seem to fix my problem. I also can't see console.log('page Is Ready!'); when making the first one.

I'm just having some troubles when I have to trigger a script like Swiper after the page is loaded in order to avoid these kind of problems. My images seem to have no height. I think that the problem is caused by ng-repeat not fully loading before triggering my script.

What am I doing wrong? Are there better solutions?

Answer

Joe Enzminger picture Joe Enzminger · May 31, 2015

Before getting into how to make this work, most of these types of jQuery plugins don't work well with angular since they make modifications to the DOM that angular doesn't know about.

That being said, the trick is deferring the invocation of the jQuery plugin until after Angular has rendered the DOM.

First, put your swiper plugin into a directive:

.directive('swiper', function($timeout) {
    return {
        link: function(scope, element, attr) {
            //Option 1 - on ng-repeat change notification
            scope.$on('content-changed', function() {
                new Swiper(element , {
                    direction: 'horizontal',
                    pagination: '.swiper-pagination',
                    paginationClickable: true);
            }
            //Option 2 - using $timeout
            $timeout(function(){
                new Swiper(element , {
                    direction: 'horizontal',
                    pagination: '.swiper-pagination',
                    paginationClickable: true);
            });

        }
    };
})

For Option 1, you need a modified isLoaded directive

myApp.directive('isLoaded', function (){

   return{
     scope:false, //don't need a new scope
     restrict: 'A', //Attribute type
     link: function (scope, elements, arguments){ 

        if (scope.$last) {
            scope.$emit('content-changed');
            console.log('page Is Ready!');
        }
     }   
   }
})

Finally, your markup (note the change from isLoaded to is-loaded....this is probably the reason you weren't seeing the notification).

<div ng-init="initGetRequestMain()" swiper>

   <div ng-repeat="slide in data.slides" is-loaded>
       <div class="swiper-slide" style="{{slide.background}}" 
                   ng-click="swipers()"></div>
   </div>
</div>

As mentioned, most jQuery plugins that do carousel functionality don't handle changes to the DOM (i.e new elements) very well. Even with both options, you may see unexpected things if you change your model after the ng-repeat is first rendered. However, if your model is static, this should work for you. If your model changes, then you might want to search for a more "angular" carousel directive.