Chapter 12. It's All about the Data

Ext JS is an extremely powerful, cross-browser library, providing any developer with a beautiful, consistent set of tools for laying out browser-based applications. But there's a lot more here than just pretty boxes and grids. As the title says, it's all about the data! An application without data is really nothing more than an interactive static page, and our users are going to want to manipulate real information. One of the wonders of web applications is that they've taken computing full circle, back to the days of the client/server application model. Ext JS's AJAXified objects provide us with the means to work with real-time data straight from the server, and in this chapter we'll cover the many different ways you can retrieve from and post data to your Ext JS based applications.

Understanding data formats

We have dozens of component objects available to us within Ext JS, and most of them can take dynamic data from the server. It's all in knowing what kind of data a component can take, and what formats are acceptable.

Basic remote panel data

You've probably noticed that many components are basically just a box. Tab Panels, Accordion Panels, and the content area of a window are all just large boxes (a<div> element), or panels. Each of these unique objects has its own methods and properties, yet each of them extends the Ext.Panel object.

Applying dynamic data to a basic panel is super simple, because it takes the simplest of formats: straight text or HTML. Our first example will load a simple HTML page into a panel. First, we'll need the rendering page:

Example 1:ch12ex1.html

… <div id="mainContent">
<div id="chap12_ex01"></div>
</div>
…

Here, we have shown what will go inside the<body> tag of our example HTML page. Next, we'll need a server-side template to call for content:

Example 1:chapter_12example1ajax.html

<b>William Sheakespeare:</b> <i>Poet Lauraette</i><br />

The last thing we'll need is the actual script to create our example Panel:

Example 1:scriptschapter12_01.js

Ext.onReady(function(){
var example1 = new Ext.Panel({
applyTo:'chap12_ex01',
title:'Chapter 12: Example 1',
width:250,
height:250,
frame:true,
autoLoad:{
url:'example1ajax.html'
}
});
});

Calling the ch12ex1.html template in the browser will run this basic script. Let's look over the code to see what we're doing:

  1. We wait until the DOM is rendered (Ext.onReady() function).

  2. We create new Ext.Panel object, rendered to the chap12_ex01 element (a div on the rendered page).

  3. We load in the contents of the external URL example1ajax.html.

It is very simple to pull in an HTML template as content for a panel item. But we would really like dynamic data. Let's build on this example, and pull in data from an application server. We can use the same autoLoad attribute to call an application server processing page to return data.

Note

Author's note:

Ext JS is a client-side scripting library and, as such, you can use any server-side programming language that you feel comfortable with. You may have noticed that some earlier examples in this book are coded in PHP. Examples within this chapter require the Adobe ColdFusion server to process the dynamic data, and require you to download and install the free Developer's Edition to run the examples. We're using ColdFusion, at this point, to illustrate two different points:

  1. That any server-side processor can feed data to Ext JS.

  2. That not every application server, or remote application, will return data in a standard format. Our examples of Custom Data Readers, later in this chapterwill iterate this point further.

    The free Adobe ColdFusion Developer's Edition server is available at http://www.adobe.com. Review the sample README.txt file for more detailed instructions on accessing the example files for this chapter.

For Example 2, we'll change our<div> id to chap12_ex02, and use the chapter12_02.js file to load the data using a different approach, which now reads as follows:

Example 2:scriptschapter12_02.js

var example2 = new Ext.Panel({
applyTo:'chap12_ex02',
title:'Chapter 12: Example 2',
width:250,
height:250,
frame:true,
autoLoad:{
url:'Chapter12Example.cfc',
params:{
method:'example1',
returnFormat:'plain',
id:1289
}
}
});

You'll notice that the URL is now calling an Adobe ColdFusion Component(CFC), passing in some parameters to get its results. We have a very basic CFC, which runs a small query based on the passed parameters to generate the content that is passed back through the AJAX call.

Example 2:chapter_12Chapter12Example.cfc

<cfcomponent output="false">
<cffunction name="example2" access="remote" output="false" returntype="string">
<cfargument name="id" type="numeric" required="true" />
<cfset var output = "" />
<cfset var q = "" />
<cftry>
<cfquery name="q" datasource="chapter12">
SELECT firstName,
lastName,
occupation
FROM People
WHERE ID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ARGUMENTS.id#" />
</cfquery>
<cfcatch type="database">
<!--- Place Error Handling Here --->
</cfcatch>
</cftry>
<cfif IsDefined("q.recordcount") and q.recordcount>
<cfsavecontent variable="output"><cfoutput>
#q.firstName# #q.lastName#: #q.occupation#<br />
</cfoutput></cfsavecontent>
</cfif>
<cfreturn output />
</cffunction>
</cfcomponent>

