Home > Blog > 2014 > Sep > 23

Enterprise MVC Design Patterns: Single-Load AJAX Tabs

by Ira Endres on Tuesday, September 23, 2014

Firstly, We Love jQuery UI Tabs

What a fantastic and powerful widget, completely automated, and would you believe, already supports remote loading tabs via AJAX? It's super simple.

<div id="my-tab">
    <ul>
        <li><a href="#tab-info">Info</a></li>
        <li><a href="http://www.google.com">Google</a></li>
        <li><a href="http://www.duckduckgo.com">Duck Duck Go</a></li>
        <li><a href="http://www.bing.com">Bing</a></li>
    </ul>
    <div id="tab-info">
        <p>Click a Search Engine.</p>
    </div>
</div>

<script type="text/javascript">
    $('#my-tab').tabs();
</script>

Part of the magic of the existing jQuery AJAX tabs functionality is that is able to differentiate between local anchor links and remote URLs. If you forgot to create target <div> containers, like in the above example, jQuery UI Tabs automatically creates the target for the content with unique identifiers. With all of this fantastic functionality, there exists a scenario where the default tabs behavior doesn't meet the application requirements: single-load tabs.

Scenario: One-Time Load on Activate

The default behavior for remote tabs is to perform an AJAX request to fetch the tab contents any time it is activated. While this behavior is handy, enterprise web applications can have enormous payloads, something we do not want to reload unless it is necessary. Our application requirements are:

  • Load the tabs only upon request
  • First tab needs to be automatically loaded
  • Once a tab is loaded, the content should not automatically reload
    • We should provide a way to manually reload the content and modify the request data

A quick look at our initial requirements and only the last two are not covered by the default behavior of the jQuery UI Tabs. While we do have access to the jqxhr request object on the events and can tie in and cancel them, this results in a significant amount of code duplication in logic; specifically the logic where we determine when to allow tabs to dynamically load the contents and when to activate the existing contents.

Solution

Our solution is to create our own jQuery super widget that wraps the existing jQuery UI Tabs functionality and automatically binds the events and abstracts the additional requirements while requiring as little as possible that the developer needs to supply to complete the functionality.

Create the Widget

We will start out with utilizing the jQuery UI Widget framework rather than using the function extension because the widget factory abstracts a lot of the process of creating stateful objects and encapsulation of the components in JavaScript. To create a jQuery UI Widget, we must have both jQuery and jQuery UI loaded into the DOM. The widget requires a few required properties, version, delay, options, and the _create function which is called to instantiate the object.

$.widget('ui.ajaxTabs', {
    version: '0.0.128',
    delay: 256,
    options: {},
    _create: function() {
    }
});

_Create Method

As one of the goals of the widget will be to have as little development as possible for the developer to do, we want to emulate the same markup style that the existing jQuery UI Tabs utilizes. The widget has a few features we want: local tabs, remote tabs, and optional creation of the target div containers. We want to differentiate remote tabs by the presence of an actual URL in the href attribute of the <a> inside the <li>. However the URL cannot stay inside the href attribute; if the default Tabs functionality sees a URL there, it will treat it as a remote tab. The URL must go in a place where we know where it is, and Tabs needs to believe that the content is a local tab. To do this, we need to do the following:

  1. Iterate over the <a> in the tabs and detect the presence of a URL
    1. If it has a URL
      1. Move the URL to a custom data attribute
      2. Assign a target container id into the href
    2. Check if the target <div> container exists
      1. If not create a target with the <a href=""> value the containers id attribute

We will first populate a list of existing target containers and their ID tags; this will be for reference when create the missing target containers.

_create: function () {
    var that = this;
    var targets = [];

    // what divs already exist?
    this.element.children('div').each(function() {
        var id = $(this).attr('id');
        if (id != null && id != '') {
            targets[targets.length] = id;
        }
    });
}

Next we will iterate over the tabs and check for remote tabs. We will move any detected URL to a custom attribute data-url. We will also go ahead an assign id values for those tabs where we have moved the URLs.

_create: function () {
    var that = this;
    var rhash = /#.*$/;
    var targets = [];
    this.tabs = [];

    // what divs already exist?
    this.element.children('div').each(function() {
        var id = $(this).attr('id');
        if (id != null && id != '') {
            targets[targets.length] = id;
        }
    });

    // normalize tabs, create any needed div targets
    this.element.children('ul').children('li').each(function (index) {
        var a = $(this).children('a');
        var href = a.attr('href');
        that.tabs[index] = a;

        // Remote tabs
        if (href.search(rhash) != 0) {
            a.attr('data-url', href);
            href = 'ui-ajax-tabs-' + index;
            a.attr('href', '#' + href);
        }

        // check if target div exists, create if not found
        var id = href.replace('#', '');
        if($.inArray(id, targets) == -1) {
            that.element.append($('<div />').attr('id', id));
        }
    });
}

