Script not loaded directly - javascript

I'm loading a HTML partial through ajax. The partial is attached to the DOM by using innerHTML on an existing node.
The partial contains a few script tags at the bottom, something like:
<script src="/Scripts/Griffin.Editor.js" type="text/javascript"></script>
<script type="text/javascript">
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});
var textParser = {
parse: function (text) {
return marked(text);
}
}
var prismHighlighter = {
highlight: function (blockElements, inlineElements) {
blockElements.forEach(function(item) {
Prism.highlightElement(item);
});
}
};
var editor = new Griffin.Editor('editor', textParser);
editor.syntaxHighlighter = prismHighlighter;
editor.preview();
</script>
However, as the script tags are not executed, I traverse the loaded partial to identify all script tags. I then create new script nodes in the DOM and attach them to the HEAD.
Something like:
var scripts = viewElem.getElementsByTagName('script');
for (let i = 0; i < len; i++) {
var scriptTag = scripts[0];
let node = document.createElement('script');
if (scriptTag.src && scriptTag.src.length > 0) {
node.src = scriptTag.src;
node.type = scriptTag.type;
} else {
node.text = scriptTag.text;
node.type = scriptTag.type;
//had eval here before (instead of attaching the embedded script to the HEAD).
}
document.head.appendChild(node);
scriptTag.parentNode.remove(scriptTag);
}
From what I understand the browser should load the referenced scripts before invoking the embedded script. That is however not the case for me, because the JS console complains about not finding the object defined in the dependency script.
If I use a timer and eval the embedded script in it everything works. But that seems as a ugly workaround and I really want to understand the mechanics behind the load behavior (i.e. why the scripts are not executed when the partial is attached to the DOM and why the referenced scripts are not loaded directly when I add the nodes to the HEAD tag).

From what I've encountered, you can't have immediately executing JavaScript inside an Ajax response. The reason being you are trying to execute JavaScript inside another JavaScript function. So the browser has no idea which executing context to use with this scenario.
I would recommend using deferred execution like you mentioned. Except, you need to let the browser interpret the Ajax response first. For example:
$.get('url', function (html) {
// html = "<script>function myTest () { console.log('here'); }</script>"
$('#result').html(html);
// Now that the DOM has had time to parse the response we can do:
myTest();
});
Notice it is the Ajax callback invoking the response function not the response immediately executing itself. Hope this helps.

I found a really great article explaining in depth how scripts are loaded into the browser.
In essence you can't be sure of execution order per default when you include scripts dynamically. To be sure of the order you need to do one of the following
a. Use async=false if supported
b. Use readyState (for ie<10)
c. Use defer attribute.
Try to use the mentioned features in that order to be sure.
However, even if you do all that you will still get screwed if you mix embedded scripts (code in in the script tag) with referenced scripts (using src attribute).
The problem is that the embedded scripts will run directly, even if the references script tags are added before. To solve that you need to push the embedded scripts into a queue and hook the load event for all referenced scripts.
Once all referenced scripts have toggled the load even you are free to invoke the embedded scripts (either by added the script tags to an element or by eval their text property).
Source: http://blog.gauffin.org/2015/07/embedded-script-tags-in-content-loaded-through-ajax-and-execute-the-script-tags-dynamically/

Related

Inject external HTML & JS into a page

I'm trying to build some small widgets tools that webmasters can embed in their websites.
Is there any way that the webmaster can simply load this tool by including a script like this?
<script src="http://mysite/widget.js"></script>
I tried to inject it by loading it in AJAX and then do an appendChild() on the body, it's working this way but the JS is not executed.
Content to be injected:
<div>one div</div>
<script>alert('some js')</script>
widget.js
function load(content) {
var $body = document.body,
$div = document.createElement("div");
$div.id = "widget";
$div.innerHTML = content;
$body.appendChild($div);
}
The content variable contains HTML and JS but the JS is not executed when injected.
Since you don't know where you script will be added - to the head or body tag - and consequently when it will be executed, you need to make sure that DOM is ready, so:
function load(content) {
if (document.readyState === "complete" || document.readyState === "loaded") {
// manipulate DOM
} else {
document.addEventListener('DOMContentLoaded', function() {
// manipulate DOM
})
}
Also, since scripts elements added using innerHTML don't get executed, you'd have to either use eval or better create a script like that:
var s = document.createElement('script');
s.text = "alert(\"hi\");"
document.getElementsByTagName('head')[0].appendChild(s);
However, there is no point to add <script> tag to execute some script in your template, since your script is already running when you're adding DOM so do all necessary setups there.
UPDATE
please see Maximus' answer.
Right, the default native innerHTML doesn't execute js for safe sake.
jQuery or zepto $.fn.append() can satisfy your need, here is the reason (from zepto source code)
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
(!el.type || el.type === 'text/javascript') && !el.src)
window['eval'].call(window, el.innerHTML)
You can see in the function $.fn.append it will see if it's a script, if so it will use eval to run the content. You may need eval to, but be careful since some webmasters may deliver 'vice' code