As the purpose of this book isn't to teach you a server-side language, let's break this down in the simplest way. The CFC takes an argument of ID, which is passed into a query of the People table. If a record is returned from the query, a basic string is returned in a predetermined format: FirstName LastName: Occupation. The AJAX call (autoLoad) is passing several parameters: the method to run within the CFC, the format type to return (plain text, in this case), and the method arguments (in this case id).

Gotchas with HTML data

You must remember that you should call data only via AJAX from the domain of your site. Attempting to reference data from a site outside of your domain will error out at the browser level, as it's considered to be cross-site scripting. Cross-site scripting is a means of delivering malicious scripts to an unsuspecting user, generally from outside of the site which they're visiting. Most modern browsers now have built-in facilities to prevent this type of attack. Ext JS does provide facilities for bypassing this restriction, via the Ext.data.ScriptTagProxy, but this should only be used if you are confident of the security of the data you are requesting, and its effect on your application.

Other formats

Ext JS has the capability to consume external data in a variety of formats:

Format

Example

Plain Text

Eric Clapton is a consummate guitarist

HTML

<b>Jimi Hendrix is considered, by some, to have been one of the finest blues guitarists that ever lived</b>

JSON

var theBeatles = {

'members': 4,

'band': [

{'id':1,'first_name':'John', 'last_name':'Lennon'},

{'id':2,'first_name':'Paul', 'last_name':'McCartney'},

{'id',3,'first_name':'George', 'last_name':'Harrison'},

{'id':4,'first_name':'Ringo', 'last_name':'Starr'}

]

};

XML

<band>

<members>4</members>

<member>

<firstname>Jimmy</firstname>

<lastname>Paige</lastname>

</member>

<member>

<firstname>Robert</firstname>

<lastname>Plant</lastname>

</member>

...

JavaScript Array

var PinkFloyd = [

['1','David','Gilmour'],

['2','Roger','Waters'],

['3','Richard','Wright'],

['4','Nick','Mason']

]

The format that you choose may be decided according to the Ext JS object you are using. Many developers like to use XML, as many databases (MS SQL, Oracle, MySQL, PostgreSQL, and DB2 to name a few)can return it natively, and many RESTful web services use it. Although this is a good thing, especially when working with varying external applications, XML can be very verbose at times. Data calls that return small sets of data can quickly clog up the bit stream because of the verbosity of the XML syntax. Another consideration with XML, is the browser engine. An XML data set that looks fine to Mozilla Firefox may be rejected by Internet Explorer, and Internet Explorer's XML parsing engine is slow, as well. JSON, or JavaScript Object Notation, data packages tend to be much smaller, taking up less bandwidth. If the object you're using can accept it, a simple array can be even smaller, although you will lose the descriptive nature that the JSON or XML syntaxes provide.

The data store object

Most Ext JS objects (and even panels, with some additional work) take data as Records, or Nodes. Records are typically stored within a data store object. Think of a Store as being similar to a spreadsheet, and each Record as being a row within the spreadsheet.

The data package contains many objects for interacting with data. You have several different Store types:

  • JsonStore: Store object specifically for working with JSON data

  • SimpleStore: Store object for working with arrays and XML data

  • GroupingStore: Store object that holds 'grouped' datasets

Any Store will require some kind of Reader object to parse the inbound data, and again the data package has several:

  • ArrayReader: For working with JavaScript arrays

  • JsonReader: For working with JSON datasets

  • XmlReader: For working with XML datasets

    Note

    The TreePanel object doesn't use a traditional data Store, but has its own specialized store called a TreeLoader, which is passed into the configuration through the loader config option. The TreeLoader accepts simple arrays of definition objects, much like those expected by an ArrayReader. See the Ext JS API (http://extjs.com/deploy/dev/docs/) for more information.

Defining data

Store objects are easily configured, requiring the source of the data and a description of the expected records. Our applications know to expect data, but we have to tell them what the data is supposed to look like. Let's say, for instance, that our application manages media assets for our site. We would have a server-side object that queries a folder in our file system and returns information on the files that the folder contains. The data returned might look something like this:

{
files: [
{name: 'beatles.jpg', path:'/images/', size:46.5, lastmod: '12/21/2001'},
{name: 'led_zepplin.jpg', path:'/images/', size:43.2, lastmod: '2001-12-21 00:00:00'},
{name: 'the_doors.jpg', path: '/images/', size:24.6, lastmod: '2001-12-21 00:00:00'},
{name: 'jimi_hendrix.jpg', path: '/images/', size:64.3, lastmod: '2001-12-21 00:00:00'}
]
}

