10

I'm trying to use an Angular JS directive to center the contents of a div.

This is the simplified version of the template:

<div id="mosaic" class="span12 clearfix" s-center-content>
    <div ng-repeat="item in hits" id="{{item._id}}" >
    </div>
</div>

This is the directive:

 module.directive('sCenterContent', function() {
    var refresh = function(element){
        var aWidth= element.innerWidth();
        var cWidth= element.children()[0].offsetWidth;
        var cHeight= element.children()[0].offsetHeight;
        var perRow= Math.floor(aWidth/cWidth);
        var margin=(aWidth-perRow*cWidth)/2;
        if(perRow==0){
            perRow=1;
        }
        var top=0;
        element.children().each(function(index, child){
            $(child).css('margin-left','0px');
            if((index % perRow)==0){
                $(child).css('margin-left',margin+'px');
            }
        });
    };
    return {
        link : function(scope, element, attrs) {
            $(window).resize(function(event){
                refresh(element);
            });
        }
    };
});

It basically floats some inner divs and adds a margin to the first div in each row, so the content is centered.

This works fine when the browser is resized. The problem comes when I try to do a refresh just after initialization.

I have tried with the post linking function as seen in this question. The pre link and post link functions get called in the expected order but not after the children of the elemenet with the s-center-content directive get rendered. The call to refresh fails as no children are found.

How can I make a call to refresh ensureing the children have been processed?

Community
  • 1
  • 1
Daniel Cerecedo
  • 6,071
  • 4
  • 38
  • 51

4 Answers4

7

I think using angular's $watch is a better, more elegant solution:

link : function(scope, element, attrs) {
            $(window).resize(function(event){
                refresh(element);
            });

            var watchCount = 0,
            unWatcher = $watch(
                function() {
                   return $('*[s-center-content] img').length !== 0;
                },
                function() {
                   // ensure refresh isn't called the first time (before the count of elements is > 0)
                   if (++watchCount === 1) {
                       return;
                   }

                   refresh(element);                   

                   // stop watching the element now that refresh has been called
                   unWatcher();
                }, true);
        }

About stop watching : AngularJS : Clear $watch

Community
  • 1
  • 1
threejeez
  • 2,314
  • 6
  • 30
  • 51
  • 1
    This is very clever -- maybe a bit more description on the answer will make it more visible. – Roy Truelove Jan 02 '14 at 13:48
  • This is pretty cool. Might want to just watch on $(selector).length Although not completely sure if that always works. – David Feb 14 '14 at 23:56
5

This is a recurring issue (Somewhere there's an issue associated with this, as well as a thread in the google forums..)

Angular has no way to know when the dom is finished, it can only know when the functions that it calls return (and any promises are resolved). If those functions are asynchronous and don't have a callback or a promise, there's no way to be notified.

The way I've seen it done (and the way I've done it) is with a setInterval that checks the DOM periodically to see if it's in a completed state. I hate this solution and will be grateful for an alternative, but, it does get the job done.

Roy Truelove
  • 22,016
  • 18
  • 111
  • 153
  • What about watching the parent element? would i get notified with children additions? If so, that could be the way. I'll check! – Daniel Cerecedo Apr 11 '13 at 09:14
1

Finally made it!

I used livequery plugin and added the following line to the end of the link method:

$('*[s-center-content] > *').livequery( function(event){
    refresh(element);
});

I select any first level child (current and future) of the element with the s-center-content directive and attach a handler that is called when the elements get added to the DOM.

EDIT

A final note. The layout of my content depends mainly on inner images with dynamically evaluated src attributes. So to make it work I had to change the code to this:

$('*[s-center-content] img').livequery( function(event){
    $(this).load(function(){
       refresh(element);                    
    });
});

The full code of the directive that lays left floating divs centered is as follows. Note that it relies on adding a calculated margin to first div in the row.

module.directive('sCenterContent', function() {
    var refresh = function(element){
        var aWidth= element.innerWidth();
        var cWidth= element.children()[0].offsetWidth;
        var cHeight= element.children()[0].offsetHeight;
        var perRow= Math.floor(aWidth/cWidth);
        var margin=(aWidth-perRow*cWidth)/2;
        if(perRow==0){
            perRow=1;
        }
        var top=0;
        element.children().each(function(index, child){
            $(child).css('margin-left','0px');
            if((index % perRow)==0){
                $(child).css('margin-left',margin+'px');
            }
        });
    };
    return {
        link : function(scope, element, attrs) {
            $(window).resize(function(event){
                refresh(element);
            });
            $('*[s-center-content] img').livequery( function(event){
                $(this).load(function(){
                    refresh(element);                   
                });
            });
        }
    };
});
Daniel Cerecedo
  • 6,071
  • 4
  • 38
  • 51
  • 1
    Well done, this is very handy! I'll try to find the google group thread and the issue in angular that talks about this issue and I'll reference your answer. – Roy Truelove Apr 11 '13 at 12:56
  • Thanks for the effort Roy. – Daniel Cerecedo Apr 13 '13 at 10:28
  • 4
    If you look at the plugins [code](https://github.com/brandonaaron/livequery/blob/master/jquery.livequery.js), you will notice that it also used `setTimeout(fn, 20)` to match new elements. So its a dirty solution in friendly view 'pack'. – FelikZ May 20 '13 at 13:54
  • 1
    As an alternative, Angular's `$evalAsync` doesn't require a third party plugin http://stackoverflow.com/a/24228604/1079110. – danijar Jun 15 '14 at 11:03
0

I believe this can be accomplished with css. Is the goal centered elements regardless of the number like this?

[]  []  []  []  []

    []  []  []

If so, this is definitely possible with css and could be achieved with much less complexity. Here is an example:

http://codepen.io/thetallweeks/pen/kvAJb

thetallweeks
  • 6,875
  • 6
  • 25
  • 24