0

I have the following code I've designed to load and run script at runtime. You'll note that I save it to localStorage if it isn't already there. Now it runs fine if it's stored there already, but when it's just got the text from the file it throws ReferenceError: loginLaunch is not defined, though the text seems to have been loaded (hence the console.log lines that check the length). For your convenience I've included a line, localStorage.clear();, to make it alternate between the error message that's the problem and ReferenceError: loginLaunch is not defined, which given the code below is the desired result.

I don't understand why it should work one way and not the other. If it's a timing issue I don't see how the use of the promise, loginCode, lets it through unless possibly appendChild() is asynchronous, but I'm under the impression that it isn't (mainly because it has no callback, and I tried to find out, but could not) and even then why would code before the appendChild() have an impact?

Have I messed up one of the promises? I include the contents of the file login.js at the end. I searched SO for anything relevant but without any luck except for just one post that states that appendChild is synchronous.

Please help.

var loginCode = runCode("login_1001","./js/login.js");
loginCode.done(loginLaunch());

//FUNCTIONS START HERE

function getCode(local, source) {   //This creates the promise to get the code (not to run it)
    console.log("start of loadCode");
    dfd = $.Deferred();     //This is the one to return.

    script = localStorage.getItem(local);   //Try to load from local storage.
    // console.log("script after local attempt: "+script);
    if (script) {   //If found...
        console.log("found Local code");
        dfd.resolve(script);
                                   localStorage.clear();    //Added for debugging
    } else {    //load from file.
        ajax = $.ajax({
            url : source,
            cache : false,
            dataType : "text",  //load as text initially so that we can store it locally.
        });

        ajax.done(function(fromFile){
            localStorage.setItem(local, fromFile);  //store it locally.
            //console.log("script after ajax attempt: "+script);
            dfd.resolve(fromFile);
        });

        ajax.fail(function(){
            dfd.reject("Error retrieving code. You may be disconnected");
        });

    }

    return dfd.promise();
}

function runCode(local, source) {
    dfd = $.Deferred();     //This is the one to return.

    code = getCode(local, source);      //local promise

    code.done(function(retrievedCode){
        console.log(retrievedCode.length);
        var head = document.getElementsByTagName('head')[0];    //first head section
        var el = document.createElement("script");              //named the same as the local storage
        //script.type= 'text/javascript';   Redundant — it's the default
        // el.id = local;               //Probably redundant, but if we want to manipulate it later...
        el.text = retrievedCode;
        head.appendChild(el);       //This shouldn't run anything, just make global functions that will be called later.
        console.log(el.text.length);
        dfd.resolve();              //If we need to return the node to manipulate it later we'd make the variable above and 'return' it here
    });

    return dfd.promise();
}

Here's the contents of the login.js file.

function loginLaunch(){
    dfd = $.Deferred();     //This is the one to return.

    loadElement("login.1001", "#content", "login.html");

    //After the element has been loaded we have a disconnect — i.e. there's no promise waiting, so we have to wait for the user.

}

$("#content").delegate('#loginButton','click',function(){
    console.log("Login click");

    //php to pick up the entered details and pass them to common php that also uses the 
    checkCredentials = $.ajax({
        type : "POST",
        url : "./php/credentials.php",
        data : {
            queryString : queryString
        },
        datatype : "text",                  // 1 or 0
    });

    checkCredentials.done(credentialsChecked(success));





    // MOVE THIS STUFF
    readyPublicList();

    $.when(publicListCode,loggedIn).then(runDefaultPublicList());       //Assumes successful login so it loads the code for the list window in parallel.

    //Note that it's probable that my approach to the login window may change, because it needs to be available on the fly too.


    // $("#content").html("<p>test</p>");   //Successfully tested, well it was once.
});

function loginHide(){
    $("#loginHtml").hide;
}
SteveC
  • 351
  • 2
  • 14
  • Did you check the DOM to see if the contents of – Rajkumar Madhuram Aug 19 '14 at 21:12
  • `.appendChild()` is synchronous so that is not the issue. I've shown here http://jsfiddle.net/jfriend00/71u53y0m/ that appending a script tag with the code embedded in it runs it synchronously in all three major browsers so it doesn't appear to be an async issue unless there's something async in your script. – jfriend00 Aug 19 '14 at 22:27

2 Answers2

0

You need to change at least three things. First change this:

loginCode.done(loginLaunch());

to this:

