how to invoke a dynamically loaded javascript function - javascript

I was implementing a on-demand script controller based on jquery's getscript, it looks like this:
function controller = function(){
var script = function(){
var scripts = {};
return {
load: function(jsurl){
$.getScript(jsurl, null);
},
run: function(js){
window[js].apply(this,null);
}
}
};
return {
script: script()
};
}
var ctlr = controller();
then here is a remote script with a function to be loaded - remote.js
function remotefunc(){
alert( 'remotefunc invoked' );
}
and here is how the whole thing supposed to work, in the main script:
ctlr.script.load( 'remote.js' ); // remote script successfully loaded
ctlr.script.run( 'remotefunc' ); // got an error, window['remotefunc'] undefined
but as you can see, 'remotefunc' is defined in the global 'window' scope, so the window object is supposed to be able to 'see' it.
I thought the problem was probably the closure stuff in the 'controller' definition, so I did a direct $.getScirpt without using the 'controller':
$.getScript( 'http://path/to/remote.js', function(){
window['remotefunc'].apply( this, null ); // this worked
} );
strange. So it is about the 'controller' implementation(I kind need it)! Anybody can help me out with this? How to fix the 'controller' implementation so the
window[js].apply(this,null);
can actually work?
Thanx.

The reason it's telling you window['remotefunc'] is undefined is because you are not giving it time to actually download and execute the remote script before attempting to call a function defined in it.
The remote script is loaded asynchronously, which means the script execution isn't paused while waiting for a response.
You will need to either re-implement the getScript method to be synchronous or somehow work your class around the fact that the function will not be available in any determinate amount of time.
EDIT: Just found another possible solution, try calling this before your request
$.ajaxSetup({async: false});
This will make the getScript method synchronous

When using something like getSript, it's important to remember that it is fetching asynchronously. Meaning, the browser fires off the request and while that's happening, code after that line executes without pause.
jQuery provides a callback function parameter to get script that allows you to do something after the asynchronous fetch is finished.
Try this:
var script = function(){
var scripts = {};
return {
load: function(jsurl, callback){
$.getScript(jsurl, callback);
},
run: function(js){
window[js].apply(this,null);
}
}
};
Then, when using it:
ctlr.load( 'remote.js', function(){
// remote script successfully loaded
ctlr.run( 'remotefunc' );
});

Could this be a timing issue?
In your working example you call the function in a callback which jQuery will not invoke until the script is loaded. In your non-working example, you call the function immediately after getScript which is asynchronously loading the script.

Related

Can an eval access external functions?

