13

I'm using Angular-UI's carousel and I need to tell my google charts to redraw after they have slid into view. In spite of what I've read, I can't seem to hook into the event.

See my attempt: http://plnkr.co/edit/Dt0wdzeimBcDlOONRiJJ?p=preview

HTML:

<carousel id="myC" interval="myInterval">
  <slide ng-repeat="slide in slides" active="slide.active">
    <img ng-src="{{slide.image}}" style="margin:auto;">
    <div class="carousel-caption">
      <h4>Slide {{$index}}</h4>
      <p>{{slide.text}}</p>
    </div>
  </slide>
</carousel>

On document load:

$('#myC').live('slid.bs.carousel', function (event) { console.log("slid"); } );

It should work something like this: http://jsfiddle.net/9fwuq/ - non-angular-ui carousel

Perhaps there is a more Angular way to hook into the fact that my chart has slid into view?

Zanon
  • 29,231
  • 20
  • 113
  • 126
jduprey
  • 1,176
  • 1
  • 9
  • 9
  • You should be able to use a directive to hook in to the bootstrap event. I'll give it a go after lunch and see if it can work. – DigitalZebra Jul 11 '14 at 16:26
  • So I took a further look at this, and the reason why you aren't getting the event is because BootstrapUI doesn't use the Bootstrap JavaScript for the carousel widget. So, nothing is even firing the event. – DigitalZebra Jul 11 '14 at 21:52

6 Answers6

36

There are 3 ways I can think of and that depends of your requirement.

Please see http://plnkr.co/edit/FnI8ZX4UQYS9mDUlrf6o?p=preview for examples.

  1. use $scope.$watch for an individual slide to check if it is become active.

    $scope.$watch('slides[0].active', function (active) {
      if (active) {
        console.log('slide 0 is active');
      }
    });
    
  2. use $scope.$watch with custom function to find an active slide.

    $scope.$watch(function () {
      for (var i = 0; i < slides.length; i++) {
        if (slides[i].active) {
          return slides[i];
        }
      }
    }, function (currentSlide, previousSlide) {
      if (currentSlide !== previousSlide) {
        console.log('currentSlide:', currentSlide);
      }
    });
    
  3. use a custom directive to intercept select() function of the carousel directive.

    .directive('onCarouselChange', function ($parse) {
      return {
        require: 'carousel',
        link: function (scope, element, attrs, carouselCtrl) {
          var fn = $parse(attrs.onCarouselChange);
          var origSelect = carouselCtrl.select;
          carouselCtrl.select = function (nextSlide, direction) {
            if (nextSlide !== this.currentSlide) {
              fn(scope, {
                nextSlide: nextSlide,
                direction: direction,
              });
            }
            return origSelect.apply(this, arguments);
          };
        }
      };
    });
    

    and use it like this:

    $scope.onSlideChanged = function (nextSlide, direction) {
        console.log('onSlideChanged:', direction, nextSlide);
    };
    

    and in html template:

    <carousel interval="myInterval" on-carousel-change="onSlideChanged(nextSlide, direction)">
    ...
    

Hope this help : )

runTarm
  • 11,537
  • 1
  • 37
  • 37
  • Well that helped tremendously! Thank you. – jduprey Jul 14 '14 at 15:12
  • Using directive (third option) is more effective if you have more than one carousel in the same page. – Fran Verona Feb 09 '15 at 12:01
  • Unfortunately these methods don't help in catching the slide change event after the animation transition is finished that is triggerred by the event slid.bs.carousel in the original bootstrap library... I'm still looking for how to catch that in angular. – eloone Sep 06 '15 at 21:45
6

AngularUI Bootstrap has changed naming conventions for controllers as thery have prefixed all of their controllers with prefix uib, so below is the updated solution of the original solution provided by runTarm:

Angular:

.directive('onCarouselChange', function($parse) {
    return {
        require: '^uibCarousel',
        link: function(scope, element, attrs, carouselCtrl) {
            var fn = $parse(attrs.onCarouselChange);
            var origSelect = carouselCtrl.select;
            carouselCtrl.select = function(nextSlide, direction, nextIndex) {
                if (nextSlide !== this.currentSlide) {
                    fn(scope, {
                        nextSlide: nextSlide,
                        direction: direction,
                        nextIndex: this.indexOfSlide(nextSlide)
                    });
                }
                return origSelect.apply(this, arguments);
            };
        }
    };
});

Angular with TypeScript:

module App.Directive {

    export class CarouselChange implements ng.IDirective {

        public require: string = '^uibCarousel';

        constructor(private $parse: ng.IParseService) { }

        public link: ng.IDirectiveLinkFn = (scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: any, carouselCtrl: any) => {
            var fn = this.$parse(attributes.carouselChange);
            var origSelect = carouselCtrl.select;
            carouselCtrl.select = function(nextSlide, direction) {
                if (nextSlide !== this.currentSlide) {
                    fn(scope, {
                        nextSlide: nextSlide,
                        direction: direction
                    });
                }
                return origSelect.apply(this, arguments);
            };
        }

