Introduction to Shield UI JavaScript DataSource Widget

In this blog post entry, we will review in greater detail the ShieldUI JavaScript DataSource component - a versatile data component that aims to simplify data retrieval and manipulation.
It provides out-of-the-box binding to various data sources:

  • Javascript arrays and objects
  • JSON web services
  • JSONP web services
  • XML strings and web services
  • HTML table elements

Through its flexible configuration, the ShieldUI DataSource component abstracts out heterogenous data retrieval into an intuitive, unified API. It simplifies working with client-side data using JavaScript and enables data sharing across different components on the same HTML page. Shield UI DataSource fully supports paging, sorting and filtering both on the client-side and server-side.

The sections below elaborate in greater details on some of the features of the DataSource component. To see it in action, review the following demo section, which utilizes the ShieldUI DataSource widget to pass data to a grid component.

Getting Started

Shield UI DataSource is a JavaScript component that simplifies working with data on the client-side. It is part of the core Shield UI scripts and is available on the page once the JavaScript references are added:

<!DOCTYPE html>
<html>
<head>
<title>Shield Grid</title>
<meta charset="utf-8" />

<link rel="stylesheet" type="text/css" href="css/shieldui-all.min.css" />
<script src="js/jquery-1.10.2.min.js" type="text/javascript"></script>
<script src="js/shieldui-all.min.js" type="text/javascript"></script>

</head>
<body>

</body>
</html>

The DataSource component is initialized through the shield.DataSource constructor, passing initialization options as a first argument object literal:

$(function () {
	var ds = new shield.DataSource({
		data: [/* data array*/],
		skip: 3,
		take: 5,
		sort: { path: "path.to.sort.field", desc: true },
		filter: { path: "path.to.filter.field", filter: "eq", value: 13 }
	});
	
	ds.read().then(function () {
		var dataView = ds.view;
		//dataView contains the filtered, sorted and paged data
	});
});          

After a data source component is created, it needs to read the data and apply the specified data transformations. The read() method of the data source component returns a jQuery Promise object that can be waited for. When the success function is called, the view field of the component contains the result of the transformation as a JavaScript array. This can then be used for display on the page.

Binding to HTML Table Element

Shield UI DataSource supports binding to

elements in the HTML page. To bind to an HTML table, use the data initialization option to specify a CSS selector, HTML Table DOM element or a jQuery object containing a table element. Addtionally, set the schema.type setting to "table":
var ds = new shield.DataSource({
	//can also be a table DOM element or jQuery object
	data: "#myTable",
	schema: {
		type: "table"
	}
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains data parsed from the HTML table
});

When the read() method is called, DataSource parses the data from the HTML table and builds a JSON array. Each element in the resulting array is a JSON object representing a single row from the table’s <tbody>, where field names are extracted from the respective table header (matching the "<thead> <th>" selector) and field values are extracted from each cell in the table row (matching the "<tbody> <tr>" selector). Further filter, sort, skip and take operations are then applied to the result array, producing the final view.

Binding to Local Data

To bind Shield UI DataSource to local data, provide the data initialization option when creating a new DataSource instance. The data setting must be a JavaScript array or an object containing the data in a nested property:

var ds = new shield.DataSource({
	data: [/*...*/]
});

ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the local data
});

Alternatively, if the local data is a JavaScript array and there are no additional initialization options, you can provide the data array directly as a first constructor argument:

var myDataArray = [/*...*/];
var ds = new shield.DataSource(myDataArray);
ds.read().then(function () {
	var dataView = ds.view;
	//...
});

Once the DataSource is initialized, the local array is accessible through the data property of the DataSource.

Binding to Remote Data

Shield UI DataSource supports binding to remote web services. The remote.read initialization options provide configuration for reading from remote a web service:

var ds = new shield.DataSource({
    remote: {
        read: {
            url: "http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=b9z6zc9p7yut8cjen5bjpc4b",
            dataType: "jsonp",
            data: function () {
                return {
                    q: "Harry Potter"
                }
            }
        },
        cache: true
    },
    events: {
        change: function () {
            var view = ds.view;
            //do something with the data
        }
    }
});
ds.read();

The remote.read.url setting specifies the remote URL to read from. When Shield UI DataSource is configured for remote binding, it makes an XMLHttpRequest to the remote endpoint when the component’s read() method is called. In this process, the object specified by remote.read is passed directly to the jQuery.ajax method. This means that you can specify additional AJAX settings in the remote.read object that jQuery.ajax() will understand. Relying on jQuery for remote requests means Shield UI DataSource provides a well-known, predictable configuration model for remote web service binding.

