Using the TabView Control with My Dispatcher Plugin

By Caridy PatinoNovember 3rd, 2008
Caridy Patiño Mayea

Caridy, a leading and always-helpful contributor to the YUI community forum, has been working in front of a PC since the nineties. Until recently, he was a professional programmer developing LAMP applications for the University of Las Villas where he received his B.S. in Computer Science in 2003, and for several companies around the world. He recently joined Yahoo and works for Yahoo’s emerging markets group in Miami.

For the last two years he’s been focused on JavaScript as a development platform. Early last year he decided to create an easy-to-adopt YUI extension called “Bubbling Library” as a side project; you can read his YUIBlog introduction to the Bubbling Library here.

This article is about my Dispatcher Plugin (part of my Bubbling Library) and how to use it along with the YUI TabView to load on-demand content using the YUI Connection Manager. The Bubbling Library doesn’t ship with YUI, but it’s a free download and licensed under the same BSD terms as is YUI.

YUI TabView Overview

One of the most compelling components in the YUI Library is the TabView Control. It allows us to maximize the space in our web pages, ordering and segmenting the information and sharing the container (visualization area) at the same time. It’s easy to use as a developer and as an end user because the navigable tabbed views of content represent a well known design pattern in most OSs. Most users feel comfortable using TabView right away.

A Few Examples

One of my favorite characteristics of the YUI widgets is that they can be used to enhance the DOM in an unobtrusive way with little effort.  The TabView widget is no exception:

<div id="demo" class="yui-navset">     
  <ul class="yui-nav">         
    <li class="selected"><a href="#tab1"><em>Tab One Label</em></a></li>         
    <li><a href="#tab2"><em>Tab Two Label</em></a></li>     
  </ul>                 
  <div class="yui-content">         
    <div><p>Tab One Content</p></div>         
    <div><p>Tab Two Content</p></div>

  </div> 
</div>
<script type="text/javascript"> 
var myTabs = new YAHOO.widget.TabView("demo");
</script>

(Live demo.)

If Progressive Enhancement isn’t relevant in your implementation, you can create a TabView object purely from JavaScript, creating the tabs and rendering them into a specified DOM element. In this case, the content of the tabs will not be available for search engines, which is a drawback, but the content for each tab can be loaded on demand using AJAX or by injecting the HTML code directly during the definition of the tab. Let’s see an example:

var myTabs = new YAHOO.widget.TabView("demo");
myTabs.addTab( new YAHOO.widget.Tab({ 
label: 'Tab One Label',
content: '<p>Tab One Content</p>', active: true
}));
myTabs.addTab( new YAHOO.widget.Tab({
label: 'Tab Three Label', content: '<p>Loading, please wait...</p>',
dataSrc: 'tab2.html'
})); myTabs.appendTo(document.body);

(Live demo.)

This technique is mostly used in dynamic web applications. We can also combine the progressively enhanced and dynamic approaches, creating a tabview with the default content from the markup and then inserting the rest of the tabs on the fly. In this case, each tab will load the content using AJAX. Check out this example:

<div id="demo" class="yui-navset">     
  <ul class="yui-nav">         
    <li class="selected"><a href="#tab1"><em>Tab One Label</em></a></li>    
  </ul>                 
  <div class="yui-content">         
    <div><p>Tab One Content</p></div>

  </div> 
</div>
<script type="text/javascript"> 
var myTabs = new YAHOO.widget.TabView("demo");
var tab2 = new YAHOO.widget.Tab({ 
label: 'Tab Two Label',
content: '<p>Loading, please wait...</p>', dataSrc: 'tab2.html'
}) myTabs.addTab( tab2 );
myTabs.addTab( new YAHOO.widget.Tab({
label: 'Tab Three Label',
content: '<p>Tab Three Content</p>'
})); </script>