How do I execute Javascript only after a deferred Javascript external file has loaded?

I want to defer loading of an external Javascript file until the page has loaded (and also I don’t want to block rendering if those external resources cannot be loaded). So I have this line
<script type="text/javascript" src=“//external.otherdomain.com/path/js/myresoures.js" defer></script>
However, when this file does eventually load, I want to run the following …
<script type="text/javascript">_satellite.pageBottom();</script>
However, as is, the above may run before the script has loaded. How do I run the above (or any) Javascript only after a remote resource has loaded? Keep in mind that if the external resource doesn’t load (it times out, for example), I don’t want to run the Javascript.
Thanks, - Dave
This can be achieved by an onload function.
You can dynamically load each... This example is pulled right from MDN. It will insert your external resource above the script you call importScript() then launch the callback after loading.
sSrc = your external script, fOnload = the callback
function importScript (sSrc, fOnload) {
var oScript = document.createElement("script");
oScript.type = "text\/javascript";
oScript.defer = true;
if (fOnload) { oScript.onload = fOnload; }
document.currentScript.parentNode.insertBefore(oScript, document.currentScript);
oScript.src = sSrc;
}
Usage:
importScript("//external.otherdomain.com/path/js/myresoures.js", function () { _satellite.pageBottom(); });
Or if jQuery is an option, use .getScript() like so:
$.getScript( "//external.otherdomain.com/path/js/myresoures.js", function () {_satellite.pageBottom();} );
Either of these options can be used in a <script> at the end of the page, after the fold as they say, ensuring the entire DOM loads first.
You can use jQuery's $(window).load();
see: http://api.jquery.com/load/

Searching for for javascript within page via Chrome extension

I wanted to build a simple Chrome extension that would search the HTML/DOM of the current active tab and print out in a popup the number of elements that contained javascript matching a certain source.
I read in the Chrome extension guides that the Content Scripts are unable to either interact with or even see other javascript on the page, leading me to believe this is not possible. Does anyone know for sure if creating this type of extension is feasible?
I did something similar not long ago; I needed to see elements' onclick and other attributes, which is not normally possible:
It's worth noting what happens with JavaScript objects that are shared by the page and the extension - for example, the window.onload event. Each isolated world sees its own version of the object.
There is a technique of injecting code into the page's context. Such code can reach the window's JS context and then pass it to your content script. In my case, I just added an extra attribute to nodes with JS attached.
// Fill inline handler copies
function fillClickHandlers(callback) {
var injected = function() {
// Note: This executes in another context!
// Note: This assumes jQuery in the other context!
$("[onclick]").each(function() {
this.dataset["onclick"] = this.attributes["onclick"].value;
});
$("[onsubmit]").each(function() {
this.dataset["onsubmit"] = this.attributes["onsubmit"].value;
});
$("[onload]").each(function() {
this.dataset["onload"] = this.attributes["onload"].value;
});
}
var s = document.createElement('script');
s.textContent = "(" + injected + ")();";
(document.head||document.documentElement).appendChild(s);
// Script is synchronously executed here
s.parentNode.removeChild(s);
callback();
}
// Erase inline handlers copies
function eraseClickHandlers(callback) {
$("[data-onclick], [data-onsubmit], [data-onload]").each(function() {
delete this.dataset.onclick;
delete this.dataset.onsubmit;
delete this.dataset.onload;
});
callback();
}
// Usage:
fillClickHandlers(function() {
doActualWork(function() {
eraseClickHandlers(doSomethingElse)
});
});
Note that for actual <script> tags, you can freely inspect src or textContent attribute.

Why declare inside of $(function () { ... })?

