12

I have a function that loads html into a table with jQuery and subsequently adds a class to one of the rows with the callback. The function is triggered by various UI driven events on the page. I also have a css transition rule so the color should fade in (transition: background-color 1000ms linear). The function looks like this:

function load_tbody(row_id) {
    $('tbody').load("load_tbody.php", function() {
        $(row_id).addClass('green');
    });
}

Once the html is loaded, the class successfully gets added and row color is set to green. However, my css transition rule seems to be ignored.

When I add a slight delay, even 10ms, it works fine:

function load_tbody(row_id) {
    $('tbody').load("load_tbody.php", function() {
        setTimeout(function() {
            $(row_id).addClass('green');
        }, 10);
    });
}

The jQuery docs for .load() state:

If a "complete" callback is provided, it is executed after post-processing and HTML insertion has been performed.

To me this would indicate the new elements have been loaded into the dom with existing styles applied and are ready for manipulation. Why does the transition fail in the first example but succeed in the second?

Here is a fully functional example page to demonstrate the behaviour in question:

http://so-37035335.dev.zuma-design.com/

While the example above links jQuery version 2.2.3 from cdn, actual page in question uses version 1.7.1. The same behavior can be observed across both versions.

UPDATE:

After considering some of the comments and answers offered below, I've stumbled upon something altogether more confusing. User @gdyrrahitis made a suggestion which lead me to do this:

function tbody_fade(row_id) {
    $('tbody').load("load_tbody.php", function() {
        $('tbody').fadeIn(0, function() {
          $(this).find(row_id).addClass('green');
        });
    });
}

Adding the class inside the fadeIn() callback works, even with a duration of 0ms. So this had me wondering... if the element is theoretically there anyway, what background color does the browser think it has before I add that class. So I log the background-color:

console.log($(row_id).css('background-color'));

And do you know what? Simply getting the background-color color made everything work:

function tbody_get_style(row_id) {
    $('tbody').load("load_tbody.php", function() {
        $(row_id).css('background-color');
        $(row_id).addClass('green');
    });
}

Just adding the line $(row_id).css('background-color'); which seemingly does nothing at all causes the transition effect to work. Here's a demo:

http://so-37035335-b.dev.zuma-design.com/

I'm just dumbfounded by this. Why does this work? Is it merely adding a small delay or does jQuery getting the css property somehow have a substantial effect on the state of the newly added element?

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
  • I recently read something on jquery documentation that was related to similar thing. The docs were suggesting to wrap your call to settimeout with 0 ms. Try it, does it run with 0 ms delay? If it does, then just google jquery docs for settimeout. There was an explanation. – Uzbekjon May 04 '16 at 19:51
  • @Uzbekjon - 0ms doesn't work. Just tried some tests and Chrome seems to work with 2ms, firefox needs 5ms. – But those new buttons though.. May 04 '16 at 19:55
  • I suggest you use `requestAnimationFrame` instead of `setTimeout`. You can fill polyfills for older browsers. https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame – Gokhan Kurt May 07 '16 at 09:05
  • @GökhanKurt - I'm not using `setTimeout` as any kind of solution here - only to demonstrate the issue. – But those new buttons though.. May 07 '16 at 14:22
  • @billynoah That is why I am writing it as a comment. If I meant it as an answer to your question, I would write it as answer. – Gokhan Kurt May 07 '16 at 14:25

4 Answers4

5

jQuery load is intended to drop everything that is requested into the page.

You can leverage the power of jQuery Deferred objects by using $.get instead.

Take a look at this plunk.

Code snippet from plunk

function load_tbody(row_id) {
  $.when($.get("load_tbody.html", function(response) {
    $('tbody').hide().html(response).fadeIn(100, function() {
      $(this).find(row_id).addClass('green');
    });
  }));
}

I am using the $.when which will run its callback as soon the $.get is resolved, meaning will fetch the HTML response. After the response is fetched, it is appended to the tbody, which is fadedIn (fadeIn method) and after it is shown, the .green class is added to the desired row.

Note that if you go and just append the html and then the class to the row_id, you won't be able to see the transition, because it is executed immediately. A little nice visual trick with the fadeIn can do the work.

Update

On newly added elements to the DOM, CSS3 transition is not going to be triggered. This mainly happens because of the internal browser engine that controls all animations. There are numerous articles with workarounds on the issue, as well as stackoverflow answers. Additional resources can be found there, which I believe can explain the topic much better than me.

