70

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.

starball
  • 20,030
  • 7
  • 43
  • 238
Dobby007
  • 1,229
  • 1
  • 12
  • 22
  • 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 05 '13 at 11:27
  • 1
    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 05 '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 04 '15 at 08:19

4 Answers4

64

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);
            })
        }
    };
}]);
Oliver
  • 4,471
  • 2
  • 21
  • 18
  • 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 05 '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: http://jsfiddle.net/Zmetser/VfTar/ – Oliver Jun 06 '13 at 07:58
  • 8
    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 http://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 09 '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 00:01
7

Olivér's answer is good, but has an issue: if you forget to broadcast the event, your javascript will not run whereas your data might have changed. Another solution would be to watch for changes on the scope, for instance:

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


directive('heightStuff', ['$timeout',
  function($timeout) {
    return {
      scope: {
        myData: '='
      },
      link: function($scope, element, attrs) {
        $scope.$watch('myData', function() {
          $timeout(function() { // You might need this timeout to be sure its run after DOM render.
            element.width()
            element.height()
          }, 0, false);
        })
      }
    };
  }
]);
<div height-stuff my-data="profile"></div>

This way the javascript functions are called every time the data changes without any need for a custom event.

Kjir
  • 4,437
  • 4
  • 29
  • 34
5

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>
user3484816
  • 51
  • 1
  • 2
3

I would like to add another answer, since the preceding answers takes it that the code needed to run after the ngRepeat is done is an angular code, which in that case all answers above give a great and simple solution, some more generic than others, and in case its important the digest life cycle stage you can take a look at Ben Nadel's blog about it, with the exception of using $parse instead of $eval.

But in my experience, as the OP states, it is usually running some jQuery plugins or methods on the finally compiled DOM, which in that case I found that the most simple solution is to create a directive with a setTimeout, since the setTimeout function gets pushed to the end of the queue of the browser, its always right after everything is done in angular, usually ng-repeat which continues after it's parents postLinking function

angular.module('myApp', [])
.directive('pluginNameOrWhatever', function() {
  return function(scope, element, attrs) {        
    setTimeout(function doWork(){
      //jquery code and plugins
    }, 0);        
  };
});

For whoever wondering why not to use $timeout, its that it causes another digest cycle that is completely unnecessary.

EDIT:

Thanx to drzaus for the link to how to use $timeout without causing digest http://www.codelord.net/2015/10/14/angular-nitpicking-differences-between-timeout-and-settimeout/

bresleveloper
  • 5,940
  • 3
  • 33
  • 47
  • Should use angular's [`$timeout`](https://docs.angularjs.org/api/ng/service/$timeout) – drzaus Jun 03 '16 at 19:34
  • i have written why i think NOT to use $timeout in the last line, please read again. – bresleveloper Jun 08 '16 at 08:34
  • Weird, no idea why I didn't see that. However, if you don't want the digest cycle then just call with `invokeApply=false`. Otherwise, there's [lots](http://stackoverflow.com/questions/19609796/what-advantage-is-there-in-using-the-timeout-in-angular-js-instead-of-window-se) [of](https://www.reddit.com/r/angularjs/comments/3gzb0d/timeout_vs_settimeout/) [arguments](http://www.codelord.net/2015/10/14/angular-nitpicking-differences-between-timeout-and-settimeout/) for using what the Angular devs wrote to replace `setTimeout`. – drzaus Jun 09 '16 at 16:50