Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

In controller I get some JSON data using $http or $resource services. Then I write this data in $scope and AngularJS updates HTML structure of the page. My problem is that I need to know what is the new size (width and height) of the list (I mean, HTML DOM element) that is filled with Angular ng-repeat directive. Consequently, I have to run javascript code right after Angular finishes updating DOM structure. What is the proper way to do it? I have searched internet over the last four hours but I couldn't find any solution to my problem.

This is how I receive JSON data:

var tradesInfo = TradesInfo.get({}, function(data){
    console.log(data);
    $scope.source.profile = data.profile;
            $scope.trades = $scope.source.profile.trades;
        $scope.activetrade = $scope.trades[0];
        $scope.ready = true;


    init();  //I need to call this function after update is complete

});

And this is what happens in init() function:

function init(){
    alert($('#wrapper').width());
    alert($('#wrapper').height());
}

I know that there must be something easy to solve this problem but I can't just find it now. Thanks in advance.

share|improve this question
    
You can't. Just as simply. There may be an unexpected $compile call, any number of directives, themselves manipulating the DOM and setting things to be done after $timeouts... The AngularJS system is beautifully architected, so the pieces behave independently, and this is a small price to pay. Just find another way around. –  rewritten Jun 5 '13 at 11:27
    
I think that it must be possible to do anyway. I mean that the fact that dom with its directives can call compile function or do something else that is not predictable can't prevent it, imho. –  Dobby007 Jun 5 '13 at 16:15
    
Sorry for bringing the old question to life, but can you share the content of #wrapper? You probably just loop through trades? –  FrEaKmAn Feb 4 at 8:19

2 Answers 2

up vote 37 down vote accepted

Actually in this case the angular way is not the easy way but the only right way :)

You have to write a directive and attach to the element you want to know the height of. And from the controller you $broadcast an event, the directive'll catch the event and there you can do the DOM manipulation. NEVER in the controller.

var tradesInfo = TradesInfo.get({}, function(data){
    console.log(data);
    $scope.source.profile = data.profile;
    ...

    $scope.$broadcast('dataloaded');
});


directive('heightStuff', ['$timeout', function ($timeout) {
    return {
        link: function ($scope, element, attrs) {
            $scope.$on('dataloaded', function () {
                $timeout(function () { // You might need this timeout to be sure its run after DOM render.
                    element.width()
                    element.height()
                }, 0, false);
            })
        }
    };
}]);
share|improve this answer
    
Thank you very much. It works. Can you please explain me why $timeout of 0ms is nessesary? I'm asking because I tried to omit it and, in this case, width and height were incorrect. What is Javascript behaviour with timeout function? Does it run the code in a different thread? –  Dobby007 Jun 5 '13 at 15:49
    
No, actually there is no other thread, that why browsers does a trick. They run dom reflow only at the and of the closure, therefore if you need the dimensions of an element, you cant have them, because they not rendered yet. You have to defer execution of the measuring routine and thats where the setTimeout with 0 comes along. It'll tell the browser: Run me after you executed the current closure and already registered timeouts. Very basic example: jsfiddle.net/Zmetser/VfTar –  Olivér Kovács Jun 6 '13 at 7:58
7  
That's not correct. Browser will re-render the block immediately when you try to access the relevant style properties - so in general case you don't need to call setTimeout in this scenario - see the demo jsfiddle.net/SLTpE. But AngularJS works another way - it is Angular that doesn't update DOM instantly when you change your model, not the browser –  amakhrov Aug 9 '13 at 14:04
2  
I believe you need to call $scope.$on(...) instead of $scope.on(...) (there is a dollar sign missing). –  lanoxx Sep 12 '13 at 11:41
    
Make sure the casing of the broadcasted event matches correctly ('dataLoaded' !== 'dataloaded') –  Andy Ford Nov 27 '13 at 0:01

Another suggestion to work with JQuery. Had to work this through for a grid that was generated in a directive. I wanted to scroll to a specific row in the grid. Use $emit to broadcast from directive to parent controller:

In Controller:

    ['$timeout',function($timeout){
...
 $scope.$on('dataloaded', function () {
            $timeout(function () { // You might need this timeout to be sure its run after DOM render.
                $scope.scrollToPosition();
            }, 0, false);
        });
        $scope.scrollToPosition = function () {
            var rowpos = $('#row_' + $scope.selectedActionID, "#runGrid").position();
            var tablepost = $('table', "#runGrid").position();
            $('#runGrid').scrollTop(rowpos.top - tablepost.top);
        }

In directive

.directive('runGrid',['$timeout', function ($timeout) {
        // This directive generates the grip of data
        return {
            restrict: 'E',  //DOM Element
            scope: {    //define isolated scope
                list: '=',   //use the parent object
                selected: "="
            },

            templateUrl: '/CampaignFlow/StaticContent/Runs/run.grid.0.0.0.0.htm',  //HTML template URL

            controller: ['$scope', function ($scope) {  //the directive private controller, whith its private scope
                //$scope.statusList = [{ data_1: 11, data_2: 12 }, { data_1: 21, data_2: 22 }, { data_1: 31, data_2: 32 }];
                //Controller contains sort functionallity

                $scope.sort = { column: null, direction: 1 }
                $scope.column = null;
                $scope.direction = "asc";
                $scope.sortColumn = function (id) {
                    if(id!=$scope.column) {
                        $scope.column = id;
                        $scope.direction = "asc";
                    } else {
                        $scope.column = null;
                    }
                }
                $scope.toggleDir = function () {
                    $scope.direction = ($scope.direction == "asc") ? "desc" : "asc";
                }
               $scope.$emit('dataloaded');
            }]


        };
    }])

And this is a snippet of the grid directive html template:

 <div style="overflow-y:auto;height: 200px;" id="runGrid">
            <table class="table table-striped" style="table-layout:fixed">
           <tbody>
                <tr  ng-repeat="status in list" id="row_{{status.action_id}}" ng-class="(status.action_id==selected)?'selected':''">
                    <td>

the list and selected parameters are injected from the html that is using the directive

<run-grid list="list" selected="selectedActionID"></run-grid>
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.