The only major difference between the format of a remote.data object and a jQuery.ajax() settings objects is that the remote.read.data setting specifies a function that accepts the remote filter, sort, skip and take parameters in a single object and returns another JavaScript object that is passed to data field of settings object provided to jQuery.ajax(). Key-value pairs from this object are then translated by jQuery to query string parameters for GET requests or post data for POST HTTP requests. This mechanism allows mapping data transformation parameters from shield UI DataSource to parameters that the remote web service will understand:

$(function () {
    var ds = new shield.DataSource({
        remote: {
            read: {
                url: "http://services.odata.org/V3/Northwind/Northwind.svc/Products?$format=json",
                data: function(params) {
                    var dataObject = {};
					
                    if (params.skip != null) {
                        dataObject.$skip = skip;							
                    }
					
                    if (params.take != null) {
                        dataObject.$top = take;
                    }
					
                    return dataObject;
                }
            }
        },
        schema: {
            data: "value"
        },
        skip: 5,
        take: 7
    });

    ds.read().then(function () {
        var view = ds.view;
		
        view.forEach(function(item) {
            console.log(item);
        });
    });
});

Events and Callbacks

Every call to the DataSource read() method triggers a set of events that notify the start, completion, success and error states of the component.To register for events, use the events setting during initialization. Additionally, the read() method returns a jQuery Promise object that can be listened to using its then() method:

var ds = new shield.DataSource({
	data: [/*...*/],
	events: {
		start: function (e) {
			//e.params contains filter, sort, skip and take parameters
			//e.preventDefault() cancels the event from further executing
		},
		complete: function (e) {
			//triggered when the read operation completes successfully
		},
		change: function (e) {
			//triggered right after "complete" event
			//e.fromCache is true, if data is read from cache
		},
		error: function (e) {
			//triggered if there is an error during reading
			//e.error contains the error that is thrown
		}
	}
});
ds.read().then(function () {
	//success callback is called before the "complete" and "change" events
}, function() {
	//error callback is called before "error" event
});

The “start” and “complete” events mark the beginning and end of a single read operation (local or remote). The “start” event can be canceled, meaning that calling its event object’s preventDefault() method will abort the read operation and prevent any further events from firing.

The “change” event indicates data has been changed as a result of a successful read operation. The DataSource.view property is set by the time this event is fired and can be used to access the current view of the data after filter, sort, skip and take operations. As the result of filter, sort, skip and take operations are cached, a read() operation may return data from the cache instead of starting over. In this case the “change” event object has its fromCache property set to true to indicate data has been read from the cache.

The “error” event indicates an error occurred during a read operation and any data, if available, must be discarded. The event object’s error property contains the error that has been thrown during execution.

In addition to events, the DataSource.read() method returns a jQuery Promise object that can be listened to. The success callback of the promise is called when data is successfully read, just before the “change” event is fired by the component. The DataSource.view property is set to the current data view at this point.

Equivalently, the error callback of the promise is called just before the “error” event is raised by the component and indicates unsuccessful read operation.

Filtering

Shield UI DataSource provides a robust, fully-featured mechanism for filtering data locally on the client-side. To setup filtering during initialization, set the filter option:

var ds = new shield.DataSource( {
	data: [/*...*/],
	filter: { path: "age", filter: "eq", value: 27 }
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the filtered data
});

To modify filter options after the control has been initialized, set the filter property of the DataSource:

ds.filter = { path: "name", filter: "starts", value: "M" }
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the newly filtered data
});

The above code snippets both set a single filter expression. Each filter expression is a JavaScript object containing three fields:
path - a string of the form “property.subproperty”, specifying the path to the property to filter by.
filter - the name of a predefined filter function to use.
value - the literal value to filter by. In case a custom filter function is specified,this field is omitted.

Following is the set of predefined filter functions:

“eq” - equal (aliases: “equal”, “equals”, “==”)
“neq” - not equal (aliases: “ne”, “doesnotequal”, “notequal”, “notequals”, “!=”)
“con” - contains (aliases: “contains”)
“notcon” - does not contain (aliases: “doesnotcontain”, “notcontains”)
“starts” - string starts with (aliases: “startswith”)
“ends” - string ends with (aliases: “endswith”)
“gt” - greater than (aliases: “greaterthan”, “>”)
“lt” - less than (aliases: “lessthan”, “<”)
“gte” - greater than or equal (aliases: “ge”, “greaterthanorequal”, “>=”)
“lte” - less than or equal (aliases: “le”, “lessthanorequal”, “<=”)
“isnull” - is null (aliases: “null”)
“notnull” - is not null (aliases: “isnotnull”)

If a custom filtering mechanism is required, Shield UI DataSource supports custom filter functions specified in place of filter expression objects:

var ds = new shield.DataSource( {
	data: [/*...*/],
	filter: function (dataItem) {
		//custom filter function
		return dataItem.age > 27 && dataItem.lastName.chartAt(0) === "C";
	}
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the custom filtered data
});

