使用 AngularJS 砌体

Posted

技术标签:

【中文标题】使用 AngularJS 砌体【英文标题】:Masonry with AngularJS 【发布时间】:2013-05-06 10:19:38 【问题描述】:

我正在开发一个“艺术画廊”应用程序。

随意拉下 source on github 并使用它。

Plunker with full source.

目前让 Masonry 与 Angular 配合得很好的解决方法:

.directive("masonry", function($parse) 
  return 
    restrict: 'AC',
    link: function (scope, elem, attrs) 
      elem.masonry( itemSelector: '.masonry-brick');
    
  ;     
)
.directive('masonryBrick', function ($compile) 
  return 
    restrict: 'AC',
    link: function (scope, elem, attrs) 
      scope.$watch('$index',function(v)
        elem.imagesLoaded(function () 
          elem.parents('.masonry').masonry('reload');
        );
      );
    
  ;    
);

这不好用,因为:

随着内容的增长,在整个容器上触发重新加载的开销也会增加。

reload 函数:

追加”项目,而是重新排列容器中的每个项目。 是否在项目从结果集中过滤时触发重新加载。

在我上面给出的应用程序的上下文中,这个问题很容易复制。

我正在寻找一种使用指令来利用的解决方案:

.masonry('appended', elem).masonry('prepended', elem)

而不是每次都执行.masonry('reload')

.masonry('reload') 表示何时从结果集中删除元素。


编辑

该项目已更新为使用下面的工作解决方案。

GitHub

上获取来源

Plunker

上查看工作版本

【问题讨论】:

你能在 plunker/jsfiddle 中发布你的代码吗?会更容易测试。我很难理解你的问题;您是否希望能够在添加时选择是否添加新项目,而无需调用reload?查看Masonry adding items example page 的来源,似乎它们在添加项目时触发了重新加载:$container.prepend($boxes).masonry('reload'); @GFoley83 是的 --- 能够控制如何添加项目以及何时重新加载容器,而无需采用骇人听闻的解决方案。这是一个完整来源的 plunker:plnkr.co/akHTslTdRMvfe3KrnPeO。 .masonry('prepended', elem) 的重新加载很好——希望这将是一个容易实现的。 .masonry('appended', elem) 但似乎这将是一个真正的问题。 @DanKanze,我试过你提到的工作版本。但面对“找不到指令‘ngInit’所需的控制器‘砌体’!”问题。我已经注册了“wu.masonry”。请帮助 哦,知道了angularjs bug 【参考方案1】:

我一直在玩这个,@ganaraj 的回答非常简洁。如果您在他的控制器的appendBrick 方法中粘贴$element.masonry('resize'); 并说明 图像加载然后看起来它可以工作。

这是一个带有它的 plunker fork:http://plnkr.co/edit/8t41rRnLYfhOF9oAfSUA

之所以需要这样做是因为只有在元素上初始化砌体或调整容器大小时才会计算列数,此时我们还没有任何砖块,因此它默认为单列。

如果您不想使用“调整大小”方法(我认为它没有记录在案),那么您可以调用 $element.masonry() 但这会导致重新布局,所以您只想在添加第一块砖时调用它。

编辑:我已经更新了上面的 plunker,只在列表长度超过 0 时调用 resize,并且在同一个 $digest 中移除多个砖块时只执行一次“重新加载”循环。

指令代码为:

angular.module('myApp.directives', [])
  .directive("masonry", function($parse, $timeout) 
    return 
      restrict: 'AC',
      link: function (scope, elem, attrs) 
        elem.masonry( itemSelector: '.masonry-brick');
        // Opitonal Params, delimited in class name like:
        // class="masonry:70;"
        //elem.masonry( itemSelector: '.masonry-item', columnWidth: 140, gutterWidth: $parse(attrs.masonry)(scope) );
      ,
      controller : function($scope,$element)
          var bricks = [];
          this.appendBrick = function(child, brickId, waitForImage)
            function addBrick() 
              $element.masonry('appended', child, true);

              // If we don't have any bricks then we're going to want to 
              // resize when we add one.
              if (bricks.length === 0) 
                // Timeout here to allow for a potential
                // masonary timeout when appending (when animating
                // from the bottom)
                $timeout(function()
                  $element.masonry('resize');  
                , 2);  
              

              // Store the brick id
              var index = bricks.indexOf(brickId);
              if (index === -1) 
                bricks.push(brickId);
              
            

            if (waitForImage) 
              child.imagesLoaded(addBrick);      
             else 
              addBrick();
            
          ;

          // Removed bricks - we only want to call masonry.reload() once
          // if a whole batch of bricks have been removed though so push this
          // async.
          var willReload = false;
          function hasRemovedBrick() 
            if (!willReload) 
              willReload = true;
              $scope.$evalAsync(function()
                willReload = false;
                $element.masonry("reload");
              );
            
          

          this.removeBrick = function(brickId)
              hasRemovedBrick();
              var index = bricks.indexOf(brickId);
              if (index != -1) 
                bricks.splice(index,1);
              
          ;
      
    ;     
  )
  .directive('masonryBrick', function ($compile) 
    return 
      restrict: 'AC',
      require : '^masonry',
      link: function (scope, elem, attrs, MasonryCtrl) 

      elem.imagesLoaded(function () 
        MasonryCtrl.appendBrick(elem, scope.$id, true);
      );

      scope.$on("$destroy",function()
          MasonryCtrl.removeBrick(scope.$id);
      ); 
    
  ;
);

【讨论】:

这种工作方式正是我想要的!我有点担心在每个元素上调用resize()。在第一个元素之后调用$element.masonry() 似乎会减少开销或没有?我正在寻找性能最好的。 我真的不确定 - 每次砖块的数量从 0 变为 1 时,您都需要重新检查大小(byresizemasonry())。这将涉及跟踪游戏中的积木数量可以很简单,就像在addBricks 中增加和在removeBricks 中减少一样简单。但是你能保证每块砖只会被调用一次吗?其他跟踪可能会产生罚款。最后我选择了 resize,因为它很轻量级(做一些数学运算并检查第一块砖的宽度),并提供了一个简单、可靠的解决方案。 我已更新 plunkr (plnkr.co/edit/8t41rRnLYfhOF9oAfSUA) 以合并 @g00fy 的数砖方式。如果您想使用appended,那么将 $watch 放在砖块上没有多大意义,因为无论如何您都必须对每个单独的砖块进行工作,并且您已经从 masonry-brick 指令中知道新/旧砖块。 我觉得使用resize() 以外的timeout() 故障保护机制将是最终解决方案。我觉得他暗示 $watch 的方式是为确保操作顺序付出的代价 --- timeout() 总是有可能失败,除非我对 timeout() 的理解当然是错误的 :) ...此外,与在 DOM 上触发 reload() 的开销相比,$watch 的开销微不足道——这是我们试图解决的原始问题。顺便说一句 --- 这里是 +50,因为您的更新以及您的解决方案如此可用。 谢谢 - 我已经玩了更多,但找不到完全摆脱 $timeout 的方法(尽管超时时间长短似乎没有区别 - 你可以摆脱2,它仍然有效)。单个 $timeout 将运行整个范围的摘要周期并更新 DOM,这基本上是我们正在等待的,我怀疑 @g00fy 的 $watch 隐式发生的事情【参考方案2】:

这不是您要找的东西(prependappend),但应该正是您要找的东西:

http://plnkr.co/edit/dmuGHCNTCBBuYpjyKQ8E?p=preview

您的指令版本为每个brick 触发reload。此版本仅触发一次重新加载整个列表更改一次

方法很简单:

    在父 masonry controller 中注册新的 bricks $watch 用于更改已注册的 bricks 并触发 masonry('reload') 删除元素时从bricks 注册表中删除brick - $on('$destroy') ? 利润

您可以扩展这种方法来做您想做的事(使用prependappend),但我看不出您有任何理由这样做。这也会复杂得多,因为您必须手动跟踪元素的顺序。我也不相信它会更快 - 相反它可能会更慢,因为如果你改变了很多砖块,你将不得不触发多个append/prepend