This is a small JSON dataset. Its root is files followed by an array of objects. Now we have to define this data for our application:

var recordObj = new Ext.data.Record.create([{
name: 'Name',
mapping: 'name'
},{
name: 'FilePath',
mapping: 'path'
},{
name: 'FileSize',
mapping: 'size',
type: 'float'
},{
name: 'LastModified',
mapping: 'lastmod',
type: 'date',
dateFormat: 'm/d/Y'
}]);

We've applied a name that each field will be referenced by within our application, mapping it to a variable within a dataset object. Many variables will automatically be typed, but you can force (cast) the 'type' of a variable for greater definition and easier manipulation.

The various variable types are:

  • auto (the default, which implies no conversion)

  • string

  • int

  • float

  • boolean

  • date

We've also applied special string formatting to our date object, to have our output the way we want.

Note

Dates are typically passed as strings, which usually have to be cast into Date objects for proper manipulation. By specifying a date type, Ext JS will handle the conversion using the dateFormat we define. The Ext JS documentation for the Date object provides an extensive format list, which we can use to define the hundreds of Date string permutations we may come across in our code.

More on mapping our data

In the previous example, we covered the definition of a simple JSON object. The same technique is used for XML objects, with the only difference being how to map a Record field to a specific node. That's where the mapping config option comes in. This option can take a DOM path to the node within the XML. Take the following example:

<band>
<members>4</members>
<member>
<firstname>Jimmy</firstname>
<lastname>Paige</lastname>
</member>
<member>
<firstname>Robert</firstname>
<lastname>Plant</lastname>
</member>
...

To create a mapping of the first_name node you would have the config look like this:

mapping:'member > first_name'

JavaScript arrays are easier, as they don't require mapping, other than defining each field in the same order it would be seen in the array:

var PinkFloyd = [
['1','David','Gilmour'],
['2','Roger','Waters'],
['3','Richard','Wright'],
['4','Nick','Mason']
]

Note here that we won't create() a Record object, but just use the fields config option of the Store.

fields:[
{name:'id'},
{name:'first_name'},
{name:'last_name'}
]

Pulling data into the store

It is almost as easy to retrieve data from the server as it was to populate that Panel object earlier, and we can do so using a very similar syntax.

Example 3:scriptschapter12_03.js

var recordObj = new Ext.data.Record.create([{
name: 'NAME',
mapping: 'name'
},{
name: 'DIRECTORY',
mapping: 'path'
},{
name: 'SIZE',
mapping: 'size',
type: 'float'
},{
name: 'DATELASTMODIFIED',
mapping: 'lastmod',
type: 'date',
dateFormat: 'm/d/Y'
}]);
var ourStore = new Ext.data.JsonStore({
url:'Chapter12Example.cfc',
baseParams:{
method: 'getFileInfo',
returnFormat:'JSON',
startPath: '/images/'
},
root:'files',
id:'name',
fields: recordObj,
listeners:{
beforeload:{
fn: function(store, options){
if (options.startPath && (options.startPath.length > 0)){
store.baseParams.startPath = fieldVal;
}
},
scope: this
}
}
});
ourStore.load();

This ties all of our pieces together to create our data Store object. First, we use the url config option to define the location from where we will get our data. Then, we set the initial set of parameters to pass on the load request. Finally, it may be that we want to conditionally pass different parameter values for each request. For this, we can define a special 'listener' to pass new information. In this case, whenever the load() method is called, if the startPath property is passed into the method as an argument, before the Store retrieves the data it will change the startPath base parameter to match the value passed in.

Note

Where's the screenshot?

Ok, that's a valid question. A data Store, by itself, has no visible output in the browser display. This is why tools like Firefox with the Firebug plug-in, or the Aptana development IDE can be so important when doing JavaScript development. These tools can allow you to direct processing output to special windows, to monitor what's going on within your application.

Using a DataReader to map data

Some applications can natively return data in XML, or even JSON, but it might not always be in the format Ext JS is expecting. As an example, the JsonStore, with its built-in JsonReader, expects an incoming dataset in the following format:

{
'rootName': [
{
'variableName1': 'First record',
'variableName2': '0'
},{
'variableName1': 'Second record',
'variableName2': '3.5'
}
]
}

This (JSON) object has a rootName property, which is the name of the root of the dataset, containing an array of objects. Each of these objects has the same attributes. The attribute names are in quotes. Values are typically in quotes, with the exception of numbers which may or may not be in quotes.