TabView uses the YUI Connection Manager to load the content on demand, and it handles all the AJAX logic under the hood. Keep in mind that the Connection Manager is subjected to the Cross Domain Policy, which means that you can only use URLs within the current domain. (In YUI 3.0, the successor to Connection Manager, the IO Utility, provides Flash-based support for cross-domain AJAX [demo].)

Sometimes we want to create enhanced functionality within the tab content (when a certain tab becomes active), and for this you can use “contentChange” or “beforeContentChange” events to execute certain actions. We could add this kind of logic to the previous example as follows:

<script type="text/javascript">
function handleContentChange(e) {
alert('tab2 was loaded...'); /* you can render a datatable within the tab now :-) */
}
tab2.addListener('contentChange', handleContentChange);
</script>

The Problem

But this approach can introduce certain dependencies and complexity within our code, because the actions for a certain tab’s content will be defined at the page level, which means that if you want change the content of tab, you probably need to change something at the application level as well — the functionality of the tab’s content is not encapsulated with that content.

Creating monolithic contents (on demand content that will encapsulate its own requirements, initialization process and behaviors) seems be a solution, but there is a problem: If you want load a certain content within a tab, and that content has some JavaScript functionality, that functionality will not be executed because. The tab widget will use innerHTML to replace the tab’s content. The browser will then strip out the “script” tags because it’s injecting the code within the tab, and that script content will not be executed at all. Generally this is a good thing, as it protects you from one potential vector for XSS attacks. But in the event you need to bring in scripts this way, it’s a problem you need to work around. (Keep in mind, though, that working around this puts the responsibility for security squarely on your shoulders. Using the event-driven approach described above is a better, more secure approach for applications where it’s possible to use it.)

If you go down the path of bringing scripts into the page with the content of a tab, the good news is that you can use a plugin to delegate the work, and it will handle the AJAX routine to execute and process the CSS and the JavaScript code within the on demand contents. This plugin, from my Bubbling Library, is called “Dispatcher” (YAHOO.plugin.Dispatcher), and it has been one of the most popular components from my Bubbling Library Extension.

YUI Dispatcher Plugin

The complexity of the code will be almost the same as what we’ve seen above; even the syntax is quite similar. Let’s start with an example:

<div id="demo" class="yui-navset">     
  <ul class="yui-nav">         
    <li class="selected"><a href="#tab1"><em>Tab One Label</em></a></li>    
  </ul>                 
  <div class="yui-content">         
    <div><p>Tab One Content</p></div>

  </div> 
</div>
<script type="text/javascript">
var myTabs = new YAHOO.widget.TabView("demo");
YAHOO.plugin.Dispatcher.delegate (new YAHOO.widget.Tab({
label: 'Tab Two Label', content: '<p>Loading, please wait...</p>',
dataSrc: 'tab2.html'
}), myTabs);
myTabs.addTab( new YAHOO.widget.Tab({
label: 'Tab Three Label',
content: '<p>Tab Three Content</p>'
})); </script>

The method “delegate” will do the job for you in an efficient way. The process is quite simple:

  • The Tab object loads the content using the YUI Connection Manager
  • Before injecting the code using innerHTML, it passes the code to the dispatcher.
  • The dispatcher strips out the JavaScript and CSS code from the content.
  • The dispatcher injects the parsed content (without JavaScript/CSS tags) within the tab area.
  • The dispatcher executes each JavaScript chunk and injects each CSS tag in the order in which they appear. Remote JavaScript and remote CSS will be loaded using the YUI Get Utility by default, which means that they are not subjected to the Cross Domain Policy.

(Live demo.)

Complex Example

Let’s look at a more complex example. In this case, the second tab loads some content, and the content defines its own functionality, creating/rendering a YUI DataTable within it. Exactly the same code from the previous example:

<script type="text/javascript">
YAHOO.plugin.Dispatcher.delegate (new YAHOO.widget.Tab({
label: 'Tab Two Label',
dataSrc: 'tab2.html'
}), myTabs, { /* Object literal with the area configuration */ });
</script>