By default, Shield UI DataSource filters data locally on the client-side. However, server-side filtering is also supported. If the DataSource is configured for remote web service binding and the DataSource.transport.operations array contains “filter”, the component sends filter expressions on the server and does not filter the data locally.

Shield UI DataSource supports a combination of multiple filter expressions (a compound filter expression) along with the logical “and” / ”or” operations between them. To provide a compound filter expression, use a JavaScript object with a single field named “and” or “or” to specify the logical operator. The field must contain a JavaScript array with other filter expressions. These can be simple or compound, thus allowing for the logical nesting of multiple filter expressions into a single compound expression:

var ds = new shield.DataSource( {
    data: [/*...*/],
    //a compound filter expression containing a combination of a simple
    //and another compound expression, specifying the selection of
    //all data items with "category" field greater than or equal to 2
    //and all data items with either "age" less than 40 or "retired" equal to false
    filter: {
        and: [
            { path: "category", filter: ">=", value: 2 },
            {
                or: [
                    { path: "age", filter: "<", value: 40 },
                    { path: "retired", filter: "eq", value: false }
                ]
            }
        ]
    }
});
ds.read().then(function () {
    var dataView = ds.view;
    //dataView contains the data with compound filter applied
});

Paging

To specify page parameters, use the skip and take initialization settings when creating a new DataSource instance:

var ds = new shield.DataSource({
	data: [/*...*/],
	skip: 5,
	take: 10
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains paged data
});

To set paging parameters after initialization, use the skip and take properties of the component:

ds.skip = 7;
ds.take = 12;

ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains new page of data
});

By default skip and take operations are applied after the data is read. If binding to a remote endpoint is used and the DataSource.remote.operations array contains “skip” and/or “take”, the respective skip/take parameters are sent to the remote endpoint. In this case, the DataSource is configured for remote paging and does not apply skip and take locally.

Sorting

Shield UI DataSource supports sorting data by single or multiple fields. To specify sorting parameters, use the sort initialization setting when creating a new DataSource instance:

var ds = new shield.DataSource( {
	data: [/*...*/],
	sort: { path: "name", desc: true }
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the sorted data
});

To set sorting parameters after the DataSource is initialized, use the sort property of the component:

ds.sort = { path: "birthDate" }

ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the newly sorted data
});

Shield UI DataSource sorting by multiple fields. To specify multi-field sorting, set the sort initialization setting or DataSource property to an array of objects, each object containing a single sorting setting:

var ds = new shield.DataSource( {
	data: [/*...*/],
	sort: [
		{ path: "name", desc: true },
		{ path: "birthDate" }
	]
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the data sorted by multiple fields
});

By default, Shield UI DataSource sorts the data locally. If using remote data binding and the DataSource.remote.operations array contains “sort”, the component sends sorting parameters to the server and does not explicitly sort the resulting data. Thus, server-side sorting is supported.

The default sorting algorithm in Shield UI DataSource adheres to the following rules:

1. Numeric data is sorted in numeric order.
2. Date objects are converted to numeric data using getTime() and sorted numerically.
3. Strings are sorted in lexicographic order.
4. Null and undefined values are treated equal and are sorted according to their source order.
5. Any other non-null data is converted to string and sorted in a lexicographic order.

For cases when a different sorting algorithm is required, Shield UI DataSource supports custom sort functions. To specify a custom sort function, set the sort initialization setting or DataSource field to a function expression:

var ds = new shield.DataSource( {
	data: [/*...*/],
	sort: function (a, b) {
		//custom sort function
		return a.birthDate - b.birthDate;
	}
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView contains the custom sorted data
});

Schema Definition

Shield UI DataSource provides a series of schema settings that describe how the data is parsed and mapped into suitable JavaScript objects. The schema is a JavaScript object that is specified either through the schema setting during initilization, or using the schema property after the DataSource has been initialized:

var ds = new shield.DataSource( {
	schema: {
		//specifies the path to the data array in the result object after read()
		//can be a function accepting the result object and returning an array
		data: "result.data",
		
		//specifies the path to the total item count in the result object after read()
		//can be a function accepting the result object and returning a number
		count: "result.count",
		
		//specifies field types and maps fields to other field names
		fields: {
			productId: {
				//map data from the "id" field to a new "productId" field
				path: "id", 
				//convert data values to numbers
				type: "number"
			},
			productName: {
				path: "name"
			},
			launched: {
				path: "launchDate",
				type: "date"
			},
			discontinued: {
				type: "bool"
			}
		}
	}
});
ds.read().then(function () {
	var dataView = ds.view;
	//dataView is an array of objects, where each object has a field "productId"
	//containing numeric values, "productName" containing strings, "launched"
	//containing Date objects and "discontinued" containing Boolean values
});