1

I am fetching some data from my server and updating the DOM through Angular two way bindning. It is however not working as expected and I need to wrap it inside an ugly setTimeout function for the DOM to have time to update.

$http.post('myBackend.php', someData)
.then(function(res){
    $scope.data = res.data;
    doStuffWithDOMElements(); // Does not work
});

while this works:

 $http.post('myBackend.php', someData)
 .then(function(res){
     $scope.someDataToPopulateDOMwith = res.data;
     setTimeout(function(){ doStuffWithDOMElements();}, 50); // Yup, works
 });

The line that gives error without the timeout "Can't read propertly of null" is this:

let y_0 = document.getElementById("l_0").offsetTop;

and in my index.html

<div id="l_{{$index}}" ng-repeat = "x in data"></div>

This is weird. Shouldn't every DOM element that is wrapped up in Angular "events" be updated automatically? $scope.$apply() does not work and shouldn't be necessary either. What's wrong here?

novalain
  • 2,181
  • 19
  • 36

3 Answers3

1

The need of $timeout comes every once in a while in angularjs.Most probably to init a jQuery plugin.

Your Error line:

 let y_0 = document.getElementById("l_0").offsetTop; 

is because your DOM has not yet been set and you are trying to get the element which yet has to be set or rather rendered in the DOM.

When you use $timeout,it should run after the DOM has been manipulated by Angular, and after the browser renders (which may cause flicker in some cases).That is why it is working in your case when you set the $timeout.

If you want to learn more about digest cycle. Your should know about $evalAsync as well.

  • If code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders
  • If code is queued using $evalAsync from a controller, it should run before the DOM has been manipulated by Angular (and before the browser renders) -- rarely do you want this
  • If code is queued using $timeout, it should run after the DOM has been manipulated by Angular, and after the browser renders (which may cause flicker in some cases).

FurtherMore, Angularjs is a javascript framework. A browser has to do a number of things pretty much all at once, and just one of those is execute JavaScript. But one of the things JavaScript is very often used for is to ask the browser to build a display element. This is often assumed to be done synchronously (particularly as JavaScript is not executed in parallel) but there is no guarantee this is the case and JavaScript does not have a well-defined mechanism for waiting.

The solution is to "pause" the JavaScript execution to let the rendering threads catch up. And this is the effect that setTimeout() with a timeout of 0 does. It is like a thread/process yield in C. Although it seems to say "run this immediately" it actually gives the browser a chance to finish doing some non-JavaScript things that have been waiting to finish before attending to this new piece of JavaScript.

(In actuality, setTimeout() re-queues the new JavaScript at the end of the execution queue. See the comments for links to a longer explanation.)

IE6 just happens to be more prone to this error, but I have seen it occur on older versions of Mozilla and in Firefox.

Also, a lot has been written and explained about why the use of $timeout comes in handy time to time.

Links where you will find good explanation:

Community
  • 1
  • 1
Sunil Lama
  • 4,531
  • 1
  • 18
  • 46
1

Breakup the Operations

One of the things to do is breakup the operations by chaining them.

$http.post('myBackend.php', someData)
    .then (function onFulfilled (response) {
        $scope.someDataToPopulateDOMwith = response.data;
        return response;
    }).then (function onFulfilled2 (response) {
        doStuffWithDOMElements();
    });

This allows the $q service to execute a digest cycle before invoking the second fulfillment handler. The AngularJS framework needs to do a digest cycle so the watch handlers for the ng-repeat directive have an opportunity update the DOM. The ng-repeat directive needs to finish updating the DOM before the doStuffWithDOMElements function can safely manipulate the DOM.


Use $timeout Service

Avoid using the raw browser setTimeout function, instead use the $timeout service. The AngularJS $q service then automatically does digest cycles.

Since the $timeout service returns promises, it can be used for chaining.

$http.post('myBackend.php', someData)
    .then (function onFulfilled (response) {
         $scope.someDataToPopulateDOMwith = response.data;
         return response;
    }).then (function onFulfilled2 (response) {
         //return $timeout promise
         return $timeout(function(){return response}, 1000);
    }).then (function onFulfilled3 (response) {
         doStuffWithDOMElements();
    });

Because calling the then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.1


Re-factor the Design

The AngularJS is a MVW* framework.

Avoid manipulating HTML DOM programmatically: Manipulating HTML DOM is a cornerstone of AJAX applications, but it's cumbersome and error-prone. By declaratively describing how the UI should change as your application state changes, you are freed from low-level DOM manipulation tasks. Most applications written with Angular never have to programmatically manipulate the DOM, although you can if you want to.2

Look into modeling what the doStuffWithDOMElements() function does and creating custom directives to watch that model and update the DOM. This fits better with the AngularJS framework and will avoid these timing issues.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
0

in case of DOM manipulation in angular,it is well known.

see this SO link

Community
  • 1
  • 1
Pavan Teja
  • 3,192
  • 1
  • 15
  • 22