Remote Load-on-Demand with the Ignite UI igTree

Craig Shoemaker / Wednesday, March 27, 2013

The Ignite UI igTree is a powerful client-side jQuery UI widget which has a ton of great features baked right into the control. One of the most valuable features of any tree is its ability to support load-on-demand from a remote server. On a recent project I used the tree to read through a series of XML files which represented many megabytes of data – a remote load-on-demand configuration was must in this situation.

In this example I strip away all the complexities of the file system and server and just isolate the tree to demonstrate how to configure the control for a remote load-on-demand scenario. Along the way I’ll also demonstrate how to use a few very handy JavaScript and JSON mocking libraries to help isolate work with the tree.

The following image shows how the tree works configured for load-on-demand where each node click initiates a request for its children, the node is auto-expanded and selected and the node’s URL data member is shown in the box above the tree.

Note: The URLs used in this example are generated lorem ipsum text. The particulars of how the sample data is generated is detaild in the section Mocking Ajax Requests and Data.

igTree working in remote load-on-demand context.

Code Download

If you would like to work directly with the source files used in this example, you can download them here.

Getting Started

The first step is to create a self-executing anonymous function which then sets up the Ignite UI loader. Inside the function, the loader is configured to have known locations for the dependent JavaScript and CSS files. As represented by the comments, when the ready event fires the code to configure the tree is run. The setup for the tree is detailed in the next section.

(function () {

    $.ig.loader({
        scriptPath: 'http://localhost/js/',
        cssPath: 'http://localhost/css/',
        resources: 'igTree',
        ready: function () {

	   // add rest of code here

        }
    });
})();

Setting Up the Tree

Now that the page is ready and all the scripts and styles are loaded, the next step is to set up the tree control. The tree is instantiated by selecting the root element via the #toc selector and calling the igTree function. Next, there are a handful of options required to set the tree in a load-on-demand state. The singleBranchExpanded option tells the tree to only allow one branch of the tree to remain expanded at one time (this feature isn’t load-on-demand specific, but does make the tree easier to use for the user). The dataSourceUrl option tells the tree which URL to use to request data items in the tree. The value of remoteUrl the dataSourceType option designates to the tree that data is coming from a remote source. The loadOnDemand option tells the tree that child items in the tree will only be bound to the tree once a request for the child items is made.

The comments represent a place where the event handlers will go which allow the grid to load data when a node is expanded. Finally, incoming data is bound to the tree based on the mappings defined by the bindings object literal found at the end of the control definition.

var tree = $('#toc').igTree({
    singleBranchExpand: true,
    dataSourceUrl: '/api/items',
    dataSourceType: 'remoteUrl',
    loadOnDemand: true,

    // Add event handlers here

    bindings: {
        textKey: 'title',
        navigateUrlKey: 'url',
        childDataProperty: 'items',
        valueKey: 'url'
    }
});

Event Handlers

The previous code configured the tree for load-on-demand, but the implementation found in the event handlers is what gives the tree the desired behavior.

The nodePopulating event fires when the tree is attempting to load the child nodes of a selected node. The implementation of this event handler sets the dataSourceUrl to a location specific for the selected node each time a new node is being populated. (When the tree is configured for load-on-demand, nodes are only populated when a node is being expanded.)

    nodePopulating: function (event, ui) {
        tree.igTree('option', 'dataSourceUrl', '/api/items?id=' + ui.data.id);
        return true;
    }

The next event is the nodeClick event. The nodeClick event is handled in order to add behavior to automatically expand and select the clicked node. The first line, e.preventDefault(), is needed in order to stop the browser from attempting to navigate away from the page. Next, the tree is given commands to toggle the clicked node (where it will open if it’s closed, and vice versa) and then select it.

The call to the clicked function simply takes the current ui.node.data.url value and prints it on the screen. The last operation is to return false, again to make sure that the control stops any further processing.

    nodeClick: function (e, ui) {
        e.preventDefault();
        tree.igTree('toggle', ui.node.element);
        tree.igTree('select', ui.node.element);
        clicked(ui.node.data.url);
        return false;
    }

Note: Since the settings are being passed to the tree as an object literal, the event functions must be followed by a comma after the closing brace to the function. The full code listing at the bottom of this post shows each of the event handlers in context of the object literal.

Once the tree is set up with these options and the event handlers are implemented, the tree is fully configured for remote load-on-demand.

Mocking Ajax Requests and Data

While the tree is in working order, providing data to the tree can get involved if you need to set up a server side counterpart to handle serving data to the client. A technique I used in this example was to mock the JSON data that is provided to the tree. This is made possible by leveraging two JavaScript libraries. The first library is Mockjax which intercepts Ajax requests made by the tree and allows them to be fulfilled locally - all the while returning an identical response a server would send back to the tree.

The second library I used was mockJSON which is an engine responsible for generating mock data in JSON format based on some rules you provide. The nice thing that once the mocking is setup, all the mocking logic is easily separated from the standard executing code. In this example, moving all the mocking code into a separate file is trivial. When you are ready to remove mocked requests and data, all you have to do is simply remove the reference to the file and the tree will then go back to contacting the server just like normal using the URLs configured in the dataSourceUrl option.

Full Code Listing: Load-on-Demand Demo (index.html)

(function () {

    $.ig.loader({
        scriptPath: 'http://localhost/js/',
        cssPath: 'http://localhost/css/',
        resources: 'igTree',
        ready: function () {

            var tree = $('#toc').igTree({
                singleBranchExpand: true,
                dataSourceUrl: '/api/items',
                dataSourceType: 'remoteUrl',
                loadOnDemand: true,

                nodePopulating: function (event, ui) {
                    tree.igTree('option', 'dataSourceUrl', '/api/items?id=' + ui.data.id);
                    return true;
                },

                nodeClick: function (e, ui) {
                    e.preventDefault();
                    tree.igTree('toggle', ui.node.element);
                    tree.igTree('select', ui.node.element);
                    clicked(ui.node.data.url);
                    return false;
                },

                bindings: {
                    textKey: 'title',
                    navigateUrlKey: 'url',
                    childDataProperty: 'items',
                    valueKey: 'url'
                }
            });

            var clicked = function (msg) {
                $("#result").text(msg.toLowerCase());
            }
        }
    });
})();

Full Code Listing: Mocking Requests and Data (mock.data.js)

(function () {

    // mock requests to items
    $.mockjax({
        url: '/api/items',
        contentType: 'text/json',
        response: function () {
            this.responseText = generateResponse(true);
        }
    });

    // add wildcard mock to capture requests with query string values
    $.mockjax({
        url: '/api/items?*',
        contentType: 'text/json',
        response: function () {
            // randomizes whether or not the response has child elements
            var hasChildren = parseInt((Math.random() * 10), 10) % 2;

            this.responseText = generateResponse(hasChildren);
        }
    });

    var generateResponse = function (includeItems) {
        var data = $.mockJSON.generateFromTemplate({
            "items|5-5": [{
                "id|+1": 1,
                "title": "Item",
                "url": "http://@LOREM.com",
                "items": includeItems ? [] : null
            }]
        });
        return data.items;
    }
})();