Introduction
This article demonstrates the use of one of the JavaScript classes of my DHTML API. You can see the API in action at:
Both the sites and the documentation are in Turkish however :(
If you consider using the API, the code is distributed under a 'Creative Commons Attribution-NonCommercial-Share Alike 2.0' license. That is to say its non-commercial use is free, if you give me credit and provide me with feedback or updated code (email: volkan.ozcelik@gmail.com; see terms of use for details).
Like any developer, I'd be glad when you give credit to me after using my code/know-how. However what I mostly want is to share knowledge, help others improve, and listen to others' comments so that I can improve myself.
I'm planning to make an English version of the site as well. But I have other issues of higher priority.
Purpose of this article
In this article we will be creating an object that will return a cross-browser reference to the elements in an HTML page.
We will be writing JavaScript in an object oriented manner. We will see examples on function prototyping. As we will see, JavaScript functions are not mere pieces of procedure. In JavaScript, to be more technical - in ECMAScript, functions can be considered as objects. They can have their own methods, member variables etc..
The skeleton
Let us begin with a prototype model for our object. We will name it CBObject
, an abbreviation for cross-browser object.
function CBObject(elmID) {
...
}
CBObject.prototype.exists = function() {
...
};
CBObject.prototype.getObject = function() {
...
};
CBObject.prototype.getValidator = function() {
...
};
CBObject.prototype.getID = function() {
...
};
The CBObject(elmID)
can be considered as the constructor. It takes the ID of the object on the HTML page (elmID
) as a string and seeks the page to check whether there exists such an object.
- Method
exists
returns true
if such an object is found, false
otherwise.
- Method
getObject
returns a reference to that object.
- Don't worry about the
getValidator
, it returns a ref to a Validator
object which I will explain shortly.
- And
getID
returns the ID of the object as you can guess.
Swapping the code
Now let us swap things around:
var g_blnCBObjectProtoCalled = false;
function CBObject(elmID) {
if(!g_blnCBObjectProtoCalled) {
g_blnCBObjectProtoCalled = true;
var _this = CBObject.prototype;
_this.prototype.exists = function() {
...
};
_this.getObject = function() {
...
};
_this.getValidator = function() {
...
};
_this.getID = function() {
...
};
}
}
Modifying the code this way is my design preference. In my humble opinion, it has two advantages.
- First, the code much more resembles a class.
- Second, if the
CBObject
is never constructed with new
, the prototypes will not be attached to the object since they appear in the function body in contrast to the previous code where they were outside the function body.
We use the variable g_blnCBObjectProtoCalled
to ensure that the prototypes are attached once and only once (i.e., when the object is constructed for the first time).
The validator
Validator
object is the utility class for validating objects. Here is the complete list of methods it includes:
isDefined(x)
isEmpty(x)
isEmail(x)
isInteger(x)
isFloat(x)
isString(x)
isDate(intYear, intMonth, intDay)
getStatusCode()
You can find the entire code in the source zip. For our discussion, we will be interested in two specific methods:
function Validator() {
... truncated ...
_this.isDefined = function(x) {
if(typeof(x) == "undefined") {return false;}
return true;
};
_this.isString = function(x) {
if(!this.isDefined(x)) {return false;}
if(typeof(x) == "string") {return true;}
return false;
};
... truncated ...
}
String prototype helpers
The Validator
class uses several String
prototypes. I'm giving them for the sake of completeness.
var _spt = String.prototype;
_spt.trim = function(blnIgnoreCarriage, blnIgnoreInnerWhiteSpace) {
var temp = this.replace(/^\s*/,"");
temp = temp.replace(/\s*$/,"");
blnIgnoreCarriage = blnIgnoreCarriage ? true : false;
blnIgnoreInnerWhiteSpace = blnIgnoreInnerWhiteSpace ? true : false;
if(blnIgnoreCarriage && blnIgnoreInnerWhiteSpace) {;}
else if(blnIgnoreCarriage&&!blnIgnoreInnerWhiteSpace) {
temp = temp.replace(/\t+/g," ");
temp = temp.replace(/ +/g," ");
}
else if(!blnIgnoreCarriage && blnIgnoreInnerWhiteSpace) {
temp=temp.replace(/(\n\r)+/g,"");
}
else if(!blnIgnoreCarriage && !blnIgnoreInnerWhiteSpace) {
temp=temp.replace(/\s+/g," ");
}
if(temp==" ") { temp=""; }
return temp;
};
_spt.match = function(strRegExp, strOption) {
var regEx = new RegExp(strRegExp, strOption ? strOption : "g");
return this.match(regEx);
};
_spt.remove = function(strRegExp, strOption) {
var temp = this;
var regEx = new RegExp(strRegExp, strOption ? strOption : "g");
temp = temp.replace(regEx, "");
return temp;
};
_spt.removeTags = function() {
var regEx = /<[\/]?([a-zA-Z0-9]+)[^>^<]*>/ig;
return this.replace(regEx,"");
}
_spt.test = function(strRegExp, strOption) {
var regEx = new RegExp(strRegExp, strOption ? strOption : "g");
return regEx.test(this);
};
How to find the object reference
In the light of the above descriptions, we are now ready to focus our attention on CBObject
.
Let us begin by initializing the member variables of CBObject
:
function CBObject(elmID) {
if(!g_blnCBObjectProtoCalled) {
... truncated ...
}
this._val = new Validator();
this._obj = this._getObject(elmID);
}
A notational convenience I use is the underscore. For methods that are not supposed to be used outside the object, I prefix them with an underscore. That is, method names that begin with an underscore (_
) are intended to be used only within the object. (We can say that they are private methods of the object, and variable names that begin with underscore are private members.)
In the code above, the variables _val
and _obj
, and the method _getObject
are intended to be private to CBObject
.
The variance _val
is a reference to an internal Validator
object. And _obj
is a reference to an element with ID elmID
that exists within the HTML body.
And here comes the heart of the construct that does most of the work:
_this._getObject = function(elmID) {
var v = this.getValidator();
if(!v.isString(elmID)) {return elmID;}
if(document.getElementById) {elmID = document.getElementById(elmID);}
else if(document.all) {elmID = document.all[elmID];}
else if(document.layers) {elmID = this._getLayer(elmID);}
else if(document.forms) {
if(document.forms[elmID]) {elmID = document.forms[elmID];}
else {
for(var i=0; i<document.forms.length; i++) {
if(document.forms[i][elmID]) {
elmID = document.forms[i][elmID];
break;
}
}
}
}
else {elmID = null;}
return elmID;
};
Method getValidator
simply returns the member Validator
instance _val
that is mentioned a few paragraphs above.
Let me try to explain what _getObject
does step by step:
- If
elmID
is not a string then assume that it is an object reference and return it immediately.
- Else try to get the reference with W3C DOM function
getElementById
in a cross browser manner. Latest versions of most browsers support getElementById
.
- Else try to get it the IE way by using
document.all
which only IE understands.
- If you still fail to get the object reference, try iterating through the document's layers, which only archaic versions of Netscape (i.e. NS4-) understand. Here
_getLayer
function scans all the layers to see whether elmID
exists in one of them.
Since layers can contain other layers, the _getLayer
method should be called recursively. Here is how it is done:
_this._getLayer = function(elmID,objRoot) {
var i = 0;
var objLayer = null;
var objFound = null;
if(!objRoot) {objRoot = window;}
for(i=0; i<objRoot.document.layers.length; i++) {
objLayer = objRoot.document.layers[i];
if(objLayer.id==elmID) {return objLayer;}
if(objLayer.document.layers.length) {
objFound = this._getLayer(elmID,objLayer);
}
if(objFound) {return objFound;}
}
return null;
};
Although _getLayer
method will never be used, I think it is worth examining how the recursion is implemented.
If none of the above steps find the object, then the object can be a form
element. So for a last hope, we iterate the forms
collection. If we still fail to find anything, we return null
.
Test what we have done
The TestCBObject.html bundled with the source zip includes a simple test case.
Here is what is in the Head of the document:
<script type="text/javascript" src="String.js"></script>
<script type="text/javascript" src="Validator.js"></script>
<script type="text/javascript" src="CBObject.js"></script>
<script type="text/javascript">
function checkForObjects() {
var MyLayer = new CBObject("MyLayer");
var FormInput1 = new CBObject("FormInput1");
var FormInput2 = new CBObject("FormInput2");
var NonExistent = new CBObject("NonExistent");
if(MyLayer.exists()) {
MyLayer.getObject().style.backgroundColor = "#cc0000";
}
else {
alert("MyLayer does not exist");
}
if(FormInput1.exists()) {
FormInput1.getObject().style.backgroundColor = "#cc0000";
}
else {
alert("FormInput1 does not exist");
}
if(FormInput2.exists()) {
FormInput2.getObject().style.backgroundColor = "#cc0000";
}
else {
alert("FormInput2 does not exist");
}
if(NonExistent.exists()) {
NonExistent.getObject().style.backgroundColor = "#cc0000";
}
else {
alert("NonExistent does not exist");
}
}
</script>
And here is the body:
<div id="MyLayer">My Layer</div>
<form id="MyFirstForm" method="post" action="#" onsubmit="return false;">
<fieldset>
<legend>first form</legend>
<input name="FormInput1" />
</fieldset>
</form>
<form id="MySecondForm" method="post" action="#" onsubmit="return false;">
<fieldset>
<legend>second form</legend>
<input name="FormInput2" />
</fieldset>
</form>
<form id="ActionForm">
<div>
<input type="button" value="click me" onclick="checkForObjects();" />
</div>
</form>
Once you click the button, checkForObjects()
will be executed. It will check several objects for existence, if it finds one it will change the background color of that object to red. If it fails to find, it will alert that it couldn't.
Conclusion
In this article, we tried to retrieve a cross-browser reference to an HTML DOM object, in the meantime we looked at how prototype-based, object oriented JavaScript code can be implemented. I hope, during the discussion I didn't jump here and there a lot.
Writing cross-browser JavaScript can be quite challenging if you span a wide range of target browsers and platforms. There are still users in this world who are using browsers of pre-historic times (NS3 for instance). For sure, they have their reasons to do so.
Note to the reader
To be honest, writing this cross-browser code made the API far too complicated and bloated.
Since almost any version 4+ browser (NS7+, IE5.5+, Mozilla1.3+, Opera7.5+, Safari X+ ..) supports W3C DOM, I decided to remove pre-DOM cross-browser support from the API, and decided to add new widgets that utilize DOM.
I realized that supporting Flintstone-browsers to the best extent resulted in a switch-case mambo-jambo and a spaghetti code where even I found myself lost!
Therefore, I decided not to write code for browsers which do not understand DOM. May be it's a radical change but the time for it has already come.
See how the above messy _getObject
is simplified if we choose that course of action:
_this._getObject = function(elmID) {
if(!new Validator().isString(elmID)) {return elmID;}
else {return document.getElementById(elmID);}
};
25 lines of code (including _getLayer
stuff) with "if
", "else
-if
" and "for
" junk simply reduces to two lines! It is simple, it is fast, it is consistent, it is understandable and followable, and it is the standard way of doing it! To sum up, keep your code as little jumpy as possible. I don't say "write code for IEv7+ only". I just say, don't support old browsers just to show off.
You may think that a structure that supports all version2+ browsers throughout the globe as the most elegant coding manifest of all times. But I simply consider it as a pile of garbage. (I'd appreciate any thoughts on my decision as well, TIA.)
History
- 2005-03-05
- 2005-03-07
- Method
_getLayer
is altered a bit. Article changed accordingly.
- 2005-03-22
- "Note to the Reader" part added.