My company allows us to write code in a javascript editor online. Other libraries are preloaded, so the code we write has access to these libraries.
Specifically, we can use Underscore.js and jQuery.js functions in our code. We can also use our very own library Graphie.js.
In an effort to save myself time, I have slowly built up my own personal set of functions which I copy and paste into every code I write. That set of functions is now so long that I want to fetch it externally (in order to save space, etc).
$.getScript( 'url/to/myfunctions.js' )
I tried the above code, but it was too good to be true. This jQuery function getScript seems to run myfunctions as their own independent unit. This fails because myfunctions use our Graphie.js functions within them.
$.get( 'url/to/myfunctions', eval )
This above code fetches and successfully evals my code (i configured my server to do so). Also too good to be true. Any jQuery and Underscode functions in my code actually work. But any Graphie functions in my code cause an error.
Instead of
$.get( 'url/to/myfunctions', eval );
try
$.get( 'url/to/myfunctions', function(code) { eval(code); } );
This way the eval function is going to be executed within the same scope as the rest of your code, rather than within the scope of jQuery. After the code has been fetched and executed, you can continue with the execution of the rest of your code:
$.get( 'url/to/myfunctions', function(code) {
eval(code);
callback();
});
function callback() {
// Your code goes here
}
Explanation
For the purpose of the explanation, let's use this simplified model of the environment, in which your code is being executed:
// JQuery is defined in the global scope
var $ = {
get: function( url, fn ) {
var responses = {
"url/to/myfunctions": "try {\
if(graphie) log('Graphie is visible.');\
} catch (e) {\
log('Graphie is not visible. (' + e + ')');\
}"
}; fn( responses[url] );
}
};
(function() {
// Graphie is defined in a local scope
var graphie = {};
(function() {
// Your code goes here
$.get( "url/to/myfunctions", eval );
$.get( "url/to/myfunctions", function(code) { eval (code); } );
})();
})();
The output: <ol id="output"></ol>
<script>
function log(msg) {
var el = document.createElement("li");
el.appendChild(document.createTextNode(msg));
output.appendChild(el);
}
</script>
As you can see, the function passed to $.get gets executed inside its body. If you only pass eval to $.get, then you don't capture the local variable graphie, which is then invisible to the evaluated code. By wrapping eval inside an anonymous function, you capture the reference to the local variable graphie, which is then visible to the evaluated code.
I'd advise against the use of eval. However, you can follow the following model.
First in your myFunctions.js, wrap all your code into a single function.
(function(_, $, graphie) {
// declare all your functions here which makes use of the paramters
}) // we will be calling this anonymous function later with parameters
Then after getting the script you could do
$.get( 'url/to/myfunctions', function(fn){
var el = document.createElement('script');
el.type = 'text/javascript';
el.text = fn + '(_, jQuery, Graphie);';
document.head.appendChild(el);
});
Note that, I've put Graphie as the parameter, but I'm not sure of it. So put your correct graphie variable there.
Assuming that you have ajax access to this script (since that is what $.get is doing in your sample code shown), you could attempt to use jQuery's .html() to place the script which should execute it with the page's variable environment.
$.ajax({
url: 'url/to/myfunctions.js',
type: 'GET',
success: function (result) {
var script = '<scr'+'ipt>'+result+'</scr'+'ipt>';
var div = $("<div>");
$("body").append(div);
div.html(script);
}
});
Internally, this script will end up being executed by jQuery's globalEval function. https://github.com/jquery/jquery/blob/1.9.1/src/core.js#L577
// Evaluates a script in a global context
// Workarounds based on findings by Jim Driscoll
// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
globalEval: function( data ) {
if ( data && jQuery.trim( data ) ) {
// We use execScript on Internet Explorer
// We use an anonymous function so that context is window
// rather than jQuery in Firefox
( window.execScript || function( data ) {
window[ "eval" ].call( window, data );
} )( data );
}
}
I also asked a question related to this here: Why is it that script will run from using jquery's html but not from using innerHTML?
Thanks to everyone's help, here is the solution that worked...
The myfunctions.js file has to be wrapped in a function:
function everything(_,$,Graphie){
// every one of myfunctions now must be attached to the Graphie object like this:
Graphie.oneOfMyFunctions = function(input1,input2,etc){
// content of oneOfMyFunctions
}
// the rest of myfunctions, etc.
}
Then in my code I can retrieve it with:
$.get( '//path/to/myfunctions', eval )
everything(_,jQuery,mygraphievar);
Somehow, the code being evaled didn't have access to the global variable mygraphievar, which is why it had to be passed in and NOT part of the evaled code (here Amit made a small error).
Also, the everything function is executed OUTSIDE of the $.get() so that the changes to mygraphievar are made before any other code below gets executed.
One should notice that $.get() is actually an asynchronous function and will not call eval until after other code is executed. This causes the code to fail the very first time I run it, but after the first time the functions get saved in memory and then everything works correctly. The proper solution would be to write ALL of the code I want to execute in the callback function of the $.get(), but I was lazy.
One should also know that a slightly simpler solution is possible with $.getScript() but I don't have time to verify it.

When is $on loaded needed in an AngularJS app?

