Saturday, October 15, 2011

Working with YUI2 Data Table and Script Node Data Source

Data Table might be one of the most powerful widgets in the YUI2 library. It supports column formatter, sorter, pagination, column resizing, column reordering, and most importantly, data binding. Like other YUI2 widgets, Data Table is able to utilize YUI's universal Data Source APIs to bind data to UI components. You just specify where the data comes from, how the data looks like, and which UI parts to bind certain pieces of data. The Data Source will do the heavy lifting for you. It takes care of sending/retrieving data, parsing data, and feeding parsed data to the associated widget. Pretty powerful stuff.

In this article, I will demonstrate how to use YUI DataTable and DataSource to create a page that lets users search client records by client's first name, last name, and ID. If any client record is found, we display the search results in a table on the same page.

For DataSource, I use ScriptNodeDataSource. One of the major advantages of the ScriptNodeDataSource is that its data requests can be sent across domains by using JSONP (JSON Padding) instead of XHR. More discussions about JSONP can be found here.

First, let's define a ScriptNodeDataSource and response schema.

// Setup remote data source
var ds = new YAHOO.util.ScriptNodeDataSource(
    'http://www.anotherdomain.com/search/');

// The response JSON will have a results array
// Each result object has userId, firstName, lastName, birthDate, 
// address1, address2, address3, city, state, and zip properties.
ds.responseSchema = {
    resultsList: "results", 
    fields: [ "userId", "firstName", "lastName", "birthDate", 
        "address1", "address2", "address3", "city", "state", "zip" ]
};

Define table columns. Use column formatters for column name, date, and address. Sort table rows by names.

//
// Column formatters
//

// Format column Name
var formatName = function(elCell, oRecord, oColumn, oData) {
    // Concat the last name and first name
    var strName = oRecord.getData("lastName") + ", " 
        + oRecord.getData("firstName");

    // Wrap name in a link that goes to client details page
    var strUserId = oRecord.getData("userId");
    elCell.innerHTML = '<a href="' + getResultUrl(strUserId) 
        + '">' + strName + '</a>';
};

// Format column DOB
var formatDate = function(elCell, oRecord, oColumn, oData) {
    if (YAHOO.lang.isString(oData))
    {
        if (Y.env.ua.ie > 0)
        {
            // IE has problem to parse date string "yyyy-mm-ddT00:00:00"
            // Here, we fall back to manipulating the date string
            elCell.innerHTML = oData.split("T")[0].replace(/-/g, "/");
        }
        else
        {
            var oDate = new Date(oData);
            elCell.innerHTML = oDate.format("mm/dd/yyyy");
        }
    }
};

// Format column Address
var formatAddress = function(elCell, oRecord, oColumn, oData) {
    var strAddr = oRecord.getData("address1") + " "
        + oRecord.getData("address2") + " " 
        + oRecord.getData("address3");
    strAddr = strAddr.trim() + ", " + oRecord.getData("city") + ", " 
        + oRecord.getData("state") + " " + oRecord.getData("zip");

    elCell.innerHTML = strAddr;
};

//
// Sorters
//

// Sort by name
var sortName = function(a, b, desc) {
    var fnComp = YAHOO.util.Sort.compare;
    var compState = fnComp(a.getData("lastName"), 
            b.getData("lastName"), desc);
    if (compState == 0)
    {
        compState = fnComp(a.getData("firstName"), 
            b.getData("firstName"), desc);
    }

    return compState;
};

// Column definitions
var colDefs = [ 
    { 
        key: "name", label: "Name", 
        resizeable: true, sortable: true, 
        formatter: formatName, // formatName column formatter
        width: 120, 
        sortOptions: { sortFunction: sortName } // sortName sort function
    }, 

    {
        key: "address", label: "Address", 
        resizeable: true, sortable: true, 
        formatter: formatAddress, // formatAddress column formatter
        width: 250
    },

    {
        key: "birthDate", label: "DOB", 
        resizeable: true, sortable: true, 
        formatter: formatDate // formatDate column formatter
    },

    {
        key: "userId", label: "Client ID", 
        resizeable: true, sortable: true
    }
];

Setup table configuration. When the table is created, the data table will send out an initial request to get data. We want to capture this initial request, and prevent the server side from starting any search work, because at this moment our user hasn't filled any search keywords in the text fields yet (First Name, Last Name, and Client ID text fields). The initial request is not triggered by our users. It has to be filtered out. To do this, we append "&init=true" parameter to the initial request's URL so the server side will know.

// Table configurations
var tableCfg = {
    initialRequest: "&init=true", 
    sortedBy: {
        key: "name", dir: "asc"
    }, 
    width: "100%", 
    height: "30em", 
    MSG_LOADING: "", 
    MSG_EMPTY: ""
};

The beef is here --- the search function which is responsible of gathering user inputs, validation, clearing previous search results in the table, constructing search queries, sending out query requests, displaying returned results, and handling errors.

// Field validation
var validate = function(params)
{
    // Validation logics go here ...

    return true;
};

// Search function. 
// It will be invoked when users click the "Search" button
var fnSearch = function(e) {

    // Suppress form submission
    YAHOO.util.Event.stopEvent(e);

    // Get search field values
    var params = {
        "firstName": document.getElementById("firstName").value,
        "lastName": document.getElementById("lastName").value,
        "userId": document.getElementById("userId").value
    };

    // Field validations
    if (validate(params) == false)
    {
        return false;
    }

    // Callbacks for datasource.sendRequest  
    var callbacks = {
        success: function(oRequest, oParsedResponse, oPayload) {
            console.log("Retrieved search results");

            // Enable the table
            table.undisable();
    
            // Flush and update the table content
            table.onDataReturnInitializeTable.apply(table, arguments);

            // Sort by name in ascending order
            table.sortColumn(table.getColumn("name"), 
                YAHOO.widget.DataTable.CLASS_ASC);

            // Update the count of search results
            document.getElementById("results-count").innerHTML = 
                " - " + oParsedResponse.results.length + " result(s)";
        },

        failure: function() {
            console.log("Failed to get search results");

            // Failure handling code
        },

        scope: table
    };

    // Delete any existing rows, clear result count, 
    // and disable the table
    table.deleteRows(0, table.getRecordSet().getLength());
    document.getElementById("results-count").innerHTML = "";
    table.disable();

    // Construct search query
    var strQuery = "";
    for(var key in params)
    {
        strQuery += "&" + key + "=" + params[key].trim();
    }

    // Send out query request
    ds.sendRequest(strQuery, callbacks);
    console.log("Data source sent out request");

    return false;
};

Hook up the search function with the button click event. And finally, create the table.

YAHOO.util.Event.addListener("search-btn", "click", fnSearch);

// Construct data table. Pass in column definitions, data source, 
// and table configuration
var table = new YAHOO.widget.ScrollingDataTable("results-table", 
    colDefs, ds, tableCfg); 
console.log("Constructed data table");