And the content of the file "tab2.html" should look like this:

<link rel="stylesheet" type="text/css" 
href="http://yui.yahooapis.com/2.5.2/build/datatable/assets/skins/sam/datatable.css">
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/datasource/datasource-beta-min.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/datatable/datatable-beta-min.js"></script
<script type="text/javascript" src="http://bubbling-library.com/sandbox/dispatcher/data.js"></script>
<div id="basic" class="example"></div>
<script type="text/javascript">
var myColumnHeaders = [
{key:"id", sortable:true, resizeable:true},
{key:"quantity", type:"number", sortable:true, resizeable:true},
{key:"amount", type:"currency", sortable:true, resizeable:true},
{key:"title", type:"html", sortable:true, resizeable:true}
];
var myColumnSet = new YAHOO.widget.ColumnSet(myColumnHeaders);
var myDataSource = new YAHOO.util.DataSource(YAHOO.example.Data.bookorders);
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSARRAY;
myDataSource.responseSchema = {
fields: ["id","quantity","amount","title"]
};
var myDataTable = new YAHOO.widget.DataTable("basic", myColumnSet, myDataSource);
</script>

As you can see, the content of "tab2.html" is like another page, and the tab object works like an iframe. The differences are:

  • There is a single document (less memory and bandwidth used by the browser).
  • The tab is sharing the execution scope with the current document (easy JavaScript integration).
  • There is more flexibility for cosmetics because the container is just another DIV within the current document instead of being an iframe.

(Live demo.)

Memory Leaks

Now that we can create widgets and add listeners to certain elements within the tab content, we need to keep in eye on the memory management, especially in the tabs where we aren’t caching the dynamic content. In those tabs, the content will be loaded multiple times, replacing the old content and executing the scripts again. By default, the dispatcher releases all the listeners in the area before it displays new content using YAHOO.util.Event.purgeElement. But sometimes this is not enough.

The destroyer is a hook method and is only available during the execution of the scripts from dynamic content, which means that you can use this within the tab content:

YAHOO.plugin.Dispatcher.destroyer.subscribe (function(el, config){   
  // el: HTML Element that grap the tab’s content   
  // config: Object literal with the area configuration */ 
}); 

And because the destroyer is a YUI Custom Event, you can add multiple listeners to it. The dispatcher plugin fires the Custom Event when you try to load a new content within the same tab.

You can load fresh content within the tab by setting the cache property to false during the creation of the tab, and every time you click on the tab the YUI Connection Manager loads the content again.

At this point, the dispatcher fires the destroyer Custom Event before switching the content of the DIV (tab’s wrapper). Then it will setup a new destroyer object for the new content, and you will be able to add new listeners for the new content.

This technique will allow you to create widgets and destroy them in a more memory-friendly way when the content changes.

(Live demo.)

More Features

The dispatcher plugin is very flexible and has the following features:

  • Load content within a general container (DIV)
  • Browser history integration
  • Loading Mask integration
  • Customize the execution proccess
  • CSS path correction process

Upcoming Features

  • Layout Manager 2.6.0 integration
  • Form handling (automatic submit process thru Connection Manager)

Requirements

Include the dispatcher plugin after the connection manager, and don’t forget to include the tabview widget as well, but not necesary before the dispatcher:

<script type="text/javascript" 
src="http://yui.yahooapis.com/2.5.2/build/yahoo-dom-event/yahoo-dom-event.js"></script
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/connection/connection-min.js"></script>
<script type=”text/javascript” src=’http://js.bubbling-library.com/1.5.0/build/dispatcher/dispatcher-min.js’></script
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/tabview/tabview-min.js"></script>

Troubleshooting

Handling Errors (Debugging)

There are two configuration options to handle errors:

YAHOO.plugin.Dispatcher.delegate (new YAHOO.widget.Tab({
label: 'Tab Two Label',
dataSrc: 'tabs2.html'
}), myTabs, { onError: function (el) {
// el: DOM Reference for the area
// when the YUI connection manager call the failure method
// 403, 404, etc
},
error: function (el, jsCode) {
// el: DOM Reference for the area, jsCode: javascript code
// this method get fired when a JS error occur during the execution
// this is for debugging only...
}
});

(Live demo.)

Anonymous Functions

The dispatcher plugin uses an anonymous function to execute inline scripts, introducing a problem with the global variables. Let’s see an example:

Original Content:

function myFunc () {   
  // your stuff here 
}
var myVar = 1; 

The dispatcher transforms it into this:

(function() { 
  function myFunc () { 
    // your stuff here 
  } 
  var myVar = 1; 
})();

So, “myFunc” and “myVar” are not global variables and are only accessible within the same script tag.

To tackle this issue, you should use namespaces:

YAHOO.example.myFunc = function () { 
  // your stuff here 
};
YAHOO.example.myVar = 1;

Or the nasty trick to force it to be global:

window.myFunc = function () { 
  // your stuff here 
};
window.myVar = 1; 

Customizing the execution routine

There is a configuration argument to
customize the evaluation routine. The code should look like this:

YAHOO.plugin.Dispatcher.delegate (new YAHOO.widget.Tab({
label: 'Tab Two Label',
dataSrc: 'tabs2.html'
}), myTabs, { evalRoutine: myCustomEval
});

In this case, myCustomEval should execute the script.

(Live demo.)

Working Examples

  • TabView and Dispatcher

    In this example the dispatcher manages the content inside the tabs, executing the scripts (remote and inline "script" tags) during the dataSrc request.

  • Nested TabViews
    In this example, we use two different approach: one using the tab’s events to modify the loaded content and a second sing the dispatcher to leave the task to the content which is a more flexible approach.
  • Tabview, DataTable and Dispatcher (check the last tab)

    In this example the dispatcher manages the content inside a tab to render a YUI Datatable during the dataSrc request.

  • How to avoid memory leaks
    In this example, you can see how the first tab "Datatable Control" define the rules to destroy the YUI Datatable.
  • More examples here.