loginCode.done(function() {loginLaunch()});

You need to be passing a function reference to the .done() handler so it can be called later. The way you had it, you were calling it immediately BEFORE loginCode() was done with its work, thus it was getting called too early.

In addition, loginLaunch doesn't exist yet so you can't pass a reference directly to it. Instead, you can pass a reference to a wrapper function that then calls loginLaunch() only after it finally exists.

And second, you need to declare your local variables with var so they aren't implicit globals and stomp on each other. For example, you have multiple functions who call each other trying to use the same global dfd. That is a recipe for disaster. Put var in front of it to make it a local variable so it's unique to that scope.

And third, el.text doesn't look like the right property to me for your script. Perhaps you meant to use .textContent or since you have jQuery, you can do:

$(el).text(retrievedCode);

In a couple style-related issue, ALL local variables should be declared with var before them so they are not implicit globals. This will bite you hard by causing mysterious, hard to track down bugs, even more so with async code.

And, you can generally use the promise returned by jQuery from ajax functions rather than creating your own.

To incorporate those improvements:

runCode("login_1001","./js/login.js").done(loginLaunch);

function getCode(local, source) {   //This creates the promise to get the code (not to run it)
    var script = localStorage.getItem(local);   //Try to load from local storage.
    if (script) {   //If found...
        localStorage.clear();    //Added for debugging
        // return a resolved promise (since there's no async here)
        return $.Deferred().resolve(script);

    } else {    //load from file.
        // return the ajax promise
        return $.ajax({
            url : source,
            cache : false,
            dataType : "text",  //load as text initially so that we can store it locally.
        }).then(function(fromFile){
            localStorage.setItem(local, fromFile);  //store it locally.
            return fromFile;
        });

    }
}

function runCode(local, source) {
    return getCode(local, source).then(function(retrievedCode){
        console.log(retrievedCode.length);
        var head = document.getElementsByTagName('head')[0];    //first head section
        var el = document.createElement("script");              //named the same as the local storage
        $(el).text(retrievedCode);
        head.appendChild(el);       //This shouldn't run anything, just make global functions that will be called later.
        console.log(el.text.length);
    });
}

FYI, if you just want to insert a script file, you don't have to manually retrieve the script with ajax yourself. You can use the src property on a script tag and let the browser do the loading for you. You can see a couple ways to do that here and here.

Community
  • 1
  • 1
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • The problem remains. However, all of your suggestions were useful, so thanks anyway. – SteveC Aug 20 '14 at 01:22
  • @SteveC - Did you fix all the problems I wrote about? Can you verify that the code is being loaded properly and that you only have a timing problem? – jfriend00 Aug 20 '14 at 01:58
  • The code wasn't being written into the header. However, see my own answer. Why changing the .done() made a difference is still unclear. – SteveC Aug 20 '14 at 02:12
  • @SteveC - I edited my answer to include your latest discovery and my explanation for why that works. – jfriend00 Aug 20 '14 at 02:26
0

I'm not sure why this works:

var loginCode = runCode("login_1001","./js/login.js");
loginCode.done(function(){loginLaunch();});

and this doesn't:

var loginCode = runCode("login_1001","./js/login.js");
loginCode.done(loginLaunch);

My one thought is that maybe if you pass literal named functions to .done then they are validated when loginCode is created, while anonymous functions aren't validated until they are about to be run.

I should note that the error was appearing before the console.log output.

Maybe someone with a better grasp of the technicalities can clarify. For now I'm just happy to stop tearing my hair out, but I like to know how things work...

SteveC
  • 351
  • 2
  • 14
  • The difference is that `loginLaunch` doesn't exist yet (it hasn't been loaded yet) so you can't pass a reference to it to `.done()`! But, you can pass a reference to a wrapper function. Good find. I edited my answer to include this last part of the solution. – jfriend00 Aug 20 '14 at 02:25
  • Remember `.done()` is executed as soon as `runCode()` finishes executing BEFORE the promise is done. But, the purpose of `.done()` is to just store away a function callback that it can later call when the actual promise is done. So since `.done()` gets executed before the promise is done, it gets executed before the script is loaded and thus it is executed before `loginLaunch` has been defined. Thus, you can't pass a reference directly to `loginLaunch` to `.done()`. But you can pass a reference to the wrapper because it won't execute until `LoginLaunch` does actually exist. – jfriend00 Aug 20 '14 at 02:41