I'm following this angular tutorial and am confused by the $on("loaded", ...) calls.
$scope.user.$on("loaded", function() {
populatePosts();
populatecomments();
});
...
function populatecomments() {
$scope.comments = {};
angular.forEach($scope.user.comments, function(comment) {
var post = Post.find(comment.postId);
post.$on("loaded", function() {
$scope.comments[comment.id] = post.$child("comments").$child(comment.id);
$scope.commentedPosts[comment.postId] = post;
});
});
}
Why can't we just call populatePosts() and populatComments() here? Why is a second $on("loaded", ...) needed within populatecomments() if it isn't called until loaded?
The first:
$scope.user.$on("loaded",...
will be triggered when $scope.user is loaded, the second:
post.$on("loaded",...
will be triggered for each post when the given one is loaded.
Angular(and mostly javascript) does network calls asynchronously, so if you want to postprocess their result, you have to define a callback which will be called when they are loaded.
As you see populatecomments() uses $scope.user.comments which could be inaccessible if you call populatecomments() directly, outside of the loaded event handler.
In this case, it's not. You could (and should) refactor this code to use promises instead.

How to find if the chained asynchronous scripts has been loaded?

Here's the scenario. I am doing a $.getScript()function call to get a script in my javascript file. The script that I'm downloading from the $.getScript() tries to download some other scripts that it's dependent on. In my script I'm using done() to check if the script loaded completely or not. And if it did, then I try calling the function that's not on the script that I just loaded form $.getScript but the script that was loaded in it.
It's getting confusing so let me demonstrate with some code:-
//my script.js
$.getScript("http://myexternaljs.com/first.js").done(function(){
doSecond(); //<- this resides in second.js which is being called in first.js below
}
//first.js
(function(){
$.getScript("http://firstexternal.com/second.js");
}());
//second.js
function doSecond(){
return console.log("hello world");
}
The problem here is second.js takes a little time to download so I keep getting doSecond() is undefined error on my call to doSecond() on done().
I could use a timeout and check if second.js loaded or not but is there a better way to do this?
I'm open to any AMD loaders or Promises answers as well.
You can also use $.ajaxSuccess:
$(document).ajaxComplete(function(ev, jqXhr, options) {
// You could be as specific as you need to here.
if ( options.url.match('second.js') ) {
doSecond();
}
});
Alternatively, you could do this inside ajaxComplete:
$(document).ajaxComplete(function(ev, jqXhr, options) {
// You could simplify to
// doSecond && doSecond()
// if you can trust that it will always be a function
if ( doSecond && $.isFunction(doSecond) ) {
doSecond();
}
});
The facts:
You have first.js and within this script is an include for second.js
You need to make a call to doSecond() which is defined in second.js
You need to ensure doSecond() is available before you call it
You can't directly change first.js or second.js but you can have someone else change it
Possible solutions, ordered by best to worst
1) Request that second.js be removed from first.js. Call them separately so that you can nest them:
$.getScript("first.js").done(function(){
$.getScript("second.js").done(function(){
doSecond();
});
});
This is the best solution. There are alternatives to this that basically do he same thing in principle (e.g. other people's answers here). If first.js was including second.js synchronously or otherwise forcing load before continuing (e.g. option #3 below), you wouldn't be running up against this problem to begin with. Therefore first.js already must be structured to deal with second.js be *a*sync loaded, so there shouldn't be an issue with them removing it from the file and you calling it yourself.
But you mentioned that the location of second.js is defined in first.js so this somehow isn't feasible to you (why not? can they put the path/to/script in a variable for you to access?)
2) Request that second.js be wrapped in a .done or equivalent loaded callback that pops a callback function that you can define.
// inside first.js
$.getScript("second.js").done(function(){
if (typeof 'secondLoaded'=='function')
secondLoaded();
});
// on-page or elsewhere, you define the callback
function secondLoaded() {
doSecond();
}
This is just a generic and easy "callback" example. There are a million ways to implement this principle, depending on what all is actually in these scripts and how much effort people are willing to make to restructure things.
3) Request that second.js script include be changed to be included via document.write
document.write(unescape("%3Cscript src='second.js' type='text/javascript'%3E%3C/script%3E"));
This will force js to resolve document.write before js can move on, so second.js should be loaded by the time you want to use doSecond(). But this is considered bad practice because until document.write is resolved, nothing else can happen. So if second.js is taking forever to load or eventually times out.. that makes for bad UX. So you should avoid this option unless you have no other choice because of "red tape" reasons.
4) use setTimeout to try and wait for it to load.
function secondLoaded() {
if (!secondLoaded.attempts) secondLoaded.attempts = 0;
if (secondLoaded.attempts < 5) {
if (typeof 'doSecond'=='function') {
doSecond();
} else {
secondLoaded.attempts++;
window.setTimeout('secondLoaded()',100);
}
}
}
secondLoaded();
I list this worse than #3 but really it's kind of a tossup.. In this situation you basically either have to pick between deciding a cutoff time to just not execute doSecond() (in this example, I try 5 times at 100ms intervals), or code it to just keep checking forever and ever (remove the .attempts logic or else swap it up w/ setInterval and removeInterval logic).
You could modify how $.getScript works.
$.fn.getScript = (function() {
var originalLoad = $.fn.getScript;
return function() {
originalLoad.apply($, arguments);
$(document).trigger('load_script')
};
})();
This will fire an event every time a script is loaded.
So you can wait for these events to fire and check if your method exists.
$(document).one('second_loaded', function() {
doSecond();
}).on('load_script', function() {
doSecond && document.trigger('second_loaded');
});
Note that one rather than on. It makes the event fire once.
Have you considered using jQuery.when:
$.when($.getScript("http://myexternaljs.com/first.js"),$.getScript("http://firstexternal.com/second.js"))
.done(function(){
doSecond();
}
If I were you, I'd facade $.getScript and perform some combination of the above tricks. After reading through the comments it seems that there is a possibility of loading the same script twice.
If you use requirejs this problem is solved for you because it only loads each script once. The answer here is to hang on to the requests made.
Loader:
var requiredScripts = {};
function importScript(url) {
if (!requiredScripts[url]) {
requiredScripts[url] = $.getScript(url);
}
return requiredScripts[url];
}
Usage:
// should cause 2 requests
$.when(
importScript('first.js'),
importScript('second.js')
).done(function() {
// should cause no requests
$.when(importScript('first.js')).done(function() {});
$.when(importScript('second.js')).done(function() {});
});
Real world example here using MomentJS and UnderscoreJS: http://jsfiddle.net/n3Mt5/
Of course requirejs would handle this for you and with better syntax.
Requirejs
define(function(require) {
var first = require('first'),
second = require('second');
});
$(window).one("second", function(e, t) {
if ( $(this).get(0).hasOwnProperty(e.type) && (typeof second === "function") ) {
second(); console.log(e.type, e.timeStamp - t);
$(this).off("second")
};
return !second
});
$.getScript("first.js")
.done(function( data, textStatus, jqxhr ) {
if ( textStatus === "success" ) {
first();
$.getScript("second.js")
.done(function( script, textStatus, jqxhr, callbacks ) {
var callbacks = $.Callbacks("once");
callbacks.add($(window).trigger("second", [ $.now() ]));
return ( textStatus === "success" && !!second
? callbacks.fire()
: $(":root").animate({top:"0"}, 1000, function() { callbacks.fire() })
)
});
};
})
// `first.js` : `function first() { console.log("first complete") }`
// `second.js` : `function second() { console.log("second complete") }`

jQuery function execution order

I am having a problem, or perhaps a lack of understanding, with the jQuery execution order of $.get() function. I want to retrieve some information from a database server to use in the $.ready() function. As you all know, when the get returns, it passes the data to a return handler that does something with the data. In my case I want to assign some values to variables declared inside the ready handler function. But the problem is, the return handler of $.get() does not execute until after ready has exited. I was wondering if (a) am I doing this right/is there a better way or if (b) there was a way around this (that is, force the get return handler to execute immediately or some other fix I'm not aware of). I have a feeling this is some closure thing that I'm not getting about JavaScript.
As per request, I'll post an example of what I mean:
$(function() {
var userID;
$.get(uri, function(returnData) {
var parsedData = JSON.parse(returnData);
userID = parsedData.userID;
});
});
So as you can see, I'm declaring a variable in ready. Then using a get call to the database to retrieve the data needed. Then I parse the JSON that is returned and assign the userID to the variable declared before. I've tested it with a couple alerts. An alert after the get shows userID as undefined but then an alert in get's return handler shows it to be assigned.
$.get() is asynchronous. You have to use a callback to fill your variable and do the computation after the request is complete. Something like:
$(document).ready(function(){
$.get( "yourUrl", function( data, textStatus, jqXHR ) {
var myData = data; // data contains the response content
// perform your processing here...
registerHandlers( myData ); // you can only pass "data" off course...
});
});
// your function to register the handlers as you said you need to.
function registerHandlers( data ) {
// registering handlers...
}
$.get is an ajax request. A in AJAX stand for asynchronous, so script won't wait for this request to finish, but instead will proceed further with your code.
You can either use complete callback or you can use $.ajax and set async to false to perform synchronous request.
The $.get() function executes an async httprequest, so the callback function will be executed whenever this request returns something. You should handle this callback outside of $.ready()
Maybe if you explain exactly what do you want to do, it would be easier to help!
Are you looking for something like:
$(document).ready(function(){
var variable1, variable 2;
$.get('mydata.url', function(data){
variable1 = data.mydata1;
variable2 = data.mydata2;
});
});
If you declare the variables first, then you can set their values within the get call. You can add a function call at the end of the get handler to call a separate function using these values? Without some kind of example, its hard to go into any more detail.
Without seeing the full code, my guess is that you should declare your variable outside $.ready; initialize it in ready for the initial page load; then update it from the get callback handler.
for example
var x = ""; // declaration
$(document).ready(function() { x = "initial value"; });
$.get(...).success(function() { x = "updated from ajax"; });

jQuery load access main javascript

I'm having an issue with a prototype where I load pages through
$("#tab").load(/a/relative/pad);
The problem is that I'm unable to access the Javascript in the original environment, is there a way to get around this?
Javascript in the content of a loaded document does not get fired. What you can do is have a the callback of load fire the Javascript method you need.
If the function you need call after the load is dependent on the data you get back from the load, you can get the parameters you need during the callback of load.
$("#tab").load("/a/relative/pad", function(){
//find your parameter
var parameter = $("#parameter", "#tab");
//call your callback method
callback(parameter);
});
If no extra parameters are needed to make the callback:
$("#tab").load("/a/relative/pad", function(){
//call your callback method
callback(parameter);
});
Or if you prefer to not have an anonymous function:
$("#tab").load("/a/relative/pad", callback);
In all these cases callback is a function that exists on page A
Alternatively, while this is not in best practices, you could have a hidden block of text on page B that is actually a script block and you could call ecal() on that script.
$("#tab").load("/a/relative/pad", function(){
//get the block of script you need to run
var scriptToRun = $("#hiddenScriptBlock", "#tab");
//run the script
eval(scriptToRun);
});

Categories