        static Factory(): ng.IDirectiveFactory {
            var directive: ng.IDirectiveFactory = ($parse: ng.IParseService) => new CarouselChange($parse);
            directive['$inject'] = ["$parse"];
            return directive;
        }
    }
}

Thanks,

Manish Kumar
  • 1,131
  • 15
  • 28
  • 1
    This doesn't work for me; I get `TypeError: this.indexOfSlide is not a function`. – David Rhoderick Apr 27 '16 at 18:46
  • This works for the select function but for some strange reason, not the next or prev functions. it should work all the same as far as I can see, any chance you know of a workaround for that? I need to know when they hit the next, not when the slide actually changes. http://stackoverflow.com/questions/39295918/ui-bootstrap-control-uib-carousel-bind-to-next-event – Shaggy13spe Sep 03 '16 at 16:05
3

Following the answer given by runTarm If you want to know the index of the next slide, you should add something like this:

 .directive('onCarouselChange', function ($parse) {
    return {
      require: 'carousel',
          link: function (scope, element, attrs, carouselCtrl) {
            var fn = $parse(attrs.onCarouselChange);
            var origSelect = carouselCtrl.select;
            carouselCtrl.select = function (nextSlide, direction,nextIndex) {
      if (nextSlide !== this.currentSlide) {
        fn(scope, {
          nextSlide: nextSlide,
          direction: direction,
          nextIndex:this.indexOfSlide(nextSlide)
        });
      }
      return origSelect.apply(this, arguments);
    };
  }
  };
})

Then, in the controller you just need to do this to catch the new index:

$scope.onSlideChanged = function (nextSlide, direction, nextIndex) {
    console.log(nextIndex);
}
Lu Chinke
  • 622
  • 2
  • 8
  • 27
1

I managed to modify runTarm's answer so that it calls the callback once the slide has finished sliding into view (i.e. the sliding animation has finished). Here's my code:

.directive('onCarouselChange', function ($animate, $parse) {
    return {
        require: 'carousel',
        link: function (scope, element, attrs, carouselCtrl) {
            var fn = $parse(attrs.onCarouselChange);
            var origSelect = carouselCtrl.select;
            carouselCtrl.select = function (nextSlide, direction) {
                if (nextSlide !== this.currentSlide) {
                    $animate.on('addClass', nextSlide.$element, function (elem, phase) {
                        if (phase === 'close') {
                            fn(scope, {
                                nextSlide: nextSlide,
                                direction: direction,
                            });
                            $animate.off('addClass', elem);
                        }
                    });
                }
                return origSelect.apply(this, arguments);
            };
        }
    };
});

The secret lies in using $animate's event handler to call our function once the animation is finished.

R. Kazeno
  • 111
  • 1
  • 2
  • 4
  • This also doesn't work, although I'm not sure it is because I am using a later version of Twitter Bootstrap for Angular (I need to use `uib`). I get `TypeError: Cannot read property '0' of undefined` on the first $animate line. – David Rhoderick Apr 27 '16 at 18:53
  • 2
    OK, this is I think a change to Angular because I am using 1.5.5; you need to remove the `$` from `nextSlide.$element` so it becomes `nextSlide.element`. I think that's all I had to change for this to work. – David Rhoderick Apr 27 '16 at 19:26
0

here's an alternate method that uses controllers, somewhere between runTarm's #2 and #3.

original HTML + a new div:

<carousel id="myC" interval="myInterval">
  <slide ng-repeat="slide in slides" active="slide.active">
    <div ng-controller="SlideController"> <!-- NEW DIV -->
      <img ng-src="{{slide.image}}" style="margin:auto;">
      <div class="carousel-caption">
        <h4>Slide {{$index}}</h4>
        <p>{{slide.text}}</p>
      </div>
    </div>
  </slide>
</carousel>

the custom controller:

  .controller('SlideController',
    function($log, $scope) {
      var slide = $scope.slide;
      $scope.$watch('slide.active', function(newValue) {
        if (newValue) {
          $log.info("ACTIVE", slide.id);
        }
      });
    });
ionous
  • 3
  • 2
0

And if you just want to start playing a video when the slide comes into view, and pause when it leaves:

JS

{# Uses angular v1.3.20 & angular-ui-bootstrap v0.13.4 Carousel #}
{% addtoblock "js" %}<script type="text/javascript">
angular.module('videoplay', []).directive('videoAutoCtrl', function() {
  return {
    require: '^carousel',
    link: function(scope, element, attrs) {
      var video = element[0];
      function setstate(visible) {
        if(visible) {
          video.play();
        } else {
          video.pause();
        }
      }
      // Because $watch calls $parse on the 1st arg, the property doesn't need to exist on first load
      scope.$parent.$watch('active', setstate);
    }
  };
});
</script>{% endaddtoblock %}
{% addtoblock "ng-requires" %}videoplay{% endaddtoblock %}

NOTE: Has additional bits for Django

HTML:

<carousel interval="15000">
  <slide>
   <video class="img-responsive-upscale" video-auto-ctrl loop preload="metadata">
    <source src=
...
Unheilig
  • 16,196
  • 193
  • 68
  • 98
racitup
  • 416
  • 4
  • 11