I have hunted around for answer to this one, and though have found related quesions, I couldn't quite find an exact match for this.
I have a fairly large app which is supposed to load pages into divs in another page using the jQuery.load() method. The problem I have is that when loading the same page over and over again into the same div, I see the memory of the browser increase substantially (memory leak). If I call $("*").unbind, I of course do not see a leak, but then everything has been reset, so this isn't reallya fix. The following code example reproduces this problem:
Test1.htm
<head>
<title></title>
<script type="text/javascript" language="javascript" src="Scripts/jquery-1.3.2.js"></script>
<script type="text/javascript" language="javascript">
<!--
var i = 0;
$(document).ready(function() {
$("#btn").click(
function() {
i++;
$("#Test1").load("Test2.htm", null, function() {
//$(document).trigger("test");
})
$("#count").html(i);
});
});
//-->
</script>
</head>
<body>
<img id="btn" src="someimage.png" />
<h3>We are loading Test2.htm into below div</h3>
<div>
Count loads =<span id="count">0</span>
</div>
<div id="Test1" style="border-style:solid">EMPTY</div>
</body>
Test2.htm = any old html page..
If you load Test1.htm and click the button mutliple times, you'll notice the browser memory steadily increasing. I believe the problem is that the loaded js and DOM elements are never set for garbage collection. In my real world system I have tried removing (elem.remove() or .empty()) the loaded elements, but this doens't really fix the problem. I also have many js files loaded using "src", which I replaced with $.getScript, this seems to have had made a small improvement. These are all workarounds thought, and I wanted to find a real solution for this problem. Any ideas?
Edit: update due to more info provided about test2.htm (the page being loaded)
Original answer (for historical purposes): I don't actually see any leaks in the code/markup you have provided - is it possible the leak is in Test2.htm (which you haven't provided the code/markup for)?
New answer:
I would suggest that it it probably due to either multiple loads of jQuery, or other scripts you have in test2.htm.
Assuming jQuery does not leak by simply instantiating and then nullifying jQuery and $, loading multiple times will keep at least 2 copies of jQuery in memory. When loaded, jQuery keeps a backup of any previous versions of $ and jQuery in _$ and _jQuery - so you are going to have at least 2 copies of jQuery loaded when you use load() multiple times.
The above assumption is most likely not correct however - there is every chance that jQuery has leaks even if you "unload" it by setting $,jQuery,_$ and _jQuery to null - it's not really intended to be loaded multiple times like that (however I'm sure that they allow it intentionally, so you can use noConflict() to load and use two different versions of jQuery if necessary).
You can add a "selector" to a load URL. For example:
$("#Test1").load("Test2.htm body", null, function() {
//callback does nothing
});
//or
$("#Test1").load("Test2.htm div#the_Div_I_Want", null, function() {
//callback does nothing
});
I would suggest doing this if you are not interested in any scripts in the ajax result, or alternatively if you do want scripts, you'd need to choose a selector to disable only certain elements/scripts, e.g.
/* load with selector "all elements except scripts whose
src attribute ends in 'jquery.js'" */
$("#Test1").load("Test2.htm :not(script[src$='jquery.js'])", null, function() {
//callback does nothing
});
Also of note is that if you leave out the "data" argument (you have it as null), and provide a function as the second argument, jQuery will correctly determine that the second argument is the callback, so
$("#Test1").load("Test2.htm :not(script[src$='jquery.js'])", function() {
//callback does nothing
});
is acceptible
Hmm perhaps it's just something really basic, but if i set $.ajaxSetup({ cache: false }); before the load calls, I don't seem to get the problem. Now, of course my "real" code has this call, so why might I see a problem? I believe the Tabs UI extension is causing caching to be switched on (I don't actually believe this, but invoking the false cache call before each load seems to fix it!!)
Ok so I finally found the problem and it's not a leak at all (which I suspected), it's simply the result of attaching multiple very complex handlers to the same trigger/event. I raised this question relating to that:
JQuery event model and preventing duplicate handlers
Related
Whats the difference? I have on $(document).ready function which should check if extjs is loaded but the main problem is extjs does not load on time and things inside $(document).ready starts to execute, extjs create function which produces the main error 'cannot execute create of undefined' on Ext.create("...", {..}); line. If i put double check like this:
$(document).ready(function() {
Ext.onReady(function() {
Ext.create('Ext.Button', {...});
});
});
Things magically work. Now I'm using ext-all.js which has ~1.3MB minified which is pretty large imho...and things get magically loaded while he does the second check...but I think those 2 functions are not the same as they definitions suggest, because if I put another $(document).ready instead of Ext.onReady() line, things break again. I think Ext.onReady({}); function does some other black magic which $(document).ready() does not, and I'm interested if someone knows what is this kind of magic?
Because it work's and I don't know why which is killing me.
Thanks for reading the post. =)
ps. I'm using ExtJS for about day so I'm pretty new to it.
No they're not the same, the first one will proc when your jQuery library is loaded, the Ext.onReady(.. will proc when your ExtJS library is loaded.
If you want to combine them you could do something like this:
var extReady = false;
var jQueryReady = false;
var librariesReady = function () {
if (jQueryReady && extReady) {
//They're both ready
}
};
$(document).ready(function () {
jQueryReady = true;
librariesReady();
});
Ext.onReady(function () {
extReady = true;
librariesReady();
});
Ext.onReady() and $(document).ready() have nothing to do about either library being loaded as the current accepted answer suggests.
According to the documentation both are about the DOM being loaded and ready.
Documentation
Ext JS: https://docs.sencha.com/extjs/6.7.0/modern/Ext.html#method-onReady
jQuery: https://api.jquery.com/ready/
An Answer to Your Case
It's possible that you're loading the Ext JS resource after your script fires, but jQuery is already loaded above your script. Thus using jQuery to wait until the DOM is loaded guarantees that the DOM has been loaded and thus by then Ext JS has also been loaded.
If you try to invert them and us Ext JS first you'll likely have an error.
According to the documentation they're doing the same thing so you shouldn't need to nest them
A Fix for this Scenario
If you are loading your resources like so:
jQuery
Your Script
Ext JS
It would be best to load them in this order:
jQuery and/or Ext JS
Order shouldn't matter as they can stand by themselves without requiring one or the other
Your Script
Additional Explanation
Due to how the DOM is loaded and parsed by the time it reads your script it guarantees that jQuery and Ext JS are available. This is why you can reference their libraries in your script; you're not waiting for them to load they're already there and available to be used which is why you can call them and use their ready calls.
You need to use the ready event of one of the libraries to guarantee that all elements are loaded into the DOM and available to be accessed. You also shouldn't try to add anything to the DOM until it's ready although you can append to current elements that have been loaded above your element/script tag. It's just best practice to not touch the DOM until it's finished loading.
Additional Explanation Nobody Asked For 🔥
Handling DOM ready is more involved than these libraries make it which is why they both include such an event handler.
The following link explains with vanilla JS how you cannot only add your event listener you also need to check if it has already fired when you go to add your event listener for DOM ready. This is a common case to handle with eventing - where you create a race condition where an event may fire before you start listening for it - then you don't know that it ever happened without another way to check.
https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded#Checking_whether_loading_is_already_complete
They both check for when the DOM is ready.
If you need Ext to be loaded when using jQuery, try to invert the logic (don't know if it will work, haven't tried).
Ext.onReady(function() {
$(document).ready(function() {
Ext.create('Ext.Button', {...});
});
});
Another StackOverflow question on this subject: Extjs + jQuery together
I have a shadowbox script. When I load the page everything works fine, but when I call this jquery load function and then try to trigger the shadowbox by clicking on the image, the large image opens in new window instead.
Here's the code:
<link href="CSS/main.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="shadowbox-3.0.3/shadowbox.js"></script>
<script type="text/javascript">
Shadowbox.init();
</script>
<p id="compas"></p>
Any idea why this is happening?
EDIT
So, we finally get the bottom of this. 15 hours after first commenting on this issue, and at least 50 iterations later, we finally have identified what the problem is and how to fix it.
It actually struck me suddenly when I was creating local aaa.html and bbb.html on my server. That was when it hit me that the element nodes for the content that was being replaced was being removed altogether from the DOM when $.load() runs the callback function. So, once the #menu-home content elements were replaced, they were removed from the DOM and no longer had Shadowbox applied to them.
Once I figured this out, it was just a matter of a single web search and I found:
Nabble-Shadowbox - Reinit Shadowbox
Specifically, the response from mjijackson. What he describes is how to "restart" (reinitialize) Shadowbox using:
Shadowbox.clearCache();
Shadowbox.setup();
So once the #menu-home content was reloaded, what needs to happen is the Shadowbox cache needs to be cleared (essentially, shutting it down on the page), then the Shadowbox.setup() is run, which will detect the elements all over again. You don't run the Shadowbox.init() method again either.
I noticed that you had tried to copy/paste the Shadowbox.setup() in after the $.load(), at least sequentially in the code. However, this wasn't going to work, due to the cache clearing that needs to happen first, and primarily because the .clearCache() and .setup() functions need to be run after the $.load() completes (finishes and runs any callbacks). Those two functions need to be run in the $.load() callback handler; otherwise, you're running it's immediately, but the $.load() is asynchronous and will complete at some later time.
I'm going to go over some other changes I made, just so you understand what, why and wherefore.
Note, I'm not sure if you're familiar with <base>, but the following is at the top of the HEAD element:
<base href="http://62.162.170.125/"/>
This just let's me use the resource files on your computer. You'll not want to use this on your actual site more than likely. If you copy/paste, make sure and remove this line.
<div id="menu">
<ul>
<li><a id="menu-home" href="index.html" rel="http://jfcoder.com/test/homecontent.html">Home</a></li>
<li><a id="menu-services" href="services.html" rel="http://jfcoder.com/test/servicescontent.html">Services</a></li>
<li><a id="menu-tour" href="tour.html" rel="http://jfcoder.com/test/tourcontent.html">Tour</a></li>
<li><a id="menulogin" href="login.html">Login</a></li>
</ul>
</div>
Here, you'll notice I have a relative url in the HREF attribute, and a link to some pages on my server. The reason for the links to my server is that I couldn't access your aaa.html and bbb.html files through AJAX due to cross-site scripting limitations. The links to my website should be removed as well.
Now, the reason I'm using the rel attribute here is that I want allow for the links by way of the href attribute to continue to work in case the JS doesn't function correctly or there's some other error. If you have separate files, one for full HTML document and another for just the fragments, this is what you'll want to do. If you can serve both the full document AND the content-only from the linked file, then you probably don't need the rel attribute, but you'll need to manage the request so the server knows how to respond (full document or just the content part).
var boxInitialize = function(){
try {
if (!Shadowbox.initialized) {
Shadowbox.init();
Shadowbox.initialized = true;
} else {
Shadowbox.clearCache();
Shadowbox.setup();
}
} catch(e) {
try {
Shadowbox.init();
} catch(e) {};
}
};
All I've done here is create a central location for the initialization/setup requests. Fairly straightforward. Note, I added the Shadowbox.initialized property so I could keep track of if the Shadowbox.init() had run, which can only be run once. However, keeping it all in one spot is a good idea if possible.
I also created a variable function which can be called either as a regular function:
boxInitialize();
Or as a function reference:
window.onload = boxInitialize; // Note, no () at the end, which execute the function
You'll probably notice I removed the $() and replaced them with jQuery() instead. This can turn into a real nightmare if you end up with an environment with multiple frameworks and libraries competing for $(), so it's best to avoid it. This actually just bit me real good the other day.
Since we have a closure scope within the .ready() callback, we can take advantage of that to save several "private" variables for ow use at different times in the scripts execution.
var $ = jQuery,
$content = jQuery("#content"), // This is "caching" the jQuery selected result
view = '',
detectcachedview = '',
$fragment,
s = Object.prototype.toString,
init;
Note the , at the end of all but the last line. See how I "imported" the $ by making it equal to the jQuery variable, which means you could actually use it in that#.
var loadCallback = function(response, status, xhr){
if (init != '' && s.call(init) == '[object Function]') {
boxInitialize();
}
if (xhr.success()
&& view != ''
&& typeof view == 'string'
&& view.length > 1) {
$fragment = $content.clone(true, true);
cacheContent(view, $fragment);
}
};
This runs when the $.load() completes the process of the AJAX request. Note, the content returned in the request has already been placed on the DOM by the time this runs. Note as well that we're storing the actual cached content in the $content.data(), which should never be removed from the page; only the content underneath it.
var cacheContent = function(key, $data){
if (typeof key == 'string'
&& key.length > 1
&& $data instanceof jQuery) {
$content.data(key, $data.html());
$content.data(detectcachedview, true);
}
};
cacheContent() is one a method you may not want; essentially, if it was already loaded on a previous request, then it will be cached and then directly retrieved instead of initiating another $.load() to get the content from the server. You may not want to do this; if so, just comment out the second if block in the menuLoadContent() function.
var setContent = function(html){
$content.empty().html(html);
if (init != '' && s.call(init) == '[object Function]') {
boxInitialize();
}
};
What this does is first empty the $content element of it's contents/elements, then add the specified string-based markup that we saved earlier by getting the $content.html(). This is what we'll re-add when possible; you can see once the different links have been clicked and loaded, reclicking to get that to redisplay is really quick. Also, if it's the same request as currently loaded, it also will skip running the code altogether.
(We use $content like because it is a reference to a variable containing a jQuery element. I am doing this because it's in a closure-scope, which means it doesn't show up in the global scope, but will be available for things like event handlers.
Look for the inline comments in the code.
var menuLoadContent = function(){
// This is where I cancel the request; we're going to show the same thing
// again, so why not just cancel?
if (view == this.id || !this.rel) {
return false;
}
// I use this in setContent() and loadCallback() functions to detect if
// the Shadowbox needs to be cleared and re-setup. This and code below
// resolve the issue you were having with the compass functionality.
init = this.id == 'menu-home' ? boxInitialize : '';
view = this.id;
detectcachedview = "__" + view;
// This is what blocks the superfluous $.load() calls for content that's
// already been cached.
if ($content.data(detectcachedview) === true) {
setContent($content.data(view));
return false;
}
// Now I have this in two different spots; there's also one up in
// loadCallback(). Why? Because I want to cache the content that
// loaded on the initial page view, so if you try to go back to
// it, you'll just pickup what was sent with the full document.
// Also note I'm cloning $content, and then get it's .html()
// in cacheContent().
$fragment = $content.clone(true, true);
cacheContent(view, $fragment);
// See how I use the loadCallback as a function reference, and omit
// the () so it's not called immediately?
$content.load(this.rel, loadCallback);
// These return false's in this function block the link from navigating
// to it's href URL.
return false;
};
Now, I select the relevant menu items differently. You don't need a separate $.click() declaration for each element; instead, I select the #menu a[rel], which will get each a element in the menu that has a rel="not empty rel attribute". Again, note how I use menuLoadContent here as a function reference.
jQuery("#menu a[rel]").click(menuLoadContent);
Then, at the very bottom, I run the boxInitialize(); to setup Shadowbox.
Let me know if you have any questions.
I think I might be getting to the bottom of this. I think the flaw is the way you're handling the $.load() of the new content when clicking a menu item, coupled with an uncaught exception I saw having to do with an iframe:
Uncaught exception: Unknown player iframe
This Nabble-Shadowbox forum thread deals with this error. I'm actually not getting that anymore, however I think it came up with I clicked on the tour menu item.
Now, what you're doing to load the content for the menu items really doesn't make any sense. You're requesting an entire HTML document, and then selecting just an element with a class="content". The only benefit I can see for doing this is that the page never reloads, but you need to take another approach to how to get and display the data that doesn't involve downloading the entire page through AJAX and then trying to get jQuery to parse out just the part you want.
I believe handling the content loading this way is the root cause of your problem, hence the $.load() toggling of menu views breaks your page in unexpected ways.
Question: Why don't you just link to the actual page and skip all the $.load() fanciness? Speed-wise, it won't make that much of an impact, if any at all. It just doesn't make sense to use AJAX like this, when you could just link them to the same content without issue.
There are two alternatives that would allow you to prevent roundtrip page reloads:
Setup your AJAX calls to only request the .content portion of the markup if you have the ?contentonly=true flag in the URL, not the entire HTML document. This is how it's traditionally done, and is usually relative simple to do if you have a scripting environment.
$(".content").load('index.html?contentonly=true');
Then your server responds only with the content view requested.
Serve all of the content views within the same HTML document, then show as appropriate:
var $content = $('.content');
$content.find('.content-view').hide();
$content.find('#services-content').show();
It doesn't look like you have a whole lot of content to provide, so the initial page load probably won't have that much of an impact with this particular approach. You might have to look into how to preload images, but that's a very well known technique with many quality scripts and tutorials out there.
Either one of these techniques could use the #! (hashbang) technique to load content, although I believe there are some issues with this for search engines. However, here is a link to a simple technique I put together some time ago:
http://jfcoder.com/test/hash.html
Also, this is just a tip, but don't refer to your "content" element with a class, ie, .content. There should only be one content-displaying element in the markup, right? There's not more than one? Use an id="content"; that's what ID attributes are for, to reference a single element. classes are meant to group elements by some characteristic they share, so above when I .hide() the inline content views (see #2), I look for all of the class="content-view" elements, which are all similar (they contain content view markup). But the $content variable should refer to $('#content');. This is descriptive of what the elements are.
This worked for us, we made a site that used vertical tabs and called in the pages with our shadowbox images using jQuery.load
Just give all of your anchor tags the class="sbox" and paste this script in the header.
<script>
Shadowbox.init({
skipSetup:true,
});
$(document).ready(function() {
Shadowbox.setup($('.sbox'));//set up links with class of sbox
$('a.sbox').live('click',function(e){
Shadowbox.open(this);
//Stops loading link
e.preventDefault();
});
});
</script>
Note: we had to put the .sbox class on all our rel="shadowbox" anchors as well as the on the anchor for the tab that called the .load
Thanks to this guy-> http://www.webmuse.co.uk/blog/shadowbox-ajax-and-other-generated-content-with-jquery-and-javascript/
Well, based on Shem's answer, this is my solution.
Every click on specific class, setup and open shadowbox with elements from same class:
jQuery('.sb-gallery a').live('click',function(e){
Shadowbox.setup(jQuery('.sb-gallery a'));
Shadowbox.open(this);
//Stops loading link
e.preventDefault();
});
Thanks to all
I'm trying to do some simple jQuery stuff 'dynamically' from within a MediaWiki content page. Its really just to 'beauty up' some different features.
I've done the following:
http://www.mediawiki.org/wiki/JQuery
http://www.mediawiki.org/wiki/Manual:$wgRawHtml (mainly for Paypal buttons initially)
The below code does not work. This is put in a blank content page.
<html>
<script>
$j(document).ready(function(){
$j('#test').hover(
function(){
$j('#test').attr('background-color','red');
},
function(){
$j('#test').removeAttr('background-color');
}
);
});
</script>
<div id="test">Howdy</div>
</html>
Nothing happens...
Any ideas?
Update:
I have attempted this simple solution with no result.
example.com/wiki/index.php?title=MediaWiki:Common.js
$j('#jTest-Howdy').hover(
function(){
$j('#jTest-Howdy').addClass('jTest-red');
},
function(){
$j('#jTest-Howdy').removeClass('jTest-red');
}
);
example.com/wiki/index.php?title=MediaWiki:Common.css
.jTest-red { background-color: red; }
example.com/wiki/index.php?title=jQueryTest
<html>
<div id="jTest-Howdy">Howdy</div>
</html>
as you can see here, this code should work IF jQuery was being loaded properly...
http://jsfiddle.net/5qFhv/
but it is not working for me... any help?
If you're using the jQuery that's loaded by MediaWiki 1.17, be aware that most JavaScript is loaded after page content. An inline <script> element is executed immediately when it's reached, so $j would not be defined at this time -- you'll probably see some errors in your JavaScript error console to this effect.
(Offhand I'm not sure about the jQuery that's included with 1.16; versions of MediaWiki prior to that as far as I know did not include jQuery.)
Generally what you want to do here is to either put JavaScript code modules into the 'MediaWiki:Common.js' page and let that hook up to your HTML markup, or create a MediaWiki extension -- which you can then invoke from your pages, and which will let you create any fun HTML and JavaScript output you like.
http://www.mediawiki.org/wiki/Manual:Interface/JavaScript
http://www.mediawiki.org/wiki/Manual:Developing_extensions
Code you put in your 'MediaWiki:Common.js' page will be loaded after other UI initialization, ensuring that code and variables are present so you can call into jQuery etc.
I don't know much about MediaWiki, but to me it looks like some simple javascript mistakes.
In the first sample you are trying to set an attribute on the element,
when you need to set the css or style attribute.
$j('#test').css('background-color', 'red');
In both samples you are binding an event to an element that doesn't exist yet in the DOM, so it will fail. You could use the live method, which will work for existing and future elements introduced in the DOM.
$j.('#test').live('mouseover', function(){
$j(this).addClass('hover-class');
}).live('mouseout', function(){
$j(this).removeClass('hover-class');
});
Hope that helps.
Try putting all your custom jQuery code in its own file, then load it as a module with ResourceLoader, after jQuery.
http://www.mediawiki.org/wiki/ResourceLoader/Migration_guide_for_extension_developers
Also, as a debugging method: completely load your site in Firefox, then enter your custom jQuery code in the console. If it works, your problem is a race condition. If it doesn't, jQuery isn't loading for some reason.
From the documentation I've found this example:
We can animate any element, such as a simple image:
<div id="clickme">
Click here
</div>
<img id="book" src="book.png" alt="" width="100" height="123" />
With the element initially shown, we can hide it slowly:
$('#clickme').click(function() {
$('#book').fadeOut('slow', function() {
// Animation complete.
});
});
I remember from 5 years ago, that you should NEVER ever refer to any element until it was defined. Does this rule still apply? So I would have to put all that code in the footer of my web page? Or can I put it in a separate file and import it in the footer? What's best practice?
The recommended way of doing this is putting all initialization code into $(document).ready, like so:
$(document).ready(function() {
$('#foobar').click(function(event) { alert("You Clicked Me!"); });
});
You are correct; you cannot interact with a DOM element before it exists.
You have two options:
Put the code below the HTML, as you suggested.
Put the code anywhere, but wrap it in $(function() { ... }).
This construct will execute the function in the page load event, after the DOM exists.
The best practice is to place all SCRIPT elements at the bottom of the HTML document (right before the </body> tag. The reasons are:
loading of external JS files blocks loading of other resources (like images)
since JS code is executed immediately, it is better to parse the HTML code of the page first, and then execute the JS code afterwards
You can see an HTML5 template that demonstrates this practice here: http://vidasp.net/HTML5-template.html
Many people put it in the bottom of the page so other code can execute first. That becomes a bit of a moot point with the document ready syntax that waits until other content loads to the dom. So, using that logic, in theory it could go anywhere.
Scripts go best in the foot of the page, to provide for the speediest rendering of the DOM. The following idiom executes only once the DOM is ready:
$(function() { /* ... your code goes here ... */ }
If you have a lot of code, or code that is shared between multiple pages, you should link it in a separate file which can then be minified, should you need to optimize your download speed.
I asked this question, albeit in a different way a little while back. You might want to look at the answers I got too - they're quite ... philosophical:
JQuery - Best way of wiring GUI objects to events?
I have a couple of, what may end up being for this forum, overly-novice questions regarding unobtrusive event handling.
As I understand it, a properly set-up document would look something like this:
<html>
<head>
<title>Title</title>
<script src="jsfile.js" type="text/javascript></script>
</head>
<body>
//Body content, like some form elements in my case
</body>
</html>
Jsfile.js would look something like this:
function a() {
//code;
}
function b()...
window.addEventListener('load', a, false);
document.getElementById("id").addEventListener('click', b, false);
document.myForm.typeSel.addEventListener('change', c, false);
//or to use better browser-compatible code...
function addEvent(obj,evt,fn) {
if (obj.addEventListener)
obj.addEventListener(evt,fn,false);
else if (obj.attachEvent)
obj.attachEvent('on'+evt,fn);
}
addEvent(window, 'load', a);
addEvent(document.getElementById('id'), 'click', b);
addEvent(document.myForm.typeSel, 'change', c);
As I understand it, while in the head the browser will load this JavaScript code, adding each of those event handlers to their respective elements. HOWEVER... While the window handler is added properly, none of the others are. But if placed within a function, the (for instance) getElementById method of accessing an element works just fine, and the event handler is added. So I could conceivably make a loadEvents() function which is called via window onload, which contains all of the addEvent() functions for the other document elements for which I need event handlers. But as I understand the whole thing, I shouldn't have to do this.
In addition, if I were to stick the addEvent code within the body along with the element it addresses, such as:
<input type="checkbox" id="test" />
<script type="text/javascript>
document.getElementById("test").onclick = func;
</script>
...then it works fine. But of course it also violates the whole reason for removing inline event handlers!
So question being: In order to use *element*.addEventListener('click',func,false), addEvent(*element*,'click',func), or even *element*.onclick = func - how can I successfully reference an element at the end of a script file in the head, without having to stick it in another function? Why does getElementById and other such methods not work outside of a function in the head?
Or, is there some flaw in my underlying understanding?
Putting <script> in the <head> used to be the wisdom. But nowadays, with heavy ajax pages, <script> is more and more often but in the body, as far down below as possible. The idea is that the loading and parsing of the <script> keeps the rest of the page from loading, so the user will be looking at a blank page. By making sure the body is loaded as fast as possible, you give the user something to look at. See YAHOO best practices for a great explanation on that issue: http://developer.yahoo.com/performance/rules.html
Now, regardless of that issue, the code as you set it up now, can't work - at least, not when the elements you attempt to attach the handlers to aren't created yet. For example, in this line:
document.getElementById("id").addEventListener('click', b, false);
you will get a runtime error if the element with id="id" is inside the body. Now, if you put the <script> in the body, way below, after the content (including the lement with id="id", it will just work, since the script is executed after the html code for those elements is parsed and added to the DOM.
If you do want to have the script in the head, then you can do so, but you'll need to synchronize the adding of the event handlers with the rendering of the page content. You could do this by adding them all inside the document or window load handler. So, if you'd write:
//cross browser add event handler
function addEventHandler(obj,evt,fn) {
if (obj.addEventListener) {
obj.addEventListener(evt,fn,false);
} else if (obj.attachEvent) {
obj.attachEvent('on'+evt,fn);
}
}
addEventHandler(document, 'load', function(){
//set up all handlers after loading the document
addEventHandler(document.getElementById('id'), 'click', b);
addEventHandler(document.myForm.typeSel, 'change', c);
});
it does work.
The reason why window.addEventListener works while document.getEle...().addEventListener does not is simple: window object exists when you're executing that code while element with id="abc" is still not loaded.
When your browser downloads page's sources the source code is parsed and executed as soon as possible. So if you place script in head element - on the very beginning of the source - it's executed before some <div id="abc">...</div> is even downloaded.
So I think now you know why
<div id="test">Blah</div>
<script type="text/javascript">document.getElementById("test").style.color = "red";</script>
works, while this:
<script type="text/javascript">document.getElementById("test").style.color = "red";</script>
<div id="test">Blah</div>
doesn't.
You can handle that problem in many ways. The most popular are:
putting scripts at the end of document (right before </body>)
using events to delay execution of scripts
The first way should be clear right now, but personally I prefer last one (even if it's a little bit worse).
So how to deal with events? When browser finally download and parse whole source the DOMContentLoaded event is executed. This event means that the source is ready, and you can manipulate DOM using JavaScript.
window.addEventListener("DOMContentLoaded", function() {
//here you can safely use document.getElementById("...") etc.
}, false);
Unfortunately not every browser support DOMContentLoaded event, but as always... Google is the anwser. But it's not the end of bad news. As you noticed addEventListener isn't well supported by IE. Well... this browser really makes life difficult and you'll have to hack one more thing... Yes... once again - Google. But it's IE so it's not all. Normal browsers (like Opera or Firefox) supports W3C Event Model while IE supports its own - so once again - Google for cross-browser solution.
addEventListener might seems now the worst way to attach events but in fact it's the best one. It let you easly add or remove many listeners for single event on single element.
PS. I noticed that you consider of using Load event to execute your scripts. Don't do that. Load event is execute too late. You have to wait till every image or file is loaded. You should use DOMContentLoaded event. ;)
EDIT:
I've forgotten... dealing with cross-browser event model is much easier when you're using some framework like very popular jQuery. But it's good to know how the browsers work.
are you familiar with jQuery?
its a javascript library featuring some really awesome tools.
for instance if you want to have some js action done just after your page if fully loaded and all DOM elements are created (to avoid those annoying exceptions) you can simply use the ready() method.
also i see you want to attach click \ change events jQuery takes care of this too :) and you don't have to worry about all those cross-browser issues.
take a look at jQuery selectors to make your life easier when attempting to fetch an element.
well thats it, just give it a shot, its has a very intuitive API and a good documentation.