Welcome!

Kurt Cagle

Subscribe to Kurt Cagle: eMailAlertsEmail Alerts
Get Kurt Cagle via: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Related Topics: RIA Developer's Journal, XML Magazine

RIA & Ajax: Article

Real-World AJAX Book Preview: Instantiating the Bindings

In Mozilla, the XBL binding language (an XML language) is used to associate bindings with their respective elements

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.

Instantiating the Bindings
In Mozilla, the XBL binding language (an XML language) is used to associate bindings with their respective elements, with these tied in via CSS. Unfortunately, there are no clean hooks for adding a binding in this way for Opera (and a different mechanism for handling it in Internet Explorer).

Because of this, the approach I took to binding here was somewhat different and involved two separate routines: applyBindings() and applyBinding(bindingName,node). The first routine will always be called (it's invoked in the windows.onload event) and its role is to retrieve each <binding> element from the Web page, retrieve from that binding both its name (from the target attribute) and the binding URL (as contained in the src attribute), and then to load the JavaScript code into an object.

Once that is done, the second function applyBinding() is invoked with one argument (the name of the binding). When invoked in this manner, the applyBinding() method will retrieve all elements with this name from the calling document (the original XHTML file), and will then directly apply the binding to each such element.

It is also possible to call applyBinding() with a second argument - a specific element object - in order to apply the binding to that object directly. In general this usage will be performed via some later script. For instance, you could modify the code in the XInclude binding to automatically check and bind newly introduced elements that have just been rendered.

In addition to these two functions, this script also defines the getXPath() function, which takes as arguments an XPath expression and the node on which to apply it, and returns a nodeList object containing zero or more nodes of those that satisfy the given XPath call. Note that this is a fairly bare-bones version and is designed to work with both Mozilla, which does have its own more sophisticated XPathEvaluator object, and Opera, which doesn't. The XPath in Opera is handled by using an inline XSLT transformation, which Opera 9.0 supports.

The Knee Bone Is Connected to ...
The JavaScript binding files generally follow the same general skeleton:

   var obj = {
   constructor:function(){
     // constructor code
     },
methods:{
methodA:function(){
         // code for method A
         }
     },
     methodB:function(arg1,arg2){
         // code for method B
         }
   },
events:{
   eventA:function(evt){
     // event handler for eventA
   },
   eventB:function(evt){
     // event handler for eventB
   }
   }
};
obj;

These are actually attached to the target element directly, such that if you have a reference to element X, you can call methodA on X directly as X.methodA(). Note that this approach does tend to be fairly expensive when compared to attaching the code directly to a constructor prototype. But unfortunately all elements that are not explicitly defined in both the Mozilla and Opera DOMs are treated as a single anonymous element class and consequently such generalized bindings would cause a great deal of havoc.

Note that the skeleton currently doesn't recognize destructors though these are easy enough to add properties, the way XBL does. The latter lack of property support comes about because IE7 does not recognize setters or getters in JavaScript, along with the fact that you can always create setXXX and getXXX methods directly.

Finally, the event names should be given without the "on" prefix: "load" instead of "onload", "mousedown" vs. "onmousedown", and so forth. The code given here also doesn't differentiate bubbling phases, primarily again because of restrictions with Internet Explorer's event flow model.

The code for performing the specific XInclude binding is given as follows:

XIncludeBinding.js