The application I am looking at loads an external javascript file which looks like this:
$(function () {
// Don't allow browser caching of forms
$.ajaxSetup({ cache: false });
var dialogs = {};
var getValidationSummaryErrors = function ($form) {
// We verify if we created it beforehand
...
...
}
return errorSummary;
};
I understand that the file setups up some variables and also declares a function called getValidationSummaryErrors.
What I don't understand is why is this all within
$(function () { ... }
What's the purpose of this? Can I not just declare the variable and things inside the flat file without the "$(function () { }" ?
$(function() { ... }); is just short for $(document).ready(function() { ... });, which ensures that the code is not executed until the DOM is ready, otherwise some code that affects the DOM may not work properly.
See http://api.jquery.com/ready/
$() is shortcut for jQuery.ready(), which executes code after page DOM is fully loaded. Sometimes you want to make sure that document is ready before you do certain things.
$(function () { ... });
Means that the function will run after the page (DOM part) is loaded, not when the code gets parsed. This you can make sure that the page is loaded faster, and also everything necessary is available for the javascript to run.
This is a concise notation for $(document).ready(function() {...}) ". NOTE : the jQuery document ready fires when the DOM has been loaded. It doesn't wait for entire page (included images and the like) to load.
Practically, any script that you put into the <head> executes immediately i.e. if the Script interacts with the DOM it needs to be ready.
Thirdly it is needed for separations of concerns. Ideally your javaScript and HTML are in separate files. If you follow this you will not have any in-line script tags in your HTML at all.

How to detect if javascript files are loaded?

Is there an event that fires when JavaScript files are loaded? The problem came up because YSlow recommends to move JavaScript files to the bottom of the page. This means that
$(document).ready(function1) is fired before the js file that contains the code for function1 is loaded.
How to avoid this kind of situation?
I don't have a reference for it handy, but script tags are processed in order, and so if you put your $(document).ready(function1) in a script tag after the script tags that define function1, etc., you should be good to go.
<script type='text/javascript' src='...'></script>
<script type='text/javascript' src='...'></script>
<script type='text/javascript'>
$(document).ready(function1);
</script>
Of course, another approach would be to ensure that you're using only one script tag, in total, by combining files as part of your build process. (Unless you're loading the other ones from a CDN somewhere.) That will also help improve the perceived speed of your page.
EDIT: Just realized that I didn't actually answer your question: I don't think there's a cross-browser event that's fired, no. There is if you work hard enough, see below. You can test for symbols and use setTimeout to reschedule:
<script type='text/javascript'>
function fireWhenReady() {
if (typeof function1 != 'undefined') {
function1();
}
else {
setTimeout(fireWhenReady, 100);
}
}
$(document).ready(fireWhenReady);
</script>
...but you shouldn't have to do that if you get your script tag order correct.
Update: You can get load notifications for script elements you add to the page dynamically if you like. To get broad browser support, you have to do two different things, but as a combined technique this works:
function loadScript(path, callback) {
var done = false;
var scr = document.createElement('script');
scr.onload = handleLoad;
scr.onreadystatechange = handleReadyStateChange;
scr.onerror = handleError;
scr.src = path;
document.body.appendChild(scr);
function handleLoad() {
if (!done) {
done = true;
callback(path, "ok");
}
}
function handleReadyStateChange() {
var state;
if (!done) {
state = scr.readyState;
if (state === "complete") {
handleLoad();
}
}
}
function handleError() {
if (!done) {
done = true;
callback(path, "error");
}
}
}
In my experience, error notification (onerror) is not 100% cross-browser reliable. Also note that some browsers will do both mechanisms, hence the done variable to avoid duplicate notifications.
When they say "The bottom of the page" they don't literally mean the bottom: they mean just before the closing </body> tag. Place your scripts there and they will be loaded before the DOMReady event; place them afterwards and the DOM will be ready before they are loaded (because it's complete when the closing </html> tag is parsed), which as you have found will not work.
If you're wondering how I know that this is what they mean: I have worked at Yahoo! and we put our scripts just before the </body> tag :-)
EDIT: also, see T.J. Crowder's reply and make sure you have things in the correct order.
Take a look at jQuery's .load() http://api.jquery.com/load-event/
$('script').load(function () { });
Further to #T.J. Crowder 's answer, I've added a recursive outer loop that allows one to iterate through all the scripts in an array and then execute a function once all the scripts are loaded:
loadList([array of scripts], 0, function(){// do your post-scriptload stuff})
function loadList(list, i, callback)
{
{
loadScript(list[i], function()
{
if(i < list.length-1)
{
loadList(list, i+1, callback);
}
else
{
callback();
}
})
}
}
Of course you can make a wrapper to get rid of the '0' if you like:
function prettyLoadList(list, callback)
{
loadList(list, 0, callback);
}
Nice work #T.J. Crowder - I was cringing at the 'just add a couple seconds delay before running the callback' I saw in other threads.
I always make a call from the end of the JavaScript files for registering its loading and it used to work perfect for me for all the browsers.
Ex: I have an index.htm, Js1.js and Js2.js. I add the function IAmReady(Id) in index.htm header and call it with parameters 1 and 2 from the end of the files, Js1 and Js2 respectively. The IAmReady function will have a logic to run the boot code once it gets two calls (storing the the number of calls in a static/global variable) from the two js files.
Change the loading order of your scripts so that function1 was defined before using it in ready callback.
Plus I always found it better to define ready callback as an anonymous method then named one.
Like T.J. wrote: the order is defined (at least it's sequential when your browser is about to execute any JavaScript, even if it may download the scripts in parallel somehow). However, as apparently you're having trouble, maybe you're using third-party JavaScript libraries that yield some 404 Not Found or timeout? If so, then read Best way to use Google’s hosted jQuery, but fall back to my hosted library on Google fail.

Categories

Resources