So, if a server-side call returns data in this expected format, then the base JsonReader included within the JsonStore will automatically parse the received datasets to populate the Store object. But what happens if the server-side application uses a slightly different format? As an example, the Adobe ColdFusion 8 server can automatically return JSON datasets for remote requests, translating any of ColdFusion's native data types into JSON data. But ColdFusion's JSON formatting is typically different, especially when dealing with query data (which is what a dataset would usually be created from). Here's an example of a JSON dataset being returned from a ColdFusion query object:

{
"COLUMNS":["NAME","SIZE","TYPE","DATELASTMODIFIED","ATTRIBUTES", "MODE","DIRECTORY"],
"DATA":[
["IMG1.jpg",582360,"File","June, 13 2003 23:50:08","","","H:\wwwroot\ExtBook\images"],
["IMG2.JPG",1108490,"File","June, 13 2003 23:50:52","","","H:\wwwroot\ExtBook\images"],
["IMG3.JPG",1136108,"File","June, 13 2003 23:51:02","","","H:\wwwroot\ExtBook\images"],
["IMG4.JPG",1538506,"File","June, 13 2003 23:51:12","","","H:\wwwroot\ExtBook\images"]
]
}

All of the data we need is here, but the format can't be properly parsed by the base JsonReader. So what do we do now?

Using a custom DataReader

Ext JS's built-in classes provide outstanding ease of use 'out-of-the-box', but (as we can see) sometimes we need something a little special, possibly due to a different implementation for a specific application server (such as ColdFusion), or possibly due to an external application API (Flickr for example). We could probably implement something on the server-side to put our data in the necessary format, but this creates unnecessary overheads on our server-side platform. Why not just use the client to handle these minor transformations? This helps distribute the load in our applications, and makes more effective use of all of the resources that we have on hand.

Ext JS provides us with the facilities for creating custom DataReaders for mapping our data, as well as simple means (the reader config option) for defining these readers in our store.

