Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Masonry with AngularJS

I am developing an "art gallery" app.

Feel free to pull down the source on github and play around with it.

Plunker with full source.

The current work around for getting Masonry to play nice with 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');         });       });     }   };     }); 

This doesn't work well because:

  • As the content grows, so does the overhead of tiggering reload on the entire container.

The reload function:

  • Does not "append" items, rather re-arranges every item in the container.
  • Does work for triggering a reload when items are filtered out of a result set.

In context with the app I've given links to above, this problem becomes very easy to replicate.

I am looking for a solution that will use directives to leverage:

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

Rather than executing .masonry('reload') every time.

.masonry('reload') for when elements are removed from result set.


EDIT

The project has been updated to use the working solution below.

Grab the source on GitHub

See a working version on Plunker

like image 455
Dan Kanze Avatar asked May 12 '13 04:05

Dan Kanze


1 Answers

I've been playing around with this a bit more and @ganaraj's answer is pretty neat. If you stick a $element.masonry('resize'); in his controller's appendBrick method and account for the images loading then it looks like it works.

Here's a plunker fork with it in: http://plnkr.co/edit/8t41rRnLYfhOF9oAfSUA

The reason this is necessary is because the number of columns is only calculated when masonry is initialized on the element or the container is resized and at this point we haven't got any bricks so it defaults to a single column.

If you don't want to use the 'resize' method (I don't think it's documented) then you could just call $element.masonry() but that causes a re-layout so you'd want to only call it when the first brick is added.

Edit: I've updated the plunker above to only call resize when the list grows above 0 length and to do only one "reload" when multiple bricks are removed in the same $digest cycle.

Directive code is:

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);       });      }   }; }); 
like image 109
James Sharp Avatar answered Sep 20 '22 21:09

James Sharp