var obj = {
constructor:function(){
   if (this.getAttribute("id") == null){
     this.setAttribute("id", this.nodeName + "_" + Math.floor
(100000*Math.random()) + "_" + Math.floor(100000*Math.random()));
     }
     this.child_id = this.nodeName + "_" + Math.floor(100000*Math.random());
     this.innerHTML = "<div id='"+this.child_id+"'></div>";
     this.old_path = "";
     this.current_path = "";
     this.current_doc_id = "";
     this.activeDocument = null;
     this.refresh();
     this.toc_displayed = true;
     },
   methods:{
     refresh:function(){
       var control = this;
       if (control.getAttribute("timeoutInterval ")!== ""){
         var timeoutInterval = control.getAttribute("timeoutInterval");

         }
       else {
         var timeoutInterval = 10000;
         }
   // var container = document.getElementById(control.child_id);
       var container = control;
       var src = control.getAttribute("src");
       if (/#/.test(src)){
         var srcArr = src.match(/(.*)#(.*)/);
         control.current_path = RegExp.$1;
         control.current_doc_id = RegExp.$2;
         }
       else {
         control.current_path = src;
         control.current_doc_id = "";
         }
       if (control.current_path != control.old_path){
         control.old_path = control.current_path;
         container.innerHTML = "Loading ...";
         control.http = new XMLHttpRequest();
         var timeoutNeeded = true;
         control.http.open("GET",control.current_path,true);
         if (control.getAttribute("disable-cache") == "yes"){
     control.http.setRequestHeader("Expires",0);
     control.http.setRequestHeader("Cache-Control","no-cache");
     }
   control.http.onreadystatechange= function(){
   if (control.http.readyState == 4){
     var doc = control.http.responseXML;
     control.activeDocument = doc;
     control.paint();
     }
   }
     control.http.send(null);
     }
   else {
     control.paint();
     }
   },
   paint:function(content){
     // alert("Paint Called");
     var control = this;
     var toc_show = false;
     // var container = document.getElementById(control.child_id);
     var container = control;
     // xml_alert(control.activeDocument);
   if (control.current_doc_id != ""){
     var node = control.activeDocument.getElementById(control.current_doc_id);
     var docStr = ((new XMLSerializer()).serializeToString(node
));
     }
   else {
   if ((control.getAttribute("buttons") != "no") && (control.getAttribute("buttons") != "")){
     var node = getXPath(".//*[@id]",control.activeDocument).item(0);
     //var docStr = ((new XMLSerializer()).serializeToString(node));
     control.current_doc_id = node.getAttribute("id");
     toc_show = true;
     }
   else {
     var docStr = ((new XMLSerializer()).serializeToString(
     control.activeDocument.documentElement));
     }
   }
   if (content == null){
     var body =docStr;
     }
   else {
     var body = content;
     }
   if ((control.getAttribute("buttons") != "no") && (control.getAttribute("buttons") != "")){
     var buttonState = control.getAttribute("buttons");
     var current_button_index = 0;
     function getButtonBar(location){
       startBuf = "<div xmlns='http://www.w3.org/1999/xhtml'
class='xincludeButtonBar xincludeButtonBar_"+location+"'>";
       var buf = "";
       var className = "";
       var idNodes = getXPath("//*[@id]",control.activeDocument);
       for (var index = 0; index != idNodes.length; index++){
       var idNode = idNodes.item(index);
   if (control.current_doc_id == idNode.getAttribute("id")){
       className = 'xincludeButtonSelected';
       current_button_index = index;
       }
   else {
     className = 'xincludeButton';
     }
     var buttonTitle = "";
     for (var child_index = 0; child_index != idNode.
      childNodes.length;child_index++){
     var childNode = idNode.childNodes.item(child_index);
   if (childNode.nodeType == childNode.ELEMENT_NODE){
       buttonTitle = childNode.textContent.replace(/</g,"<");
       buttonTitle = buttonTitle.replace(/\"/g,""");
       break;
       }
     }
     var button = '<button title="'+buttonTitle+'"
       onclick="var control = document.getElementById(\''+control.getAttribute("id")+'\
   ');control.showPage(\''+control.current_path+"#"+idNode.getAttribute("id")+'\',\
   ''+idNode.getAttribute("id")+'\')" class="'+className+'">'+(1 + index)+'</button>';
       buf += button;
       }
     var toc_button = '<button onclick="document.
       getElementById(\''+control.getAttribute("id")+'\').showTOC()"
       class="xinclude_toc">TOC</button>';
     var previous_id = idNodes.item((idNodes.length + current_
       button_index - 1) % idNodes.length).getAttribute("id");
     var next_id = idNodes.item((current_button_index + 1)
       % idNodes.length).getAttribute("id");
     var previous_button = '<button onclick="document.
       getElementById(\''+control.getAttribute("id")+'\').showPage(\''+control.
       current_path+"#"+previous_id+'\',\''+previous_id+'\')" class="xinclude_
       previous"><<</button>';
     var next_button = '<button onclick="document.
       getElementById(\''+control.getAttribute("id")+'\').showPage(\''+control.current_
       path+"#"+ next_id+'\',\''+next_id+'\')" class="xinclude_next">>></button>';
   if (control.getAttribute("page-buttons") == "no"){
       buf = "";
       }
       buf = startBuf + toc_button + previous_button +
     next_button + buf;
       buf += "</div>";
     return buf;
       }
   if ((buttonState == "yes") || (buttonState == "top") ||
     (buttonState == "both")){
     container.innerHTML = getButtonBar("top");
     }
     container.innerHTML += "<div class='xinclude_body'>"+body+"</div>";
   if ((buttonState == "yes") || (buttonState == "bottom") ||
       (buttonState == "both")){
       container.innerHTML += getButtonBar("bottom");
       }
   if ((control.getAttribute("title") != null)){
     var titleKey = control.getAttribute("title");
     var titleElement = control.activeDocument.getElementsByTagName(titleKey).item(0);
     var buf = '<h1 class="xincludeTitle">'+titleElement.innerHTML+'</h1>';
     container.innerHTML = buf + container.innerHTML;
     }
   }
   if (toc_show){
     control.toc_displayed = false;
     control.showTOC();
     }
},
     showPage:function(url,id){
     this.setAttribute('src',url);
     this.current_doc_id = id;
},
     showTOC:function(){
     var control = this;
     var container = control;
   if (!control.toc_displayed){
     control.toc_displayed = true;
     var buf = "<div class='xinclude_toc'><ol>";
     var idNodes = getXPath("//*[@id]",control.activeDocument.documentElement);
   for (var index = 0; index != idNodes.length; index++){
     var idNode = idNodes.item(index);
   if (control.current_doc_id == idNode.getAttribute("id")){
     className = 'xincludeButtonSelected';
     current_button_index = index;
     }
   else {
     className = 'xincludeButton';
   }
     var title = "";
   for (var child_index = 0; child_index != idNode.childNodes.
length;child_index++){
     var childNode = idNode.childNodes.item(child_index);
   if (childNode.nodeType == 1){
     title = childNode.textContent.replace(/\</g,"<");
     title = title.replace(/\"/g,""");
     title = title.replace(/&/g,"&");
     // title = title.replace(/\'/g,"'");
     // title = title.replace(/\&/g,"&");
     break;
    }
}
   var listItem = '<li><span onclick="var control = document.
   getElementById(\''+control.getAttribute("id")+'\');control.showPage(\''+control.
   current_path+"#"+idNode.getAttribute("id")+'\',\''+idNode.getAttribute("id")+'\')"
   class="xinclude_TOC_item">'+title+'</span></li>';
     buf += listItem;
     }
   buf += "</ol></div>";
     control.paint(buf);
   // control.toc_displayed = false;
   }
   else {
     control.toc_displayed = false;
     control.paint();
     }
   }
},
events:{
DOMAttrModified:function(evt){
     var target = evt.target;
   if (target.old_src != target.getAttribute("src")){
     target.refresh();
       }
     }
   }
};
obj;

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.

More Stories By Kurt Cagle

Kurt Cagle is a developer and author, with nearly 20 books to his name and several dozen articles. He writes about Web technologies, open source, Java, and .NET programming issues. He has also worked with Microsoft and others to develop white papers on these technologies. He is the owner of Cagle Communications and a co-author of Real-World AJAX: Secrets of the Masters (SYS-CON books, 2006).

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.