Organize your Scripts
what you should have:
- basic Javascript understanding
- a standard compliant browser
- Firefox with the Firebug plugin installed
in the early days of Web Coding dynamic functionality was added to a web site using the onEvent attributes.
Expand|Select|Wrap|Line Numbers
- <span onclick="doSomething()">click me</span>
Expand|Select|Wrap|Line Numbers
- <script type="text/javascript">
- function doSomething()
- {
- // sample code
- alert("you have successfully clicked me");
- }
- </script>
2. Play it again, Sam or the traditional model
As time went by, people started to put their code in an external javascript file to keep markup and behaviour separate, because you can maintain your code a lot better this way.
Still the functionality was maintained via the onEvent attribute.
Expand|Select|Wrap|Line Numbers
- <script type="text/javascript">
- function doSomething()
- {
- // sample code
- alert("page finished loading");
- }
- // attach the function to an event attribute
- window.onload = doSomething;
- </script>
This is where the Document Object Model (DOM) came into play. This may seem tedious work at first, but when you advance within Javascript, you'll see the benefits it brings (no Javascript calls in the HTML code required, inclusion of the Javascript is enough).
Expand|Select|Wrap|Line Numbers
- // HTML
- <span id="submit">click me</span>
Expand|Select|Wrap|Line Numbers
- // Javascript
- function doSomething()
- {
- // sample code
- alert("you have successfully clicked me");
- }
- // this will only work, if the element is already loaded into the browser
- document.getElementById("submit").onclick = doSomething;
- // to delay any function call after the page load, you have to use the onload event
- window.onload = function()
- {
- document.getElementById("submit").onclick = doSomething;
- }
All this code has a real drawback: you can only apply one function call per event (and element), i.e. if you include several scripts, only the last window.onload call will work (reason, you only have on onEvent attribute per element available).
3. Javascript, next generation or the DOM way of life
All this changed, when the W3C discharged the DOM Level 2. Now you can handle events through functions: addEventListene r & removeEventList ener which bind for a selected event a function to an element.
Expand|Select|Wrap|Line Numbers
- function doSomething()
- {
- // sample code
- alert("you have successfully clicked me");
- }
- function doSomethingElse()
- {
- // code comes here...
- }
- function init()
- {
- document.getElementById("submit").addEventListener("click", doSomething, false);
- document.getElementById("submit").addEventListener("click", doSomethingElse, false);
- }
- window.addEventListener("load", init, false);
Note: I only scratch the Event topic here, because it is hardly explained in such a short article, though event handling is at the heart of Javascript (and a good example too).
Further note: I can't cover IE bugs and workarounds here, because that would need an article for itself.
4. Cleaning up the code
there are already good articles out there, howto write good code:
Unobtrusive Javascript
From DHTML to DOM scripting - an example of how to replace outdated JavaScript techniques.
Vitamin Features » The Importance of Maintainable JavaScript
So far we used functions to organize our script, which will be no problem as long as there's not too much code. Imagine you have 3 script files and each of them has its own init() function! this will surely result in problems. So what now? If you have 3rd party scripts, you can't really rewrite their code...
Since Javascript has OOP capabilities (admittedly it's no match to Java) we can use this to stop our functions interfering with each other.
To understand how this works, we have to learn about scope (for instance here: Digital Web Magazine - Scope in JavaScript). Every variable in Javascript has a scope (the context in which it is defined). The important ones are Global Scope (the variable is accessible everywhere in the script) and Local Scope (the variable is accessible only inside the function it was defined in). So any function we define (as we did it) will have Global Scope. In every script file.
So if you have two functions defined, the last defined will win because it will overwrite the former (no matter if they are defined in different files or not).
Expand|Select|Wrap|Line Numbers
- function init()
- {
- alert(1);
- }
- // somewhere else but later
- function init()
- {
- alert(2);
- }
- // alerts '2'
- init();
Note: While talking about scope, there is also a very useful technique called Closures, which you may find handy (JavaScript Closures for Dummies | Developing thoughts — Morris Johns)
Now that we know about scope, how will that benefit us? The two functions from above had the same name ('init') and the same scope (global). To make them different we need to change at least one of them. You can of course invent countless names including 'init', but we chose this name in the first place, because it described (and still does) the function best.
That's leaving us with the option of changing the scope, but how do we do that? That's where Object Oriented Programming (OOP) comes into play. OOP means dealing with Objects.
In short words, an object is a construct, that has (can have) values (called properties) and functions (called methods) that belong exclusively to that object.
Objects are used as follows:
Expand|Select|Wrap|Line Numbers
- // define a new value for the property
- object.property = x;
- // assign the property value to a variable
- var y = object.property;
- // call a method
- object.method();
- // call a method with arguments
- object.method(arg1, arg2);
- // doesn't work
- property = x;
- var y = property;
- method();
the object defines the scope for its methods (functions) and properties (values).
Thus: obj1.init() and obj2.init() are two different functions. Got it?
To solve our scope problem, we just define our functions as methods of different objects.
Note: this is not the original idea of OOP, but I'm not explaining OOP here (it would take too long and it's out of the scope of this article).
Let's get it on. How do we define those objects? There is a way of writing (called the Object Literal (Wait till I come! » Blog Archive » Show love to the object literal)) we can use here.
Expand|Select|Wrap|Line Numbers
- var object_name = {
- property_1 : "value_1",
- property_2 : "value_2",
- method_1 : function()
- {
- alert("method 1 executing");
- },
- method_2 : function(x)
- {
- if (this.x)
- {
- alert(this.x);
- }
- }
- }
- // alerts “method 1 executing”
- object_name.method_1();
- // does nothing (value_1 is not the name of a valid property)
- object_name.method_2("value_1");
- // alerts “value_2”
- object_name.method_2('property_2');
Expand|Select|Wrap|Line Numbers
- // since we define the objects (not its methods) in global scope,
- // we can omit the var keyword
- NS1 = {
- init : function()
- {
- alert(1);
- }
- }
- NS2 = {
- init : function()
- {
- alert(2);
- }
- }
- // alerts “1”
- NS1.init();
- // alerts “2”
- NS2.init();
5. Structuring the script
When you write a script, then you have (in most cases) only a few number of tasks to do (e.g. event handling (some click events, maybe a mouseover event), dynamic content loading (AJAX, Submenu) or form validation). To get these started, one init() function per task is enough. All the other functions around only help those starter functions getting the work done. So you can easily group the helper functions around the starter functions. And every group can be assigned to its own scope.
Of course you can structure the scopes too … (but that should only be necessary for large libraries)
example:
Expand|Select|Wrap|Line Numbers
- myName.menu // function group for menu creation
- myName.forms // function group for form validation
- myName.commons // functions used in many groups
- // e.g. event handling, loop functions, …
We can do that too. With a little change in the code, we can make public and private properties and methods!
Expand|Select|Wrap|Line Numbers
- // our scope (let's call it namespace …)
- bytes = {
- // subclasses
- // technically not necessary, but providing an overview
- // esp. if you have many subclasses
- josah : new Object,
- msquared : new Object
- // you can define metadata here (dublincore.org)
- }
- // object with only public members
- bytes.msquared = {
- favouriteColour : "purple",
- hateColour : "gray",
- state : function()
- {
- alert("I like " + this.favouriteColour);
- },
- stateNot : function()
- {
- alert("I hate " + this.hateColour);
- }
- }
- // alerts "I hate gray"
- bytes.msquared.stateNot();
- // alerts "I like blue"
- bytes.msquared.favouriteColour = "blue";
- bytes.msquared.state();
- // an object with private and public members
- // I didn't go for real functions here……… ;-)
- bytes.josah = function() {
- // this is a private property
- var hateColour = "purple";
- // this is a private function
- // the 'this' keyword does not necessarily
- // point to bytes.josah!
- function throw()
- {
- this.lift();
- this.move();
- this.drop();
- }
- return {
- // this is a public function
- defenestrate : function(num)
- {
- var OP = thread[num].firstChild;
- OP.addEventListener("unlike", throw, true);
- // you can call members from different scopes
- bytes.msquared.hateColour = hateColour;
- bytes.msquared.stateNot();
- }
- };
- }();
- // doesn't work
- bytes.josah.throw();
- // works (but I wouldn't try it)
- // defenestrates #666 and alerts "I hate purple"
- bytes.josah.defenestrate(666);
And finally a real world example
Description: popup an image (or anything else you can do within window.open()) when you click on an registered element. there's no constraint, what the element can be, as long as its id attribute is registered for popup. you can even open more than one popup windows, though don't make it too many (the user might get annoyed then).
Expand|Select|Wrap|Line Numbers
- /*
- #########################################################
- # Popup script by Dormilich, bytes.com #
- # http://bytes.com, If you use this script on your site #
- # Please keep this comment at the top of the javascript #
- #########################################################
- */
- Dormilich.Popup = function() {
- /**
- * [private] array to store the objects holding the necessary information for window.open()
- */
- var IMG = new Array;
- /**
- * [private] calling window.open() on event
- * since the image info object is not part of the element, the correct image object
- * is identified by ID comparison
- *
- * @return (void)
- */
- function pop()
- {
- var l = IMG.length;
- for (var j=0; j<l; j++)
- {
- // because pop() is used by addEvent() 'this' points to the object
- // having the event attached to
- if (IMG[j].id == this.id)
- {
- window.open(IMG[j].src, IMG[j].titel, IMG[j].props);
- }
- }
- }
- return {
- /**
- * [public] create an image info object that hold the parameters for window.open() and put
- * it in the private storage array. src, titel & props are the parameters for window.open()
- *
- * @param (string) ID id of the element to apply the onclick event to
- * @param (string) LOC the location of the image to popup
- * @param (string) TTL title of the popup window
- * @param (int) HGT image height
- * @param (int) WDT image width
- * @return (void)
- */
- create : function(ID, LOC, TTL, HGT, WDT)
- {
- var obj = {
- id : ID,
- src : LOC,
- titel : TTL,
- props : "height=" + HGT + ",width=" + WDT + ",menubar=no,resizable=yes,scrollbars=yes"
- }
- IMG.push(obj);
- },
- /**
- * [public] loop through all element in the private storage array and attach the private pop() method
- * with the onclick event handler. (interestingly, even though the addEvent() method comes from a
- * different class it's executed in the local scope)
- *
- * @return (void)
- */
- register : function()
- {
- var l = IMG.length;
- for (var i=0; i<l; i++)
- {
- var z = document.getElementById(IMG[i].id);
- if (z)
- {
- z.style.cursor = "pointer";
- // currently Tino Zijdel's addEvent() function namespaced
- Events.addEvent(z, "click", pop);
- }
- }
- }
- };
- }();
- Events = {
- addEvent : function(obj, evType, fn, useCapture)
- {
- // full code see http://therealcrisp.xs4all.nl/upload/addEvent2
- // of course you can use any other addEvent() function you like
- }
- };
Expand|Select|Wrap|Line Numbers
- /* Javascript */
- // add image
- // assigning the image "test.png" to the element with the id "test"
- // a new 500x700 popup (inner size) will appear onclick
- Dormilich.Popup.create('test', 'test.png', 'TST', 500, 700);
- // open a PDF in a new window (just used a random size)
- Dormilich.Popup.create('oath', 'slayers_try_prophecy.pdf', 'TRY', 387, 632);
- // register elements/images
- Events.addEvent(window, "load", Dormilich.Popup.register);
- /* HTML */
- // show a full size image when clicking on a thumbnail
- <img src="test_thumb.jpg" width="50" height="70" alt="test image for popup" title="click for full image" id="test" class="thumbnails" />
- // show the full text of an essay
- <div id="oath" class="shortdesc" title="click for full text">There shall come a controller of a dark star who shall call forth the light. (…)</div>
Dormilich