Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS - Manipulating the DOM after ng-repeat is finished

I'm experiencing some problems regarding manipulation of the DOM after looping through the data.

We have a jQuery slider plugin that is tied to data and works normally, but when using ng-repeat, we have to wrap its initialization with $timeout for it to work — and now that isn't even working.

I think using $timeout is unreliable, which makes for a bad fix. In jQuery I could use $(document).ready() — which was solid, but using angular.element(document).ready() doesn't seem to work either.

The slider directive is invoked but cannot obtain the height of the images in the slider because the images have not been loaded into the DOM — resulting in the slider having a calculated height of 0.

I'm finding it very frustrating at the moment - there must be a way to manipulate the DOM after data (in an ng-repeat for example) has cycled through.

The Initialization of the slider is performed as follows:

var sliderLoad = function () {
    $timeout(function () {
        var setHeight = elem.find('.slide:eq(0)').outerHeight(true);
        elem.css({
            height: setHeight
        });
    }, 1000);
    // Show the slider nav buttons
    elem.parent().find('.direction-nav').show();
};

… and here is a reproduction demo.

like image 558
30secondstosam Avatar asked Mar 13 '14 15:03

30secondstosam


People also ask

Does ng-repeat create a new scope?

Directives that Create Scopes In most cases, directives and scopes interact but do not create new instances of scope. However, some directives, such as ng-controller and ng-repeat, create new child scopes and attach the child scope to the corresponding DOM element.

What is the use of NG-repeat in AngularJS?

AngularJS ng-repeat Directive The ng-repeat directive repeats a set of HTML, a given number of times. The set of HTML will be repeated once per item in a collection. The collection must be an array or an object. Note: Each instance of the repetition is given its own scope, which consist of the current item.

Where is the last element in NG-repeat?

You can use $last variable within ng-repeat directive. Take a look at doc. Where computeCssClass is function of controller which takes sole argument and returns 'last' or null .


1 Answers


Quick-'n-Dirty it (Demo)

We would want to make sure that all images are loaded, so let's write a directive for that:

app.directive('loadDispatcher', function() {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            element.bind('load', function() {
                scope.$emit('$imageLoaded');
            });
        }
    };
})

… and attach it to the ng-src'd elements:

<img class="thumb-recipe" ng-src="{{ object.tile_url }}" load-dispatcher/>

Now we can compare the number of events caught with our model, and act as needed:

var loadCount = 0;
scope.$on('$imageLoaded', function () {
    if (loadCount++ === scope.videos.objects.length - 1) {
        _initSlider(); // act!
    }
});


Decouple it (Demo)

This is a little disturbing, as it's not adhering to the Law of Demeter — any directive that will watch the $imageLoaded event will have to know about the model (scope.videos.objects.length).

We can prevent this coupling by finding out how many images were loaded without explicitly addressing the model. Let's assume that the events will be handled in an ng-repeat.

  1. Make sure ng-repeat has finished, and fire an event with the items count. We can do that by attaching a controller with a sole purpose - to watch the $last property. Once it's found (with a truthy value), we will fire an event to notify about it:

    .controller('LoopWatchCtrl', function($scope) {
        $scope.$watch('$last', function(newVal, oldVal) {
            newVal && $scope.$emit('$repeatFinished', $scope.$index);
        });
    })
    
    <div ng-repeat="object in videos.objects" ng-controller="LoopWatchCtrl">
    
  2. Now, catch the events and activate the slider initialization accordingly:

    var loadCount = 0,
        lastIndex = 0;
    
    scope.$on('$repeatFinished', function(event, data) {
        lastIndex = data;
    });
    
    scope.$on('$imageLoaded', function() {
        if (lastIndex && loadCount++ === lastIndex) {
            _initSlider(element); // this is defined where-ever
        }
    });
    

There, now our directive does not have to know about the model. But, it's a bit cumbersome, now we have to tie a directive and a controller.


Fold it (Demo)

Let's extract this whole shabang into a single directive:

app.directive('imageLoadWatcher', function($rootScope) {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            if (typeof $rootScope.loadCounter === 'undefined') {
                $rootScope.loadCounter = 0;
            }
            element.find('img').bind('load', function() {
                scope.$emit('$imageLoaded', $rootScope.loadCounter++);
            });
        },
        controller: function($scope) {
            $scope.$parent.$on('$imageLoaded', function(event, data) {
                if ($scope.$last && $scope.$index === $rootScope.loadCounter - 1) {
                    $scope.$emit('$allImagesLoaded');
                    delete $rootScope.loadCounter;
                }
            });
        }
    };
});

… which will be applied to the ng-repeated element:

<div ng-repeat="object in videos.objects" class="slide" image-load-watcher>

Now we can simply watch $allImagesLoaded e.g. in the slider:

scope.$on('$allImagesLoaded', function() {
    _initSlider(element);
});


Generalize it (if you want to) (Demo)

We can break it down again and apply this approach app-wide to use event dispatching for any ng-repeat finish or ng-src load, which is not always necessary (1), but can be quite useful. Let's see how:

  1. Decorate the ng-src directive, so it sends an event when an image is loaded:

    app.config(function($provide) {
        $provide.decorator('ngSrcDirective', function($delegate) {
            var directive = $delegate[0],
                link = directive.link;
            directive.compile = function() {
                return function(scope, element, attrs) {
                    link.apply(this, arguments);
                    element.bind('load', function() {
                        scope.$emit('$imageLoaded');
                    });
                };
            };
    
            return $delegate;
        });
        // ...
    });
    
  2. Decorate ng-repeat to notify when it finishes:

    app.config(function($provide) {
        // ...
        $provide.decorator('ngRepeatDirective', function($delegate) {
            var directive = $delegate[0],
                link = directive.link;
            directive.compile = function() {
                return function(scope, element, attrs) {
                    link.apply(this, arguments);
                    scope.$watch('$$childTail.$last', function(newVal, oldVal) {
                        newVal && scope.$emit('$repeatFinished');
                    });
                };
            };
    
            return $delegate;
        });
    });
    
  3. Now can catch the events anywhere, e.g. in your slider directive:

    var repeatFinished = false;
    var loadCount = 0;
    
    scope.$on('$repeatFinished', function() {
        repeatFinished = true;
    });
    
    scope.$on('$imageLoaded', function () {
        if (repeatFinished && 
                    loadCount++ === scope.videos.objects.length - 1) {
            _initSlider(); // this is defined where-ever
        }
    });
    

It seems to defeat the purpose, as we're back to square one, but it can be very powerful. And also — look, mommy, no new directives!

<div ng-repeat="object in videos.objects" class="slide">
    <img class="thumb-recipe" ng-src="{{ object.tile_url }}"/>
</div>


Put a sock in it (Demo)


     TL;DR, just gimme tha demo ! ! !     
 



1. Decoration has to be considered carefully, as the result will be sending an event on each image loaded across the application.


Overriding the link function will not be possible in version 1.3.x onwards.

like image 57
Eliran Malka Avatar answered Oct 30 '22 22:10

Eliran Malka