How can I unload a JavaScript resource with all its defined objects from the DOM?
Im developing a simple framework that enables to load html fragments into a "main" html. Each fragment is self contained and may include references to additional JS and CSS files. The JS and CSS resources are parsed and dynamically added to the html. When the fragment is removed/replaced from the DOM I want to remove its JS and CSS.
If I remove the script element in the example below, the functions defined in page1.js are still available.
<html>
<head>
<script src="page1.js" type="text/javascript"></script>
</head>
<body>
...
Is there a way to unload page1.js objects from the DOM?
========= The test code I use =======
I tried the advice i got in the comments below; to delete the added objects using a cleanup function - but even this fails. The sources i used for testing:
<html>
<head>
<script type="text/javascript">
function loadJSFile(){
var scriptTag = document.createElement("script");
scriptTag.setAttribute("type", "text/javascript");
scriptTag.setAttribute("src", "simple.js");
var head = document.getElementsByTagName("head")[0];
head.appendChild(scriptTag);
}
function unloadJSFile(){
delete window.foo;
delete window.cleanup;
alert("cleanedup. typeof window.foo is " + (typeof window.foo));
}
</script>
</head>
<body>
Hello JavaScript Delete
<br/>
<button onclick="loadJSFile();">Click to load JS</button>
<br/>
<button onclick="foo();">call foo()</button>
<br/>
<button onclick="unloadJSFile();">Click to unload JS</button>
</body>
</html>
simple.js source:
var foo = function(){
alert("hello from foo");
}
This cannot be done.
When a script is executed, function definitions are added to the global window object. There may be debugging symbols attached to the function that indicate where the function came from, but this information is not available to scripts.
About the only way you could achieve something like this would be to create a pseudo-namespace in the script and then throw away that whole namespace when you are done with it. However, this question hints to me that you are trying to do something the wrong way. Perhaps elaborating on your scenario would help us provide alternate solutions.
No, that is not possible. You could build a simple cleanup function that removes all variables that were defined in that file:
var foo = 'bar';
var cleanup = function () {
delete window.foo;
delete window.cleanup;
};
// unload all resources
cleanup();
Another approach would be to use a modular object structure for your fragments, that clean up after themselves. That involves a slightly higher overhead but is probably worth it, as it makes the code much easier to read and maintain:
// creates the object using the second parameter as prototype.
// .create() is used as constructor
GlobalModuleHandlerThingy.addModule('my_module', {
create: function () {
this.foo = 'bar';
return this;
},
foo: null,
destroy: function () {
// unload events, etc.
}
});
GlobalModuleHandlerThingy.getModule('my_module').foo; // => bar
GlobalModuleHandlerThingy.unloadModule('my_module'); // calls .destroy() on the module and removes it.
perhaps you need to consider conditionally loading it rather than conditionally unloading it...
you can make them = null
function fnc1 (){
}
window.fnc1 = null
//or
window["fnc1"] = null
If you need to unload a specific object, it's fairly easy: just set it to {}
ie: myobj = {};
So if you know what objects are created in a particular include, it won't be hard at all.
On the other hand, if you don't know what objects are created in a particular include, there isn't a mechansim to find out - you can't ask Javascript to tell you what was defined in a particular include.
However, I would say that if you don't know what objects are being loaded in a particular javascript file, you're probably not doing yourself any favours in loading it (you should always have a reasonable idea what code does in your site), or in trying to unload it manually (if you don't know what it does, that implies its a third party include, which means that unsetting it manually is likely to break things).
Was researching for something like that myself and thought I'll post my findings
Wrap your stuff in a global namespace in js file so it can be removed easily, ie
var stuff = { blabla: 1, method: function(){} };
When you need to get rid of it, simply set stuff = {}, or null even
Remove script tag from page
*** If you use requirejs - require js remove definition to force reload
Note: as long as you don't reference modules inside the namespace from anywhere else everything will be collected by GC and you are good to go.
I figured a trick for this. I was wondering here days finding an answer for this and I just realized a perfect trick to do this without trying to unload the java Script. only you have to do is create a global variable like currentPage in your main page's java script and when you loading the page assign the page name to currentPage . then in every other .js file use $('document').ajaxComplete() insted of $('document').ready() add an if statement as first line inside every $('document').ajaxComplete() function. set it to check currentPage variable equals to a new page name. add all other events inside if statement. i don't know English very well so check my code. and This is my first answer here so sorry if i make some mistakes.
main.html
<body>
<div id='container></div>
<button id="load1">
<button id="load1">
</body>
main.js
var currentPage = "";
$(document).ready(function () {
$('#load1').click(function () {
loadSource('page1', 'body');
});
$('#load2').click(function () {
loadSource('page2', 'body');
});
});
function loadSource( page, element){
currentPage = page;
$('#container').load('views/' + page + '.php', element);
$.getScript('js/' + page + '.js');
$('#css').prop('disabled', true).remove();
$('head').append('<link rel="stylesheet" href="css/' + page + '.css" type="text/css" />');
}
all of my pages scripts and styles are in seperate folders views, js, css.
page1.html
<body>
<button id="test1">
<button id="test2">
</body>
page1.js
$(document).ajaxComplete(function () {
if(currentPage == 'page1'){
/*$('#test1').click(function () {
console.log('page1');
});*/
$('#test2').click(function () {
console.log('page1');
});
}
});
page2.html
<body>
<button id="test1">
<button id="test2">
</body>
page2.js
$(document).ajaxComplete(function () {
if(currentPage == 'page2'){
$('#test1').click(function () {
console.log('page2');
});
/*$('#test2').click(function () {
console.log('page2');
});*/
}
});
i commented one button in each script to check if that button still has old script's affect.
Related
On the SO-chat, I was advised to ask the following question here:
I would like to load some Javascript globally on my complete website, via a Drupal theme's Javascript; insead of having to re-insert the Javascript-code each article over and over again.
For example, I have managed this with the following code (imagine it wrapped in <script type="text/javascript">...</script>, when inserted locally):
(which worked fine both locally when inserted per page, and also when loaded in to the Drupal theme's javascript globally as such):
(function ($) {
$(document).ready(function () {
$
$(".toggler").click(function () {
$(this).next().slideToggle("slow");
}).next().hide();
$("a[href^=#]").click(function () {
var id = $(this).attr('href');
$(id).parents('.toggled').fadeIn("fast");
});
});
})(jQuery)
However, for 2 other pieces of Javascript, I can't get things to work when loading them globally. However, they work great when inserted locally in to each consecutive article, as such (I'll mention just 1 smaller code, for reference):
(imagine it wrapped in <script type="text/javascript">...</script> again, when inserted locally):
(function($) {
$(document).ready(function() {
function getKey(element) {
return element.href;
}
function sameGroupAs(element) {
var key = getKey(element);
return function() {
return getKey(this) === key;
}
}
$(document).on("mouseenter", "a", function() {
$("a").filter(sameGroupAs(this)).addClass(
"active");
}).on("mouseleave", "a", function() {
$("a").filter(sameGroupAs(this)).removeClass(
"active");
});
});
})(jQuery)
Just for reference: if you would be interested how I added the Javascript to a Drupal's theme.
This problem is unrelated to Drupal, and is just a javascript thing. I suppose (but did not test) it occurs because you are defining an anonymous function (function($) {...})(jQuery) twice. As in all languages two function names may never be the same. Details should have been visible in the JavaScript console of your browser if you want to know for sure.
Apart from that, it is neater to combine both scripts into one, as they both fire when the document has loaded. If you require more visibility on what part of the script performs a certain function, splitting it over multiple files (as you already discovered) works as well.
The solution was simply to put every single "script" in a seperate .js-file. If they were put into one .js-file, they would simply all become dysfunctional.
Anyone knows whether this is a Drupal-specific strategy, or whether this is general to Javascript and known to everyone in "the field"?
The situation where jQuery is loaded late on the page but javascript that relies on jQuery being available is loaded before jQuery is a pretty common scenario, especially if you follow the practice of putting your scripts closer to </body>.
So basically I want to go from this:
<script>
someFunctionThatUsesLateJQuery(){ [code that relies on jQuery] }
</script>
...
<script src="jquery.js"></script>
<script>
$(function(){
someFunctionThatUsesLateJQuery();
});
</script>
To something like this:
<script>
_$.ready(function(){ [code that relies on jQuery] });
</script>
...
<script src="jquery.js"></script>
Much like asynchronous stats tracking (รก la Google Analytics), is there anything out there that allows you to register javascript function calls to be executed once jQuery is loaded without getting the dreaded $ is undefined error?
I mean for this to happen without registering and deregistering timeouts/intervals.
Has there ever been/is there the possibility of adding some sort of pre-registration variable to jQuery that it recognises and handles once it's loaded?
Use case:
It's worth noting that the specific use-case I've got is a drop-in JS widget, where I want some DOM manipulation to happen at the scene of the <script> placement, which therefore has the very real possibility of appearing before jQuery has loaded in the case of jQuery loading happening near </body>.
I then don't want to burden the user further by requiring them to register a specific function call at the correct point in code execution (which will be dependent on their implementation)... I want it to "just work"
As suggested here by rich.okelly you could try this:
(function() {
var runMyCode = function($) {
// jquery-dependent code here
$("#foo").data('bar', true);
};
var timer = function() {
if (window.jQuery && window.jQuery.ui) {
runMyCode(window.jQuery);
} else {
window.setTimeout(timer, 100);
}
};
timer();
})();
I also found a more complex solution, but I personally prefer the above solution over this solution.
Update without timeout:
<script>
var runMyCode = function($) {
// jquery-dependent code here
$("#foo").data('bar', true);
};
</script>
...
<script src="jquery.js"></script>
<script>
$(function(){
runMyCode(window.jQuery);
});
</script>
You could try to use common and popular solution when all your function calls moved to data-attributes. Then, when jQuery is loaded, you go throw all the DOM elements with such attributes and run this functions.
I.e. instead of function call you add data-type='load' data-function='YourNamespace.yourFunctionName' and after window.load event you select all the elements by $('[data-type="load"]'), iterates them, and perform function calls.
UPD: Guess, async function queuing pattern could be usefull to read about:
What's the name of Google Analytics async design pattern and where is it used?
If you inject the jQuery script include, then you can listen for the onload event.
function loadJS(url, onloadCallback, elId){
//Inject script include into HEAD
var scriptEl = document.createElement('script');
scriptEl.type = 'text/javascript';
scriptEl.src = url;
if(elId)scriptEl.id = elId;
document.getElementsByTagName('head')[0].appendChild(scriptEl);
if(onloadCallback){
scriptEl.onload = scriptEl.onreadystatechange = function(){
if(!scriptEl.readyState || (scriptEl.readyState === 'complete' || scriptEl.readyState === 'loaded')){
onloadCallback();
scriptEl.onload = scriptEl.onreadystatechange = null;
}
}
}
}
loadJS("http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js", function(){
console.log( window.$ );
});
Perhaps I don't understand the use case fully, but RequireJS sounds like it might be a good option for you:
https://github.com/requirejs/example-jquery-shim
define(["jquery", "jquery.alpha", "jquery.beta"], function($) {
//the jquery.alpha.js and jquery.beta.js plugins have been loaded.
$(function() {
$('body').alpha().beta();
});
});
I made a little script that makes my JS initialization in Partial Pages a bit easier.
It simply searches for all data-onload attributes, and executes the function defined there on-load.
There is also some other functionality. So is the data-onload called automatically when that specific partial view is loaded through an AJAX call.
Anyway, the syntax looks like this:
<div class="some-partial-html-stuff">
<button>[...]</button>
</div>
<script data-onload="partialInit">
function partialInit()
{
// executes onload and on-ajax-load stuff for this Partial Page
$('.some-partial-html-stuff button').doSomething();
}
function otherFunctions()
{
// [...]
}
</script>
The only thing that I still would love to tackle is that right now I need to have a unique functionName for every partial page (otherwise the names will clash when they are both loaded).
So I have manageProfileInit(), editImageInit() etc.
Now is the OCD-devil in me wondering if there is some way to clean this up even further (without too many negative consequences). I would love to have the situation where I can have a simple clean functon init() in any scriptblocks, and have the same funcionality described above.
Of course in the current situation all the functions will override each other. But does anyone know a nice trick or workaround how this could work?
To summarize, I want to make a script that makes sure this will work on every Partial Page, without any clashes.
<div class="some-partial-html-stuff">
<button>[...]</button>
</div>
<script data-autoinit>
function init()
{
// this method is automatically called if the 'data-autoinit' is defined
// executes onload and on-ajax-load stuff for this Partial Page
$('.some-partial-html-stuff button').doSomething();
}
</script>
When I do stuff like this, I call them features. Tags look like this:
<div data-feature="featureName"></div>
Then we get all of the tags that have the data-feature tag and loop over them, creating an array of features the page is going to use:
var featureObjects = $('[data-feature]');
var features = [];
if ( !featureObjects.length ) return false;
for ( var i = 0, j=featureObjects.length; i<j; i++ ) {
var feature = $(featureObjects[i]).data('features');
if ($.inArray(feature, features) == -1){
if (feature !== ""){
features.push(feature);
}
}
};
Now you'll want to load the JS file asychronously and call it's init function once it's loaded:
for (var i=0, j=features.length; i<j; i++){
var feature = features[i];
$.ajax({
url: "path/to/js/" + feature + ".js",
dataType: "script",
async: false,
success: function () {
App.features[feature].init();
},
error: function () {
throw new Error("Could not load script " + script);
}
});
}
The actual modules look like this and attach themselves to App.features for later use:
App.features.featureName = (function(feature){
// INIT FUNCTION
feature.init = function(){
};
return feature;
}(App.features.featureName || {}));
Just remember to make sure App.features is an array before doing all of this, hopefully somewhere towards the top of your main.js file. I keep other functionality such as helpers and utilities in the app, so I usually kick it off with something like:
var App = {
utilities: {},
features: {},
helpers: {},
constants: {}
};
Now you can just tag DOM objects with a data-feature tag and functionality will be added automatically and as-needed, keeping a nice tie between specific JavaScript and specific DOM, but without the need of having to keep the JS inline next to the actual DOM. It also makes those "blurbs" re-usable should they need to be used elsewhere, which lowers maintenance overhead when working on your application.
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.
My requirements are the following:
I've got a rich webpage that at a certain moment loads a bunch of HTML in a div, via AJAX.
The HTML I retrieve does have javascript (<script>...</script>)
The retrieved javascript contains $('document').ready( ... ) parts
I can not modify the retrieved javascript; it comes from an external lib
I've got a javascript function that is called when the AJAX is loaded. I'm trying to "trick it" into executing by doing:
function AjaxLoaded() {
$('document').trigger('ready');
}
That doesn't cut it, I'm afraid.
I've seen several responses on Stack Overflow that "evade" this question by changing the code that is returned on the AJAX (make it a function and call it after loading, or just remove the $(document).ready()). I need to stress out that I can't change the retrieved code on this case.
Afer some research i created a way to get it to work.
here is my test that shows it working: http://www.antiyes.com/test/test2.php
here is the relevant code:
<script>
// easy copy of an array
Array.prototype.copy = function() {
return [].concat(this);
};
// this function is added to jQuery, it allows access to the readylist
// it works for jQuery 1.3.2, it might break on future versions
$.getReadyList = function() {
if(this.readyList != null)
this.myreadylist = this.readyList.copy();
return this.myreadylist;
};
$(document).ready(function() {
alert("blah");
});
</script>
<script>
// this should be added last so it gets all the ready event
$(document).ready(function() {
readylist = $.getReadyList();
});
</script>
then in the body I have:
<input type="button" onclick="$(readylist).each(function(){this();});" value="trigger ready" />
basically what i did was add a function to jQuery that copies the readyList before it's cleared out, then it will be available to be used by you.
it looks like the code below doesnt work:
function AjaxLoaded() {
$(document).trigger('ready');
}
drop the quotes around document.
Since the jQuery readyList is not exposed as of version 1.4 (discussed here) the nice solutions above are broken.
A way around this is by creating your own readyList, through overriding the original jQuery-ready method. This needs to be done before other scripts that use the original ready method are loaded. Otherwise just the same code as John/Kikito:
// Overrides jQuery-ready and makes it triggerable with $.triggerReady
// This script needs to be included before other scripts using the jQuery-ready.
// Tested with jQuery 1.7
(function(){
var readyList = [];
// Store a reference to the original ready method.
var originalReadyMethod = jQuery.fn.ready;
// Override jQuery.fn.ready
jQuery.fn.ready = function(){
if(arguments.length && arguments.length > 0 && typeof arguments[0] === 'function') {
readyList.push(arguments[0]);
}
// Execute the original method.
originalReadyMethod.apply( this, arguments );
};
// Used to trigger all ready events
$.triggerReady = function() {
$(readyList).each(function(){this();});
};
})();
I'm not sure whether it is advisable to override the ready method. Feel free to advise me on that. I have not yet found any side effects myself though.
Just in case anyone needs it, I refined John's solution a bit so it could be used directly as an included javascript file.
// jquery_trigger_ready.js
// this function is added to jQuery, it allows access to the readylist
// it works for jQuery 1.3.2, it might break on future versions
$.getReadyList = function() {
if(this.readyList != null) { this.myreadylist = [].concat(this.readyList); }
return this.myreadylist;
};
$(document).ready(function() {
readylist = $.getReadyList();
});
$.triggerReady = function() {
$(readylist).each(function(){this();});
}
Including this file after including jquery allows for triggering ready by invoking $.triggerReady(). Example:
<html>
<head>
<title>trigger ready event</title>
<script src="test2_files/jquery-1.js" type="text/javascript"></script>
<script src="jquery_trigger_ready.js" type="text/javascript"></script>
</head>
<body>
<input onclick="$.triggerReady();" value="trigger ready" type="button">
<script type="text/javascript">
$(document).ready(function(){
alert("blah");
});
</script>
</body>
</html>
By the way, I wanted to make it $(document).triggerReady(). If anyone is willing to share some advice on that, ill be appreciated.
We had the same problem and solved it another way.
Instead of
$(document).ready(function () {
$('.specialClass').click(....
We used :
$(document).bind('ready', function(event) {
$('.specialClass', event.target).click(..
jQuery will trigger a "ready" event on the document as usual. When we load the content of a new div via ajax, we can write:
loadedDiv.trigger('ready')
And have all the initialization performed only on the div, obtaining what expected.
Simone Gianni's Answer I think is the most elegant and clean.
and you can even simplify it to become even more easy to use:
jQuery.fn.loadExtended = function(url,completeCallback){
return this.load(url,function(responseText, textStatus, XMLHttpRequest) {
if (completeCallback !== undefined && completeCallback !== null) {
completeCallback(responseText, textStatus, XMLHttpRequest);
}
$(this).trigger("ready");
});
};
So, now instead of using:
$(".container").load(url,function(responseText, textStatus, XMLHttpRequest) {
$(this).trigger("ready");
});
you can just use:
$(".container").loadExtended("tag_cloud.html");
or:
$(".container").loadExtended("tag_cloud.html",function(){
alert('callback function')
});
This has the advantage of only applying the trigger on the div that's being updated.
If your new loaded HTML contain <script> elements and you try insert it into main HTML with pure JS (element.innerHTML = newHTML), then $(document).ready handlers at newHTML and wrapped functions like (function() { /* some functions */ })(); - will not execute because JQuery unbind 'ready' event after first triggering and you can not trigger it repeatly. PS. But you can use $.holdReady(true) and trigger when need.
So, try insert code with jquery method, $(element).html(newHTML). This solved similar problem for me, seems jquery handle js before inserting. Using this method you also will not see the <script> elements among DOM nodes (at browser's Elements Inspector for ex.)