I am developing an \"art gallery\" app.
Feel free to pull down the source on github and play around with it.
Plunker with full
Rather than using two directives you could incorporate them both into one directive. Something like:
.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
}
}
});
and html usage:
<div class="masonry" pool="pool"></div>
This is not exacly what you are looking for (prepend
and append
), but should be just what you are looking for:
http://plnkr.co/edit/dmuGHCNTCBBuYpjyKQ8E?p=preview
Your version of the directive triggers reload
for every brick
. This version triggers only reload only once for the whole list change.
The approach is very simple:
bricks
in parent masonry
controller
$watch
for changes in the registered bricks
and fire masonry('reload')
brick
from bricks
registry when you are removing the element - $on('$destroy')
You can extend this approach to do what you wanted (use prepend
and append
) but I don't see any reason why you would want to do that. This also would be much more complicated, since you would have to manually track the order of the elements. I also don't belive it would be any faster - on the contrary it may be slower, since you would have to trigger multiple append/prepend
if your changes a lot of bricks.
I am not quite sure, but I guess you could use ng-animate
for this (the JavaScript
animation version)
We have implemented something similar for tiling
events in our calendar app. This solution turned out to be the fastest. If anyone has better solution, I'd love to see that.
For those who want to se the code:
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);
});
}
};
});
Edit: there is one thing I forgot about (loading images) - just call 'reload' when all images were loaded. Ill try to edit the code later.
Hey I just made masonry directive for AngularJS that is far more simpler than most of the implementations I've seen. Check the gist out here: https://gist.github.com/CMCDragonkai/6191419
It's compatible with AMD. Requires jQuery, imagesLoaded and lodash. Works with dynamic amount of items, AJAX loaded items (even with initial items), window resizing, and custom options. Prepended items, appended items, reloaded items... etc. 73 lines!
Here's a plunkr showing it work: http://plnkr.co/edit/ZuSrSh?p=preview (without AMD, but the same code).
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);
});
}
};
});
I believe that I have had exactly the same problem:
Many images in a ng-repeat loop, and want to apply masonry/isotope to them when they are loaded and ready.
The issue is that even after imagesLoaded is called back there is a period of time when the images are not 'complete', and so can not be measured and layed out properly.
I have come up with the following solution that works for me and only requires one layout pass. It occurs in three stages
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'
});
}
This works with layout like this
<div id="imagesHolder">
<div class="imageHolder"
check-last
ng-repeat="image in images.image"
<img ng-src="{{image.url}}"/>
</div>
</div>
I hope this helps.
One of the least documented feature of Angular is its Directive Controllers ( though it is on the front page of www.angularjs.org - Tabs ).
Here is a modified plunker that makes use of this mechanism.
http://plnkr.co/edit/NmV3m6DZFSpIkQOAjRRE
People do use Directive Controllers but it has been used ( and abused ) for things it probably was not meant for.
In the plunker above I have only modified the directives.js file. Directive controllers are a mechanism for communication between directives. Sometimes , it is not sufficient / easy to do everything in one directive. In this case, you have already created two directives but the right way to make them interact is through a directive controller.
I was not able to figure out when you wanted to prepend and when you wanted to append. I have only implemented "append" currently.
Also on a side note : If resources doesnt already implement promises, you can implement them yourself. It isnt really hard to do that. I noticed you are using a callback mechanism (which I wouldnt recommend ). You have already put in promises there but still you are using callbacks which I was not able to understand why.
Does this provide a proper solution to your problem ?
For documentation see http://docs.angularjs.org/guide/directive > Directive Definition Object > controller.