AngularJS - 在 ng-repeat 完成后操作 DOM [英] AngularJS - Manipulating the DOM after ng-repeat is finished

查看:22
本文介绍了AngularJS - 在 ng-repeat 完成后操作 DOM的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在遍历数据后,我在操作 DOM 时遇到了一些问题.

我们有一个 jQuery 滑块插件,它与数据绑定并正常工作,但是在使用 ng-repeat 时,我们必须为其初始化用 $timeout 包装工作现在这甚至不起作用.

我认为使用 $timeout 是不可靠的,这是一个不好的修复.在 jQuery 中,我可以使用 $(document).ready() —这是可靠的,但使用 angular.element(document).ready() 似乎也不起作用.

slider指令被调用但是无法获取slider中图片的高度,因为图片还没有加载到DOM中—导致滑块的计算高度为 0.

我现在发现它非常令人沮丧 - 必须有一种方法可以在数据(例如在 ng-repeat 中)循环后操作 DOM.

滑块的初始化执行如下:

var sliderLoad = function () {$超时(函数(){var setHeight = elem.find('.slide:eq(0)').outerHeight(true);elem.css({高度:设置高度});}, 1000);//显示滑块导航按钮elem.parent().find('.direction-nav').show();};

…这是一个复制演示.

解决方案


Quick-'n-Dirty it (演示)

我们希望确保所有图像都已加载,因此让我们为此编写一个指令:

app.directive('loadDispatcher', function() {返回 {限制:'A',链接:函数(范围,元素,属性){element.bind('load', function() {scope.$emit('$imageLoaded');});}};})

…并将其附加到 ng-src 的元素上:

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

现在我们可以比较我们的模型捕获的事件数量,并根据需要采取行动:

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


解耦(演示)

这有点令人不安,因为它不遵守德米特法则 —任何将观看 $imageLoaded 事件的指令都必须了解模型 (scope.videos.objects.length).

我们可以通过在不明确寻址模型的情况下找出加载了多少图像来防止这种耦合.假设事件将在 ng-repeat 中处理.

  1. 确保 ng-repeat 已完成,并使用项目计数触发事件.我们可以通过附加一个具有唯一目的的控制器来做到这一点 - 观察 $last 属性.一旦找到(带有一个真值),我们将触发一个事件来通知它:

    .controller('LoopWatchCtrl', function($scope) {$scope.$watch('$last', function(newVal, oldVal) {newVal &&$scope.$emit('$repeatFinished', $scope.$index);});})

  2. 现在,捕获事件并相应地激活滑块初始化:

    var loadCount = 0,最后索引 = 0;scope.$on('$repeatFinished', function(event, data) {lastIndex = 数据;});scope.$on('$imageLoaded', function() {if (lastIndex && loadCount++ === lastIndex) {_initSlider(元素);//这是在任何地方定义的}});

现在我们的指令不必知道模型.但是,这有点麻烦,现在我们必须绑定一个指令一个控制器.


折叠(演示)

让我们将整个 shabang 提取到一个指令中:

app.directive('imageLoadWatcher', function($rootScope) {返回 {限制:'A',链接:函数(范围,元素,属性){if (typeof $rootScope.loadCounter === '未定义') {$rootScope.loadCounter = 0;}element.find('img').bind('load', function() {scope.$emit('$imageLoaded', $rootScope.loadCounter++);});},控制器:功能($范围){$scope.$parent.$on('$imageLoaded', function(event, data) {if ($scope.$last && $scope.$index === $rootScope.loadCounter - 1) {$scope.$emit('$allImagesLoaded');删除 $rootScope.loadCounter;}});}};});

…这将应用于 ng-repeated 元素:

现在我们可以简单地观看 $allImagesLoaded 例如在滑块中:

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


概括它(如果你愿意)(演示)

我们可以再次分解它并在应用范围内应用这种方法来为任何ng-repeat完成或ng-src 加载,这并不总是必要的(1),但可能非常有用.让我们看看如何:

  1. 修饰 ng-src 指令,以便在加载图像时发送事件:

    app.config(function($provide) {$provide.decorator('ngSrcDirective', function($delegate) {var 指令 = $delegate[0],链接 = 指令.链接;指令.编译=函数(){返回函数(范围,元素,属性){link.apply(this, arguments);element.bind('load', function() {scope.$emit('$imageLoaded');});};};返回 $delegate;});//...});

  2. 装饰 ng-repeat 以在完成时通知:

    app.config(function($provide) {//...$provide.decorator('ngRepeatDirective', function($delegate) {var 指令 = $delegate[0],链接 = 指令.链接;指令.编译 = 函数(){返回函数(范围,元素,属性){link.apply(this, arguments);scope.$watch('$$childTail.$last', function(newVal, oldVal) {newVal &&scope.$emit('$repeatFinished');});};};返回 $delegate;});});

  3. 现在可以在任何地方捕捉事件,例如在您的滑块指令中:

    var repeatFinished = false;var loadCount = 0;scope.$on('$repeatFinished', function() {重复完成 = 真;});scope.$on('$imageLoaded', function () {if (repeatFinished &&loadCount++ === scope.videos.objects.length - 1) {_initSlider();//这是在任何地方定义的}});

这似乎违背了初衷,因为我们又回到了原点,但它可能非常强大.还有—看,妈妈,没有新指令!

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


把袜子放进去(演示)


    TL; DR,给我一个演示!!!   
 


<小时>

1.必须仔细考虑装饰,因为结果将在应用程序加载的每个图像上发送一个事件.

<小时>

从 1.3.x 版本开始,将无法覆盖 link 函数.

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.

解决方案


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.

这篇关于AngularJS - 在 ng-repeat 完成后操作 DOM的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