In our current exercise, we're lucky in that we don't have to write our own DataReader. Because the Adobe ColdFusion server platform is so widely used, the Ext JS community has already produced a custom reader just for this task. A simple search of the Ext JS Forums (http://extjs.com/forum/) will help you find many custom readers for data in a variety of formats. Just take the time to verify (read) the code prior to use, because it is is being provided by a third party. By using the CFJsonReader, with a few minor modifications to our script we can easily read the JSON data format being returned by ColdFusion.

First, let's convert our Record object into a simple array, which will become a model of our records. Include a script tag in your calling template to include the CFJsonReader.js file, prior to the script tag for your custom script. Then we'll make the following adjustments to our custom script:

var recordModel = [
{name:'file_name',mapping:'NAME'},
{name:'file_size',mapping:'SIZE'},
{name:'type',mapping:'TYPE'},
{name:'lastmod',mapping:'DATELASTMODIFIED'},
{name:'file_attributes',mapping:'ATTRIBUTES'},
{name:'mode',mapping:'MODE'},
{name:'directory',mapping:'DIRECTORY'}
];

Now, we'll define our new DataReader as a CFJsonStore object:

var ourReader = new Ext.data.CFJsonReader(recordModel, {id: 'NAME', root: 'DATA'});

Next, we'll change our data Store from being a JsonStore to being the base Store object type, and apply our reader config option to ourReader:

var ourStore = new Ext.data.Store({
...
reader: ourReader

We also remove the fields, id, and root properties from the store definition, as these are now all handled from within the reader's definition.

The last thing we'll do is apply another custom listener to our script, so that we can verify whether the dataset is properly loaded. Let's modify our listeners config option, so that we can attach a function to the Store's load event listener:

listeners:{
beforeload:{
fn: function(store, options){
if (options.startPath && (options.startPath.length > 0)){
store.baseParams.startPath = fieldVal;
}
},
scope:this
},
load:{
fn: function(store, records, options){
console.log(records);
},
scope: this
}
}

If you're using Internet Explorer for development, then this line of code will break, as the console.log() method isn't natively supported in that environment (you can include additional scripts to use the console.log() method in IE, like the one found at (url: http://www.moxleystratton.com/article/ie-console) ). Firefox, with the Firebug plugin, however will now give you some output once the data has been retrieved, parsed, and loaded into the data Store, so that we can see that the data is now in our Store.

Note

A word about events

Many Ext JS objects have events that are fired when certain actions are taken upon them, or when they reach a certain state. An event-driven application, unlike a procedural programming model, can listen for changes in the application, such as the receipt of data or the change of a Record's value. Ext JS provides an extensive API, giving us the ability to apply custom event listeners to key actions within the application. For more information, review the object' information within the Ext JS API: http://extjs.com/deploy/dev/docs/.

Our final script might now look like this:

var recordModel = [
{name:'file_name',mapping:'NAME'},
{name:'file_size',mapping:'SIZE'},
{name:'type',mapping:'TYPE'},
{name:'lastmod',mapping:'DATELASTMODIFIED'},
{name:'file_attributes',mapping:'ATTRIBUTES'},
{name:'mode',mapping:'MODE'},
{name:'directory',mapping:'DIRECTORY'}
];
var ourReader = new Ext.data.CFJsonReader(recordModel,{id:'NAME',root:'DATA'});
var ourStore = new Ext.data.Store({
url:'Chapter12Example.cfc',
baseParams:{
method: 'getFileInfoByPath',
returnFormat: 'JSON',
queryFormat: 'column',
startPath: '/images/'
},
reader: ourReader,
listeners:{
beforeload:{
fn: function(store, options){
if (options.startPath && (options.startPath.length > 0)){
store.baseParams.startPath = options.startPath;
}
},
scope:this
},
load: {
fn: function(store,records,options){
console.log(records);
}
},
scope:this
}
});
ourStore.load();

This now wraps up our code pieces, defining what our data will look like, configuring our custom reader, and setting up our data Store to pull in our JSON data. The load listener will display the Records retrieved from the server that are now in our Store.

Getting what you want: Finding data

Now that we have data, we'll typically need to manipulate it. Once we've loaded our data Store with Records, the entire dataset remains resident in the browser cache, ready for manipulation or replacement. This data is persistent until we move away from the page or destroy the dataset or the data Store.

Ext JS provides many different options for dealing with our data, all of which are documented within the API. Here, we'll explore some of the most common things to do.

Finding data by field value

The first thing we might want to do is find a specific Record. Let's say we needed to know which Record from our previous examples contains the picture of Jimi Hendrix:

var jimiPicIndex = ourStore.find('NAME','Jimi',0,false,false);

This method would return the index of the first record that had Jimi as part of the value of the NAME field. It would start its search from the first record(0), as it is in JavaScript array notation), look for the string from the beginning of the field's value (using true here will search for the string in any location of the NAME field within any record), and perform a case-insensitive search.

Finding data by record index

Having the index is nice; at least we know which Record it is now. But we also need to retrieve the Record:

var ourImg = ourStore.getAt(jimiPicIndex);

The getAt() method of the Store object will get a Record at a specific index position within the Store.

Finding data by record ID

The best way to look for a unique record is by ID. If you already know the ID of your record, this process just becomes easier. We used the NAME field as our ID, so let's find the record for that same record:

var ourImg = ourStore.getById('jimi_hendrix.jpg'),

So, now we can find a Record by partial value within a field, get a Record by its specific index, or retrieve a Record by its ID value.

Getting what you want: Filtering data

Sometimes, you only need a specific subset of data from your Store. Your Store contains a complete dataset (for caching and easy retrieval), but you need it filtered to a specific set of Records. As an example, the cfdirectory tag we used in our ColdFusion server-side call can return an entire directory listing, including all subdirectories. After retrieving the data, it may be that we only need the names of the files within the startPath that we posted. For this, we can filter our client-side cached dataset to get only the Records of type, File:

ourStore.filter('TYPE','File',false,false);

This filters our dataset using the TYPE field. The dataset will now only contain Records that have a TYPE field, with a value of File (matched from the beginning, and case-insensitive).

After working with our filtered dataset, there will come a time when we want our original dataset back. When we filtered the dataset, the other Records didn't go away. They're still sitting in cache, to the side, waiting to be recalled. Rather than query the server again, we can simply clear our filter:

ourStore.clearFilter();

Remote filtering: The why and the how

Client-side filtering is great, reducing our trips to the server. Sometimes, however, our record set is just too large to pull in at once. A great example of this is a paging grid. Many times we'll only be pulling in 25 Records at a time. The client-side filtering methods are fine if we only want to filter the resident dataset, but most of the time we'll want a filter applied to all of our data.

Sorting data on remote calls is pretty easy, as we can set the Store's remoteSort property to true. So, if our Store was attached to a grid object, clicking on a column heading to sort the display would automatically pass the value in its AJAX request.

Filtering data on remote requests is a bit harder. Basically, we would pass parameters through the Store's load event, and act on those arguments in our server-side method.

So, the first thing we'll need is some server-side code for handling our filtering and sorting. We'll return to our ColdFusion component to add a new method:

Example 3:Chapter_12Chapter12Example.cfc

<!---
/ METHOD: getDirectoryContents
/
/ @param startPath:string
/ @param recurse:boolean (optional)
/ @param fileFilter:string (optional)
/ @param dirFilter:string (optional - File|Dir)
/ @param sortField:string (optional - NAME|SIZE|TYPE|DATELASTMODIFIED|ATTRIBUTES|MODE|DIRECTORY)
/ @param sortDirection:string (option - ASC|DESC [defaults to ASC])
/ @return retQ:query
--->
<cffunction name="getDirectoryContents" access="remote" output="false" returntype="query">
<cfargument name="startPath" required="true" type="string" />
<cfargument name="recurse" required="false" type="boolean" default="false" />
<cfargument name="sortDirection" required="false" type="string" default="ASC" />
<!--- Set some function local variables --->
<cfset var q = "" />
<cfset var retQ = "" />
<cfset var attrArgs = {} />
<cfset var ourDir = ExpandPath(ARGUMENTS.startPath) />
<!--- Create some lists of valid arguments --->
<cfset var filterList = "File,Dir" />
<cfset var sortDirList = "ASC,DESC" />
<cfset var columnList = "NAME,SIZE,TYPE,DATELASTMODIFIED,ATTRIBUTES,MODE,DIRECTORY" />
<cftry>
<cfset attrArgs.recurse = ARGUMENTS.recurse />
<!--- Verify the directory exists before continuing --->
<cfif DirectoryExists(ourDir)>
<cfset attrArgs.directory = ourDir />
<cfelse>
<cfthrow type="Custom" errorcode="Our_Custom_Error" message="The directory you are trying to reach does not exist." />
</cfif>
<!--- Conditionally apply some optional filtering and sorting --->
<cfif IsDefined("ARGUMENTS.fileFilter")>
<cfset attrArgs.filter = ARGUMENTS.fileFilter />
</cfif>
<cfif IsDefined("ARGUMENTS.sortField")>
<cfif ListFindNoCase(columnList,ARGUMENTS.sortField)>
<cfset attrArgs.sort = ARGUMENTS.sortField & " " & ARGUMENTS.sortDirection />
<cfelse>
<cfthrow type="custom" errorcode="Our_Custom_Error" message="You have chosen an invalid sort field. Please use one of the following: " & columnList />
</cfif>
</cfif>
<cfdirectory action="list" name="q" attributeCollection="#attrArgs#" />
<!--- If there are files and/or folders, and you want to sort by TYPE --->
<cfif q.recordcount and IsDefined("ARGUMENTS.dirFilter")>
<cfif ListFindNoCase(filterList,ARGUMENTS.dirFilter)>
<cfquery name="retQ" dbtype="query">
SELECT #columnList#
FROM q
WHERE TYPE = <cfqueryparam cfsqltype=" cf_sql_varchar" value="#ARGUMENTS.dirFilter#" maxlength="4" />
</cfquery>
<cfelse>
<cfthrow type="Custom" errorcode="Our_Custom_Error" message="You have passed an invalid dirFilter. The only accepted values are File and Dir." />
</cfif>
<cfelse>
<cfset retQ = q />
</cfif>
<cfcatch type="any">
<!--- Place Error Handler Here --->
</cfcatch>
</cftry>
<cfreturn retQ />
</cffunction>

This might look complicated, but it really isn't! Again, our mission here isn't to learn ColdFusion, but it is important to have some understanding of what your server-side process is doing. What we have here is a method that takes some optional parameters related to sorting and filtering via an HTTP POST, statement. We use the same cfdirectory tag to query the file system for a list of files and folders. The difference here is that we now conditionally apply some additional attributes to the tag, so that we can filter on a specific file extension, or sort by a particular column of the query. We also have a Query-of-Query statement to query our returned recordset if we want to filter further by the record TYPE, which is a filtering mechanism not built into the cfdirectory tag. Lastly, there's also some custom error handling to ensure that valid arguments are being passed into the method.

We'll make a few modifications to our previous Store script as well. First, we'll need a few methods that can be called to remotely filter our recordset:

filterStoreByType = function (type){
ourStore.load({dirFilter:type});
}
filterStoreByFileType = function (fileType){
ourStore.load({fileFilter:fileType});
}
clearFilters = function (){
ourStore.baseParams = new cloneConfig(initialBaseParams);
ourStore.load();
}

We have methods here for filtering our Store by TYPE, for filtering by file extension, and for clearing the filters. The values passed into these methods are mapped to the proper remote method argument names. Our Store's beforeload listener automatically applies these arguments to the baseParams prior to making the AJAX call back to the server. The important thing to remember here is that each added parameter stays in baseParams, until the filters are cleared.

It's also important to note that the load() method can take four arguments: params, callback (a method to perform on load), scope (the scope with which to call the callback), and add (to add the load to the already-existing dataset). In the format used above, the object used as an argument for the load() method is assumed to be the params argument, because no others are passed. If you are using all of the arguments (or at least the first three), they would have to be within an object.

ourStore.load({{dirFilter:type},someMethodToCall,this,false});

The clearFilter() method does not work with remote filtering, so we need to have a way to recall our initial baseParams when we need to clear our filters, and get our original dataset back. For this, we first abstract our baseParams configuration:

var initialBaseParams = {
method: 'getDirectoryContents',
returnFormat: 'JSON',
queryFormat: 'column',
startPath: '/testdocs/'
};

We then need a way to clone the config in our actual Store configuration. If we passed the initialBaseParams into the baseParams config option directly, and then filtered our dataset, the filter would be added to the initialBaseParams variable, as the variable gets passed by reference. Because we want to be able to recall our actual beginning baseParams, we'll need to clone the initialBaseParams object. The clone gets set as the baseParams config option. Filters don't touch our original object, and we can recall them whenever we need to clearFilter().

For this, we'll need a simple method of cloning a JavaScript object:

cloneConfig = function (config) {
for (i in config) {
if (typeof config[i] == 'object') {
this[i] = new cloneConfig(config[i]);
}
else
this[i] = config[i];
}
}

We can then change our baseParams attribute in our Store configuration:

baseParams: new cloneConfig(initialBaseParams),

We used the same function within our clearFilters() method, to reset our baseParams to their initial configuration. Here is what our entire script looks like now:

Example 4:chapter12_04.js

cloneConfig = function (config) {
for (i in config) {
if (typeof config[i] == 'object') {
this[i] = new cloneConfig(config[i]);
}
else
this[i] = config[i];
}
}
Ext.onReady(function(){
var recordModel = [
{name:'file_name',mapping:'NAME'},
{name:'file_size',mapping:'SIZE'},
{name:'type',mapping:'TYPE'},
{name:'lastmod',mapping:'DATELASTMODIFIED'},
{name:'file_attributes',mapping:'ATTRIBUTES'},
{name:'mode',mapping:'MODE'},
{name:'directory',mapping:'DIRECTORY'}
];
var ourReader = new Ext.data.CFJsonReader({id:'NAME',root:'DATA'}, recordModel);
var initialBaseParams = {
method: 'getDirectoryContents',
returnFormat: 'JSON',
queryFormat: 'column',
startPath: '/testdocs/'
};
var ourStore = new Ext.data.Store({
url:'Chapter12Example.cfc',
baseParams: new cloneConfig(initialBaseParams),
reader: ourReader,
fields: recordModel,
listeners:{
beforeload:{
fn: function(store, options){
for(var i in options){
if(options[i].length > 0){
store.baseParams[i] = options[i];
}
}
},
scope:this
},
load: {
fn: function(store,records,options){
console.log(records);
}
},
scope:this
}
});
ourStore.load({recurse:true});
filterStoreByType = function (type){
ourStore.load({dirFilter:type});
}
filterStoreByFileType = function (fileType){
ourStore.load({fileFilter:fileType});
}
clearFilters = function (){
ourStore.baseParams = new cloneConfig(initialBaseParams);
ourStore.load();
}
});

To test our changes, we can put some links on our HTML page to call the methods that we've created for filtering data, which we can then monitor in Firebug.

Example 4:ch12ex4.html

<div id="chap12_ex04">
<a onclick="filterStoreByType('File')" href="javascript:void(0)">Filter by 'File's</a><br />
<a onclick="filterStoreByFileType('*.doc')" href="javascript:void(0)">Filter by '.doc File's</a><br />
<a onclick="clearFilters()" href="javascript:void(0)"> Clear Filters</a><br />
</div>

After the page has loaded, we see the console logging the initial recordset being loaded into the Store. Clicking our first link will remove all of the directory Records through filtering. Clicking our second link takes it a step further, and filters out any files other than those ending in .doc. The last link resets the filters to the original baseParams and reloads the initial recordset.

Dealing with Recordset changes

One of great things about Ext JS data Stores is change management. Our applications might attack changing Records in a variety of ways, from editable data Grids to simple Forms, but making changes really only means something when we act on it. We might only change our display, but we are more than likely to send changes back to the server.

One of the easiest things to do is to apply an update event listener to our Store object. We've applied two other listeners in the past: the beforeload and load listeners. Now, let's apply an update listener to our script.

listeners:{
beforeload:{
fn: function(store, options){
for(var i in options){
if(options[i].length > 0){
store.baseParams[i] = options[i];
}
}
},
scope:this
},
load: {
fn: function(store, records, options){
console.log(records);
},
scope: this
},
update: {
fn: function(store, record, operation){
switch (operation){
case Ext.record.EDIT:
// Do something with the edited record
break;
case Ext.record.REJECT:
// Do something with the rejected record
break;
case Ext.record.COMMIT:
// Do something with the committed record
break;
}
},
scope:this
}
}

When a Record is updated, the update event fires in the Store, passing several objects into the event. The first is the Store itself, which is good for reference. The second is the Record that's been updated. The last object is the state of the Record that was updated. Here, we have laid out a quick switch/case statement to trigger different actions according to the state of the Record. We can add code into the action block for the Ext.record.EDIT state to automatically send every edit to the server for immediate Record revision.

One other option that we can address is the Ext.record.COMMIT state. It is sometimes better to let the user affect many different changes, to many different records, and then send all the updates at once.

ourStore.commitChanges();

This will collect all of the edited Records, flag them by using Ext.record.COMMIT, and then the update event would fire and affect each Record. Our last operation state is perfect for processing this situation, for which we can add additional client-side validation, or AJAX validation, or whatever our process might call for.

The Ext.record.REJECT state is generally set directly by the data Store, whereby the Store rejects any changes made to the Record, and will revert the Record's field values back to their original (or last committed) state. This might occur if a value of the wrong data type is passed into a field.

Many objects take a Store

The beauty of the Store object is in its many uses. So many objects, within the Ext JS library, can consume a Store as part of their configuration, automatically mapping data in many cases.

Store in a ComboBox

For example, the ComboBox object can take a Store, or any of its subclasses, as a data provider for its values:

var combo = new Ext.form.ComboBox({
store: states,
displayField: 'state',
valueField: 'abbreviation',
typeAhead: true,
mode: 'remote',
triggerAction: 'all',
emptyText: 'Select a state...',
selectOnFocus: true,
applyTo: 'stateCombo'
});

This ComboBox takes a Store object called states, and maps its state field to the display, while mapping the abbreviation field to its underlying selected value.

Store in a DataView

The DataView object is one of the most powerful objects within Ext. This object can take a Store object, let us apply a Template or XTemplate to each Record (which the DataView refers to as a Node), and have each item render within the DataView, contiguously, wrapping items as they run out of space. The DataView opens up some very interesting ways to visually produce contact lists, image galleries, and file system explorers, and opens up our applications to be able to exchange data among a variety of objects through custom drag-and-drop functionality.

Store in a DataView

Stores in Grids

We've seen examples of applying a data Store to a Grid in a previous chapter (Chapter 5) of this book. There are several different types of Grids (Editor, Grouping, Property, and Basic Grids), but all of them take Store objects as input, and the applicable ColumnModel, to coordinate their data display.

The Grid objects and the ComboBox are probably the most prevalent uses of Store objects, with the Grid and the DataView being the two primary means of displaying multiple Records of data. The Tree object takes a special data source called a TreeLoader. The TreeLoader is actually part of the Tree package of classes, and does not extend the base Store object, although it operates in much the same way. Rather than Record objects, the TreeLoader takes an array of objects, which it then converts into Nodes. The structure to its incoming data is something like this:

var dataset = [{
id: 1,
text: 'Node 1',
leaf:: false
},{
id: 2,
text: 'Node 2',
leaf: true
}];

When a leaf is true, then it is an expandable item, which will query the server for further data when passing the node data. A leaf:false statement says that the Node has no children.

Summary

In this chapter, we've learned how to pull dynamic, server-side data into our applications. Ext JS's Store objects, with their versatility and mappable syntax, are easily-configured datasources for a lot of Ext JS objects. In this chapter, we've bound simple external data to a Panel object, gone over the various data formats that Ext JS can consume, and seen a basic overview of the data Store object and some of its more important subclasses.

Getting into the meat of things, we learned how to define our data using the Record object, after which we learned how to populate our Store with Records from a remote data source. We also learned about the purpose behind DataReaders, the different ones available to you, and how to configure a custom DataReader.

Pulling it all together, we got busy learning Store manipulation techniques such as finding Records by field values, indexes, or IDs. We also touched on filtering our Stores to get a working subset of data Records. We also talked about dealing with local data changes via the update event listener.

Finally, we covered some of the other Ext JS objects that use the Store, opening the doors to external data within multiple facets of our applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.58.199