3

I've made a jsbin to show the problem: http://jsbin.com/dexeqiz/edit?html,js,output

having this html:

<div id='log'></div>
<div id='scripts'></div>

and js:

$.get('...', function(){
    $('#scripts')
        .append("<script>$(function(){$('#log').append('<p>3</p>');});<\/script>");
    $('#log').append('<p>1</p>');
    $('#log').append('<p>2</p>');
});

in jquery 1 and 2

it will render in the #log:
3
1
2

but in jquery 3 it will render
1
2
3

(so 3 is added only after the whole ajax handler was completed)

this is a problem because sometimes my code expects that the code that was appended in the line before was executed before calling the next line


right now my only workaround is to put the code after .append(newhtml) inside a setTimeout, but I would prefer not to do that because it looks slightly slower for the user. I would much rather have something like $.when(append).done(function(){code})

UPDATE: seems that this is happening because starting with jQuery 3 scripts for document ready $(function(){}); load async (https://github.com/jquery/jquery/issues/1895) and this is my current solution: http://jsbin.com/xayitec/edit?html,js,output

Omu
  • 69,856
  • 92
  • 277
  • 407
  • What about just moving the script append to the end? – Terry Aug 25 '17 at 10:04
  • 3
    This is an X/Y problem, as you're probably doing it incorrectly when you have to append script tags like that, and have issues with differences in jQuery versions? – adeneo Aug 25 '17 at 10:04
  • @Terry the code after append needs the newly added html to be executed first – Omu Aug 25 '17 at 10:08
  • @adeneo I'm appending html which may contain script tags as well, and this was working in jquery 1.12 and 2, the behaviour changed in v 3 – Omu Aug 25 '17 at 10:10
  • Might be connected to this: http://jquery.com/upgrade-guide/3.0/#deferred – Nathan Aug 28 '17 at 14:48
  • @Omu sorry I'm not sure if i got it right yet... #1 You want to add a inline script, #2 in this inline script you want to execute code and #3 after this code is executed you want to go on with other code that "lives" outside of the inline script? Am I getting you right? – Axel Aug 29 '17 at 22:40
  • What is the question, and which compromise are you looking for? – Frederik.L Aug 30 '17 at 01:01
  • Never seen use of deferred before, looks legit. The way I have traditionally handled this sort of thing is by wrapping code blocks (e.g. the two appends to #log) inside a `$(document).ready(function() { ... });`. I've also nested these blocks to ensure blocks of code executing outermost to innermost. I think deferred is a better solution though – e_i_pi Aug 30 '17 at 01:33
  • @Axel I'm getting some html from the server which has scripts as well, and after I'm appending this html my code expects the scripts in the newly added html to be executed already, as it was until jquery 3, think it's probably a bug of the current version – Omu Aug 30 '17 at 11:54

1 Answers1

1

After all fiddling it stands out: there is no real solution for this issue; at least none that is not very hackish or without changing the whole setup/workflow.

For the sake of completeness I leave my "answers" as they are (see everything below "LOG") and add some background information.

  • https://jquery.com/upgrade-guide/3.0/#breaking-change-document-ready-handlers-are-now-asynchronous

    The document-ready processing in jQuery has been powered by the jQuery.Deferred implementation since jQuery 1.6. As part of jQuery 3.0's alignment with the Promises/A+ standard, document-ready handlers are called asynchronously even if the document is currently ready at the point where the handler is added. This provides a consistent code execution order that is independent of whether the document is ready or not.

  • https://github.com/jquery/jquery/issues/3773#issuecomment-325999054

    Since you're wrapping code in your script tag in $, you can also wrap the other lines:

    $.get('...', function(){ $('#somediv') .append( "somehtml<script>$(function(){$('#log').append('<p>3</p>');});\/script>" ); $(function(){ $('#log').append('<p>1</p>'); $('#log').append('<p>2</p>'); }); });

    I'd try to not rely on such order too much, though, such code can get quite brittle. I think we technically don't guarantee that order is preserved but that's the case right now.

    Often a better solution is to put scripts at the end of body and then wrapping functions in $ is not necessary.

  • https://github.com/jquery/jquery/issues/1895
  • https://github.com/jquery/jquery/pull/2126



One more idea - very hackish

You can search for all scripts in the string that will be appended. Then search for all "$( function(){...} )" occurrences with regex then insert a function like "$( function(){...;executionHandler()} )" that will count down until all these constructs are resolved and then set an outer promise as resovled. But this would be quite hackish as I said at the beginning and also might be quite error prone.



LOG

Version 1, 2, 2.1 and 3 are all tested with jQuery versions 1.12.4, 2.2.4 and 3.2.1 and should work fine with all versions from jQuery 1.8 and above.

Version 1

$.get('...', function(){

    // make shure that $Deferred is in scope of all involved functions
    window.$Deferred = $.Deferred(); // get jQuery deferred object

    $('#scripts').append(
        "hi<script>$(function(){$('#log').append('<p>3</p>');});<\/script>" + // append your common stuff
        "<script>$(function(){$Deferred.resolve();})<\/script>" // add this line to resolve window.$Deferred
    );

    $Deferred.always(function() {
        // execute code after $Deferred was resolved or rejected
        $('#log').append('<p>1</p>');
        $('#log').append('<p>2</p>');
    });

});

Version 2

was kicked


Version 3

// $.get returns a jqXHR object which is a promise-compatible object
$.when($.get('...', function() {
    $('#scripts').append(
        "hi<script>$(function(){$('#log').append('<p>3</p>');});<\/script>"
    )
})).always(function( data, textStatus, jqXHR|errorThrown ) {
    // execute code when $.get is done or fails
    $('#log').append('<p>1</p>');
    $('#log').append('<p>2</p>');
});

Or alternatively

$.when($.get('...', function() {
    $('#scripts').append(
        "hi<script>$(function(){$('#log').append('<p>3</p>');});<\/script>"
    )
})).then(
    function( data, textStatus, jqXHR ) {
        // success callback
    },
    function( jqXHR, textStatus, errorThrown ) {
        // fail callback
    }
);

Details


References

Axel
  • 3,331
  • 11
  • 35
  • 58
  • I can't add `$Deffered.resolve()` to every script in the html that I'm getting from the server. sometimes there might not even be script tags in the html – Omu Aug 30 '17 at 11:58
  • @Omu so you have to show some realistic markup. Then I will be able to show how to handle it! I guess it will be quite simple :) – Axel Aug 30 '17 at 12:13
  • it could be any markup, usually an entire view rendered by the server ( from mvc ), it can have html and scripts, and I want to execute js after all the scripts in the appended html have executed, but would like to avoid setTimeout – Omu Aug 30 '17 at 12:25
  • Ok. Got it. Will make an update as soon as I have time. Probably in the evening... – Axel Aug 30 '17 at 12:27
  • @Omu see my updated answer and let me know if it does it for you or not. – Axel Aug 30 '17 at 22:12
  • @Omu added another version. – Axel Aug 31 '17 at 12:54
  • I don't think that there is any binding between $.when and the .html/append that is inside its function body, we can get the same result by putting the .append call inside a $(function(){}); atm this will work https://github.com/jquery/jquery/issues/3773#issuecomment-325999054 – Omu Aug 31 '17 at 16:16
  • @Omu I disagree... Version 3 makes use of $.when just the way it is supposed to while it takes the promise-compatible object returned by $.get. After reading all the stuff related to the link you posted in your last comment (which includes the thread itself but also all mentioned links in the thread) I think now I fully understand the issue you have. Thereby I must come back to the first version I wrote (just updated it slightly to match exactly your needs). So maybe you'd like to have a closer look to (updated) version 1 again. – Axel Aug 31 '17 at 20:54
  • BTW It was you who explicitly asked for "I would much rather have something like $.when(append).done(function(){code})" (which absolutely makes sense here). So I think version 1 is just right for you! Theres only one caveat with version 1: if you have more than one ongoing $.get(s) that need to treatened like this in parallel the code needs a little customization. If you want me to do the modification - let me know. – Axel Aug 31 '17 at 21:02
  • there's no difference between v1 and my current solution, even though you append script for deffered.resolve, everything inside $(funciton(){}) runs async and the order is not guaranteed – Omu Sep 01 '17 at 07:10
  • 1
    @Omu you are absolutely right. I gave my best to find a good solution! – Axel Sep 04 '17 at 12:42
  • @Omu updatet my answer a last time to give at least a kind of conclusion... – Axel Sep 04 '17 at 15:35