我不太确定,但我想您可以为此使用 ng-animatejavascript 动画版)

我们在日历应用中为tiling 事件实现了类似的功能。这个解决方案被证明是最快的。如果有人有更好的解决方案,我很乐意看到。

对于那些想要查看代码的人:

angular.module('myApp.directives', [])
  .directive("masonry", function($parse) 
    return 
      restrict: 'AC',
      controller:function($scope,$element)
        // register and unregister bricks
        var bricks = [];
        this.addBrick = function(brick)
          bricks.push(brick)
        
        this.removeBrick = function(brick)
          var index = bricks.indexOf(brick);
          if(index!=-1)bricks.splice(index,1);
        
        $scope.$watch(function()
          return bricks
        ,function()
          // triggers only once per list change (not for each brick)
          console.log('reload');
          $element.masonry('reload');
        ,true);
      ,
      link: function (scope, elem, attrs) 
        elem.masonry( itemSelector: '.masonry-brick');
      
    ;     
  )
  .directive('masonryBrick', function ($compile) 
    return 
      restrict: 'AC',
      require:'^masonry',
      link: function (scope, elem, attrs,ctrl) 
        ctrl.addBrick(scope.$id);

        scope.$on('$destroy',function()
          ctrl.removeBrick(scope.$id);
        );
      
    ;
  );

编辑: 我忘记了一件事(加载图像) - 加载所有图像后只需调用“重新加载”即可。稍后我会尝试编辑代码。

【讨论】:

性能方面,这正是我想要的。砖块在每个reload() 上重新排列的方式是一个问题。如果您查看@JamesSharps 的回答,他能够利用appended() 并使用resize() 事件来补偿DOM 滞后。是否可以修改它以在砖块上使用appended() 以保留排序并在最后触发resize()?这将是两全其美。 我对@9​​87654348@ 了解不多,但我敢打赌,您只需要更改触发这些事件的顺序即可。 你能稍微修改一下,在每个元素上使用appended(),而不是在每组新元素上使用reload()吗? 当然,但由于我不太了解masonry 的工作原理,请在有序列表中说明您预计会发生什么以及何时(触发哪些事件)。那么我修改代码就没有问题了。 @g00fy 我肯定会去desandro.com/masonry 看看它的全部内容。这是一个非常有用的工具。【参考方案3】:

嘿,我刚刚为 AngularJS 制作了 masonry 指令,它比我见过的大多数实现简单得多。在这里查看要点:https://gist.github.com/CMCDragonkai/6191419

它与 AMD 兼容。需要 jQuery、imagesLoaded 和 lodash。适用于动态数量的项目、AJAX 加载的项目(即使是初始项目)、窗口大小调整和自定义选项。前置项、附加项、重新加载项……等 73 行!

这里有一个 plunkr 显示它可以工作:http://plnkr.co/edit/ZuSrSh?p=preview(没有 AMD,但代码相同)。

【讨论】:

【参考方案4】:

Angular 中记录最少的特性之一是它的指令控制器(尽管它位于 www.angularjs.org 的首页 - 选项卡上)。

这是一个使用这种机制的修改后的 plunker。

http://plnkr.co/edit/NmV3m6DZFSpIkQOAjRRE

人们确实使用指令控制器,但它已被用于(和滥用)它可能不适合的事情。

在上面的 plunker 中,我只修改了 directives.js 文件。指令控制器是指令之间通信的一种机制。有时,在一个指令中完成所有事情是不够/容易的。在这种情况下,您已经创建了两个指令,但是让它们交互的正确方法是通过指令控制器。

我无法弄清楚你什么时候想要添加,什么时候想要添加。我目前只实现了“追加”。

另外附注:如果资源尚未实现承诺,您可以自己实现它们。做到这一点并不难。我注意到您正在使用回调机制(我不推荐)。你已经在那里做出了承诺,但你仍然在使用我无法理解的回调。

这是否为您的问题提供了适当的解决方案?

有关文档,请参阅 http://docs.angularjs.org/guide/directive > 指令定义对象 > 控制器。

【讨论】:

对不起伙计,但你分叉的 plunker 没有正确附加项目。那就是---它们一个接一个地堆叠成一行;/ ...在砌体“调整事件触发器大小”之后它们会自我纠正。如果您需要上下文来了解我希望它们在应用程序中的使用方式,那么您的做法是正确的。 appended 应该用于任何新项目,reload 应该用于删除任何项目。 prepended 似乎reload 调用后的砌体链将自我纠正堆叠问题。至于回调链——我是个菜鸟,这对我来说更像是一个学习项目。 根据您的建议,我也对原始 plunker 进行了一些重大更改。如果这能让您更轻松地工作,事情现在会干净得多。 @DanKanze 你的改动在哪里?【参考方案5】:

我相信我也遇到过同样的问题:

ng-repeat 循环中的许多图像,并希望在它们加载并准备好时对其应用砌体/同位素。

问题是即使在 imagesLoaded 被回调后,仍有一段时间图像不“完整”,因此无法正确测量和布局。

我提出了以下适用于我的解决方案,并且只需要一个布局通道。它分三个阶段发生

    等待图像加载(当最后一个从循环中添加时 - 使用 jQuery 图像加载插件)。 等待所有图像“完成” 布局图像。

angularApp.directive('checkLast', function () 
    return 
        restrict: 'A',
        compile: function (element, attributes) 
            return function postLink(scope, element) 
                if (scope.$last === true) 
                    $('#imagesHolder').imagesLoaded(function () 
                        waitForRender();
                    );
                
            
        
    
);

function waitForRender() 
    //
    // We have to wait for every image to be ready before we can lay them out
    //
    var ready = true;
    var images = $('#imagesHolder').find('img');
    $.each(images,function(index,img) 
        if ( !img.complete ) 
            setTimeout(waitForRender);
            ready = false;
            return false;
        
    );
    if (ready) 
        layoutImages();
    


function layoutImages() 
    $('#imagesHolder').isotope(
        itemSelector: '.imageHolder',
        layoutMode: 'fitRows'
    );

这适用于这样的布局

<div id="imagesHolder">
    <div class="imageHolder"
         check-last
         ng-repeat="image in images.image"
        <img ng-src="image.url"/>
    </div>
</div>

我希望这会有所帮助。

【讨论】:

【参考方案6】:

您可以将它们合并到一个指令中,而不是使用两个指令。比如:

.directive("masonry", function($timeout) 
    return 
        restrict: 'AC',
        template: '<div class="masonry-brick" ng-repeat="image in pool | filter:pool:true">' +
                        '<span>image.albumTitle|truncate</span>' +
                        '<img ng-src="image.link|imageSize:t"/>' +
                  '</div>',
        scope: 
            pool: "="
        ,
        link: function(scope, elem, attrs)
            elem.masonry(itemSelector: '.masonry-brick');

            // When the pool changes put all your logic in for working out what needs to be prepended
            // appended etc
            function poolChanged(pool) 

                //... Do some logic here working out what needs to be appended, 
                // prepended...

                // Make sure the DOM has updated before continuing by doing a $timeout
                $timeout(function()
                    var bricks = elem.find('.masonry-brick');
                    brick.imagesLoaded(function() 
                        // ... Do the actual prepending/appending ...
                    );
                );
            

            // Watch for changes to the pool
            scope.$watch('pool', poolChanged, true); // The final true compares for 
                                                     // equality rather than reference
        
    
);

html用法:

<div class="masonry" pool="pool"></div>

【讨论】:

你能把它扔到一个 plunker fork 上测试吗?这里的根本问题是,由于 DOM 就绪状态,附加事件不会触发。我过去尝试过类似的方法但没有成功:/。另外,我更喜欢模板是部分而不是注入(更清洁)。

以上是关于使用 AngularJS 砌体的主要内容,如果未能解决你的问题,请参考以下文章

AngularJS

AngularJS 工具方法以及AngularJS中使用jQuery

[AngularJS] AngularJS系列 基础篇

AngularJS + JQuery:如何在 angularjs 中获取动态内容

AngularJS进阶(三十三)书海拾贝之简介AngularJS中使用factory和service的方法

angularjs插件怎么使用