9 Comments

  1. Awesome post!I just started learning YUI yesterday and I was looking around for a way to include a datatable in a tab when I stumbled upon your post.Thanks!

  2. Hi I have a doubt.Not with your dispatcher module but with one of your code snippets

    Tab One Label

    Tab One Content

    var myTabs = new YAHOO.widget.TabView(“demo”);
    var tab2 = new YAHOO.widget.Tab({
    label: ‘Tab Two Label’,
    content: ‘Loading, please wait…’,
    dataSrc: ‘tab2.html’
    })
    myTabs.addTab( tab2 );
    myTabs.addTab( new YAHOO.widget.Tab({
    label: ‘Tab Three Label’,
    content: ‘Tab Three Content’
    }));

    the tabs are displayed.but in tab2 all i get is the ‘page loading’ message but the page tab2.html doesnt actually load.

    I have made sure that tab2.html is in the same directory as the file containing this snippet.

    Any information you can provide would be of great help to me.
    Thanks.

    Also,I havent tried your library-reason being i got stuck at this snippet…I think your concept is great and really useful to a lot of people

  3. Hey ls86,

    Here is a working example:
    http://www.bubbling-library.com/sandbox/yui2/tabview/markup-dynamic-static.html

    There are two possible reasons why this particular code can fail:

    1. You didn’t included the YUI Connection Manager in your page.
    2. You are trying to run the example locally, and you need a webserver in order to use the Browser AJAX capabilities.

    Anyway, feel free to contact me directly to review your code.

  4. Hi Caridy,
    Is this the only way to load a Datatable in a tab?

    I created a tab and then placed a div inside to hold the Datatable:

    In my application, the content for the history tab is initially blank. The datatable is dynamically created only after a user does a search. When the search results return, I then populate the table:

    var histClmnDefs = [
    {key:"request_date", label:"Request Date", sortable:true},
    {key:"info_serv_desc", label:"Requests", sortable:true, formatter:this.formatInfoServ},
    {key: "appoint_date", label:"Appointment", sortable:true},
    {key: "interp_date", label:"Mid-Tier", sortable:true},
    {key: "comments", label:"Comments"}
    {key: "check", label:"Delete", formatter:"checkbox"}
    ];

    this.dsHistory = new YAHOO.util.DataSource(“includes/controllers/dbActionController.php?”+query);
    this.dsHistory.responseType = YAHOO.util.DataSource.TYPE_JSON;
    this.dsHistory.responseSchema = {
    resultsList: “Result”,
    fields: ["request_id", "request_date", "appoint_date", "service_date", "interp_date", "comments"]

    };

    this.dtHistory = new YAHOO.widget.DataTable(“requestorHistory”, histClmnDefs, this.dsHistory, {
    selectionMode:”single”,
    scrollable:true,
    height:”20em”
    });

    The problem I am having is that the Datable does not display. I can see in Firebug that the table is being created (with the data), but the only thing that appears on the screen is a vertical line where the left edge of the table begins.

    I can also Inspect each column (in Firebug), so I know that the columns are there.

    Even more interesting, if I return data where the JSON record is malformed, the Datable does appear in the tab (all of the columns appear, but obviously no data).

    I am not creating the script inside the tabbed content as in your above examples, so I am not loading an external html file. I just don’t understand why the table does not appear on the screen even though it is obviously created (but does appear if there is a data error).

    Thanks for you help. Les.

  5. Hi,

    Thanks, this is v. helpful.

    A little snippet in case it’s of any use to anyone. I’ve got my tabs all from the html using progressive markup but with no content loaded initially. This little loop goes through them and 1. sets the dataSrc to the href of the tab, and 2. uses the Dispatcher delegation to load all my lovely text editors, context menus, datatables…

    // my TabView built from markup
    var bodyTabs = new YAHOO.widget.TabView( “bodytabs” );

    // loop through the tabs
    for each( t in bodyTabs.get( “tabs” ) ) {
    // set the dataSrc
    t.set(“dataSrc” , t.get( “href” ) );
    // use the Dispatcher
    YAHOO.plugin.Dispatcher.delegate ( t );
    }

    // and this loads the content of the first tab
    bodyTabs.selectTab(0);

  6. @Matt: nice job, it’s always good to see people whom care about progressive enhancement.

    One more thing (an update), dispatcher 2.1 is ready on github, and this time solving the issue with “Anonymous Functions”. Here is the source code:

    http://github.com/caridy/bubbling-library/tree/c05b1e552fc3e8ae3fbd542c441c914ea8861cfe/2.0/build/dispatcher

  7. @Matt (myself!) OK, now the IE compatible version that doesn’t depend on a for each loop:

    var bodyTabView = new YAHOO.widget.TabView( “bodytabs” );
    var bodyTabs = bodyTabView.get(“tabs”);

    for ( t in bodyTabs ) {
    bodyTabs[t].set(“dataSrc” , bodyTabs[t].get(“href”) );
    bodyTabs[t].set(“cacheData” , true );
    YAHOO.plugin.Dispatcher.delegate( bodyTabs[t] );
    }
    bodyTabView.selectTab(0);

  8. Hi Caridy,

    I want to call HTMLpage specific Javascript function from yui tab.
    the page is fetched from URL.

    I have tried the your utilities.js way but it gives me “access is denied” Error evrytime.

    I have also,set various browserlevel setting in IE but this doesnt work.
    So can you tell me how can i solve this issue ASAP…

  9. hi.
    example tab2 :
    ….

    createToolBarComposeMail();


    but function createToolBarComposeMail() not call.
    I need help.
    Thank!