Beautiful. Next we need to bind to the click events on the remote tabs to make the call to load the content. These events need to be able to determine if the requested tab has already been loaded so we will add another custom attribute data-loaded onto the <a> tag to determine if the content has already been loaded. Also, it would be handy for the <a> tag to know what index it is, so we will add a final custom data attribute data-tab-index to store that value for reference in the click event. Once we have constructed our tabs, created any missing target containers, and bound our events to the remote tabs, we will initialize the standard jQuery UI Tabs and make the call to load the first tab. Our final code for the _create function looks like this:

_create: function () {
    var that = this;
    var rhash = /#.*$/;
    var targets = [];
    this.tabs = [];

    // what divs already exist?
    this.element.children('div').each(function() {
        var id = $(this).attr('id');
        if (id != null && id != '') {
            targets[targets.length] = id;
        }
    });

    // normalize tabs, create any needed div targets
    this.element.children('ul').children('li').each(function (index) {
        var a = $(this).children('a');
        var href = a.attr('href');
        that.tabs[index] = a;
        a.attr('data-tab-index', index);

        // Remote tabs
        if (href.search(rhash) != 0) {
            a.attr('data-url', href);
            a.attr('data-loaded', false);
            href = 'ui-ajax-tabs-' + index;
            a.attr('href', '#' + href);

            // bind the click event
            a.click(function (e) {
                var c = $(this);
                var dataUrl = c.attr('data-url');
                if (dataUrl != null && dataUrl != '' && c.attr('data-loaded') != 'true') {
                    that.load(parseInt(c.attr('data-tab-index')));
                }
            });
        }

        // check if target div exists, create if not found
        var id = href.replace('#', '');
        if($.inArray(id, targets) == -1) {
            that.element.append($('<div />').attr('id', id));
        }
    });

    // create standard jquery tabs
    this.element.tabs();

    // load first tab
    if (this.tabs.length > 0) {
        this.load(0);
    }
}

Did you savvy coders notice anything that was added but not implemented? There are two references to a load function. That is the final function, or more correctly the widget method, that our implementation should implement; a way to make a call to load, activate or reload on a particular tab based on its type (local or remote) and whether or not it is loaded.

The load Method

The load method will be responsible for actually making the AJAX request to the remote server via the data-url attribute on our tab <a> links. As stated in the specifications, we ought to provide a way to pass additional form data to the method to programmatically allow additional information to be passed to the request in a standard format. Our load method should take 2 parameters, the index of the tab we want to load, and an object of additional properties we want to send with the request.

To accomplish this, we need to implement the following program flow:

  1. Check if local tab
    1. Activate local tab and exit
  2. Create an new AJAX request
  3. Append additional data to the request
  4. Execute the request
  5. On successful completion
    1. Set the target tab contents with the returned data
    2. Update the tabs data-loaded attribute to true

This is relatively straight forward:

load: function (index, data) {
    var that = this;

    // is it a local tab?
    var url = this.tabs[index].attr('data-url');
    if (url == null || url.length == 0) {
        this.tabs[index].trigger('click');
        return;
    }

    // execute the request
    var url = this.tabs[index].attr('data-url');
    $.post(url, data, function(response) {
        var target = that.element.find(that.tabs[index].attr('href'));
        target.empty().html(response);
        that.tabs[index].attr('data-loaded', true);
    }, 'html');
}

Final Implementation

With less than 100 lines of code, we have simplified the process of creating single-load AJAX tabs with our own jQuery super widget that wraps the existing jQuery UI Tabs functionality with a stateful object model allowing a developer to easily create tabs that load one time and can be programmatically reloaded in code with additional form data being passed to the request. This is a powerful abstraction of the logic that facilitates this functionality while at the same time, reducing code complexity and decreasing development time for this scenario.

The final markup and code for single-load tabs that the developer now needs to write is as follows:

<div id="my-tab">
    <ul>
        <li><a href="#tab-info">Info</a></li>
        <li><a href="http://www.google.com">Google</a></li>
        <li><a href="http://www.duckduckgo.com">Duck Duck Go</a></li>
        <li><a href="http://www.bing.com">Bing</a></li>
    </ul>
    <div id="tab-info">
        <p>Click a Search Engine.</p>
    </div>
</div>

<script type="text/javascript">
    $('#my-tab').ajaxTabs();
</script>

And to reload a tab, such as the Google tab the developer just needs to call $('#my-tab').ajaxTabs('load', 1, {q: 'time UTC'}); to reload the tab with the additional search query "time UTC".

Additional Suggestions for Improvement

An who wouldn't want to add more features to this tool? With the addition of a few more features, this client-side library can add additional flexibility for the developer:

  1. Add ability to override standard jQuery AJAX request options such as type, url, and dataType as well as AJAX events such as error, success, or complete.
  2. Provide a default set of AJAX options in the request such as specifying all requests have the type POST unless overridden by the load options.
  3. Unbind the tab click event after its usage (it's not used again)

Conclusion

This has been a quick look into one of the common feature requests for enterprise-level MVC applications: single-load AJAX tabs. Again, this is an incredibly useful feature to have in a web application where we could be loading and then reloading large amounts of data on the screen and we need to perform these actions in as efficient manner as possible, while keeping the development footprints for complexity and development time as low as possible.

Like our work? Be sure to connect with us via Facebook, Twitter, and Linked In to stay up to date with Software Development here in Atlanta and beyond.