This answer is about taking a step back and changing the piece of functionality that renders dynamic elements in DOM, without going to use setTimeout or requestAnimationFrame. This is just another way to achieve what you want to achieve, in clear and consistent way, as jQuery works across browsers. The fadeIn(100, ... is what is needed to catch up with the next available frame the browser is about to render. It could be much less, the value is just to satisfy visual aesthetics.

Another workaround is to not use transitions at all and use animation instead. But from my tests this fails in IE Edge, works well on Chrome, Firefox.

Please look at the following resources:

Update 2

Take a look at the specification please, as interesting stuff lies there regarding CSS3 transitions.

...This processing of a set of simultaneous style changes is called a style change event. (Implementations typically have a style change event to correspond with their desired screen refresh rate, and when up-to-date computed style or layout information is needed for a script API that depends on it.)

Since this specification does not define when a style change event occurs, and thus what changes to computed values are considered simultaneous, authors should be aware that changing any of the transition properties a small amount of time after making a change that might transition can result in behavior that varies between implementations, since the changes might be considered simultaneous in some implementations but not others.

When a style change event occurs, implementations must start transitions based on the computed values that changed in that event. If an element is not in the document during that style change even or was not in the document during the previous style change event, then transitions are not started for that element in that style change event.

Community
  • 1
  • 1
gdyrrahitis
  • 5,598
  • 3
  • 23
  • 37
  • This doesn't really answer the question in the context I asked it, using ajax request and callback. Why do I need a delay at all if the callback fires *after* the elements are inserted as stated in the docs? – But those new buttons though.. May 07 '16 at 14:38
  • Because the transition cannot be triggered. Will update answer – gdyrrahitis May 07 '16 at 15:02
  • Thanks - I actually added an interpretation of your functin to another demo page and even with `fadeIn()` set to 0 it works. I still don't quite understand why. Is `fadeIn()` acting like dom ready for the added elements? – But those new buttons though.. May 07 '16 at 15:26
  • Glad that it works for you. Actually no, it doesn't have exactly to do with DOM but the internal engine that handles browser animations (CSS transitions, CSS animations, SMIL, etc.). – gdyrrahitis May 07 '16 at 15:43
  • Just to let you know I completely discourage the use of `.load()` method as it gave me loads of trouble in the past in similar situations. Better stick with the `$.get` and promises. – gdyrrahitis May 07 '16 at 16:05
  • I understand - I tried a straight ajax call as well with same issue. Your method does work but it's still a workaround - and a bit more expensive than just doing a 15ms setTimeout. The additional info you posted is also helpful. I've given you an upvote but still not sure this exactly answers the question - at least not in a way I can make sense out of yet. – But those new buttons though.. May 07 '16 at 16:09
  • actually I don't see the point of `$.when()` here at all. You're not making use of the promise and the dom manipulation is just using the `$.get()` callback. Am I missing something? How is this any different than doing the same callback inside `load()`? – But those new buttons though.. May 07 '16 at 16:42
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/111336/discussion-between-gdyrrahitis-and-billynoah). – gdyrrahitis May 08 '16 at 08:52
3

When element is added, reflow is needed. The same applies to adding the class. However when you do both in single javascript round, browser takes its chance to optimize out the first one. In that case, there is only single (initial and final at the same time) style value, so no transition is going to happen.

The setTimeout trick works, because it delays the class addition to another javascript round, so there are two values present to the rendering engine, that needs to be calculated, as there is point in time, when the first one is presented to the user.

There is another exception of the batching rule. Browser need to calculate the immediate value, if you are trying to access it. One of these values is offsetWidth. When you are accessing it, the reflow is triggered. Another one is done separately during the actual display. Again, we have two different style values, so we can interpolate them in time.

This is really one of very few occasion, when this behaviour is desirable. Most of the time accessing the reflow-causing properties in between DOM modifications can cause serious slowdown.

The preferred solution may vary from person to person, but for me, the access of offsetWidth (or getComputedStyle()) is the best. There are cases, when setTimeout is fired without styles recalculation in between. This is rare case, mostly on loaded sites, but it happens. Then you won't get your animation. By accessing any calculated style, you are forcing the browser to actually calculate it

Trigger CSS transition on appended element

Explanation For the last part

The .css() method is a convenient way to get a style property from the first matched element, especially in light of the different ways browsers access most of those properties (the getComputedStyle() method in standards-based browsers versus the currentStyle and runtimeStyle properties in Internet Explorer) and the different terms browsers use for certain properties.

In a way .css() is jquery equivalent of javascript function getComputedStyle() which explains why adding the css property before adding class made everything work

Jquery .css() documentation

// Does not animate
var $a = $('<div>')
    .addClass('box a')
    .appendTo('#wrapper');
    $a.css('opacity');
    $a.addClass('in');


// Check it's not just jQuery
// does not animate
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity;
e.className += ' in';
.box { 
  opacity: 0;
  -webkit-transition: all 2s;
     -moz-transition: all 2s;
          transition: all 2s;
    
  background-color: red;
  height: 100px;
  width: 100px;
  margin: 10px;
}

.box.in {
    opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<div id="wrapper"></div>

Here is the listed work arounds [SO Question]

css transitions on new elements [SO Question]

Community
  • 1
  • 1
codefreaK
  • 3,584
  • 5
  • 34
  • 65
1

This is a common problem caused by browsers. Basically, when new element is inserted it is not inserted immediately. So when you add the class, it is still not in the DOM and how it will be rendered is calculated after the class is added. When the element is added to the DOM, it already has the green background, it never had a white background so there is no transition to do. There are workarounds to overcome this as suggested here and there. I suggest you use requestAnimationFrame like this:

function load_tbody(row_id) {
    $('tbody').load("load_tbody.php", function() {
        requestAnimationFrame(function() {
            $(row_id).addClass('green');
        });
    });
}

Edit:

Seems like the above solution doesn't work for all cases. I found an interesting hack here which triggers an event when an element is really parsed and added to the DOM. If you change the background color after the element is really added, the problem will not occur. Fiddle

Edit: If you want to try this solution (Won't work below IE9):

Include this CSS:

@keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    } 
}

@-moz-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    }  
}

@-webkit-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    }  
}

@-ms-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    } 
}

@-o-keyframes nodeInserted {  
    from {  
        outline-color: #fff; 
    }
    to {  
        outline-color: #000;
    }  
} 

.nodeInsertedTarget {
    animation-duration: 0.01s;
    -o-animation-duration: 0.01s;
    -ms-animation-duration: 0.01s;
    -moz-animation-duration: 0.01s;
    -webkit-animation-duration: 0.01s;
    animation-name: nodeInserted;
    -o-animation-name: nodeInserted;
    -ms-animation-name: nodeInserted;        
    -moz-animation-name: nodeInserted;
    -webkit-animation-name: nodeInserted;
}

And use this javascript:

nodeInsertedEvent= function(event){
        event = event||window.event;
        if (event.animationName == 'nodeInserted'){
            var target = $(event.target);
            target.addClass("green");
        }
    }

document.addEventListener('animationstart', nodeInsertedEvent, false);
document.addEventListener('MSAnimationStart', nodeInsertedEvent, false);
document.addEventListener('webkitAnimationStart', nodeInsertedEvent, false);

function load_tbody(row_id) {
    $('tbody').load("load_tbody.php", function() {
        $(row_id).addClass('nodeInsertedTarget');
    });
}

Can be made into a generic solution or library. This is just a fast solution.

Community
  • 1
  • 1
Gokhan Kurt
  • 8,239
  • 1
  • 27
  • 51
  • I'm interested in this but unfortunately it does not work. I've updated the example page (http://so-37035335.dev.zuma-design.com/) to include your function and there is no transition - tried in Chrome and Firefox. – But those new buttons though.. May 07 '16 at 14:48
  • Tried your link. You are right. Works for me in Chrome, IE Edge, IE 11 but not in Firefox. I don't know why it doesn't work for you in Chrome. Maybe it is about computer specs. – Gokhan Kurt May 07 '16 at 14:58
  • Might be.. it's an odd thing all around. I figure if an event is firing after something is added to DOM that css transitions should just work without any kind of timing delay hacks. – But those new buttons though.. May 07 '16 at 15:27
  • This might be another workaround but I'm not sure how that answers the question. I'm not looking for more workarounds here - I've already got a simple, effective and inexpensive one using `setTimeout` as illustrated. – But those new buttons though.. May 07 '16 at 16:46
  • 1
    @billynoah It is a workaround but it explains the reason of your problem. Basically, when you add the "green" class, the element is not parsed in the DOM yet. The transition works on change of the background color. But the background color of the element doesn't change since it is green before the moment it is "really" added to the DOM. But if you do it with a delay, the background is white at first and transitions to green after the class is added. Also if the element is harder to parse, 10 miliseconds delay might not be enough so it is not a deterministic solution. – Gokhan Kurt May 07 '16 at 16:50
  • thanks for the explanation - I'll study your answer a bit more to gain a better understanding – But those new buttons though.. May 07 '16 at 17:00
1

The main solution is understanding setTimeout(fn, 0) and its usages.

This is not related to CSS animations or jQuery load method. This is a situation when you do multiple tasks into DOM. And delay time is not important at all, the main concept is using setTimeout. Useful answers and tutorials:

function insertNoDelay() {
 $('<tr><td>No Delay</td></tr>')
  .appendTo('tbody')
  .addClass('green');
}

function insertWithDelay() {
 var $elem = $('<tr><td>With Delay</td></tr>')
  .appendTo('tbody');
 
 setTimeout(function () {
  $elem.addClass('green');
 }, 0);
}
tr { transition: background-color 1000ms linear; }
.green { background: green; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<table>
 <tbody>
  <tr><td>Hello World</td></tr>
 </tbody>
</table>


<button onclick="insertNoDelay()">No Delay</button>
<button onclick="insertWithDelay()">With Delay</button>
Community
  • 1
  • 1
dashtinejad
  • 6,193
  • 4
  • 28
  • 44
  • In your example I'm not seeing any CSS transition with either button. If I edit your snippet and increase timeout I begin seeing transition effect at 7ms (using Firefox v45). So saying "*delay time is not important at all*" is simply not true. – But those new buttons though.. May 07 '16 at 14:32