Wednesday, August 1, 2012

Pull-to-Refresh with Sencha Touch NestedList

The NestedList widget in Sencha Touch is perfect for hierarchical navigation (tree structures).  Our solution repository
is just that, a database persistent file system.  Upon building our mobile application, this widget was a natural fit.
The NestedList is backed by a 'Store' - in our case I defined a Files store.  An AJAX call to our repository web service
returns a heap of XML, as much as I wish it to be JSON it's just not happening.  Not until SUGAR.  When SUGAR hits, we
have real REST services where I can specify the format I want it back in.  Until then I have XML and this Store really
seems to want JSON.  I wrote an xml2json converter, shown below.

Ext.define('PExt.json.XMLtoJSON', {

xml2Json : function(xml, isRoot) {

var jsonObj = {};

if (!isRoot) {
if (xml.nodeType == 1) {
jsonObj.file = {};
for (var i = 0; i < xml.attributes.length; i++) {
if (xml.attributes.item(i).nodeName == 'localized-name') {
jsonObj.file['localizedName'] = xml.attributes.item(i).nodeValue;
} else {
jsonObj.file[xml.attributes.item(i).nodeName] = xml.attributes.item(i).nodeValue;
}
}
} else if (xml.nodeType == 3) {
jsonObj = xml.nodeValue;
}
}

if (xml.hasChildNodes()) {
jsonObj.children = [];
for(var i = 0; i < xml.childNodes.length; i++) {
var child = xml.childNodes.item(i);
var visible = child.attributes['visible'].nodeValue;
if (visible === 'false') {
continue;
}
jsonObj.children.push(this.xml2Json(child, false));
}
}
return jsonObj;
}

});

It's not a generic converter, it cares about our repository/attributes it has.  But the beauty of it is that I get real
JSON that the Store can readily consume.  If only it were that easy, there are special attributes that the Store/NestedList
are looking for which we don't have.  Other attributes are provided as opposites, for example, the 'leaf' attribute the
NestedList is looking for is missing, we have an attribute to tell us if something is a folder (isDirectory).  The
solution, which was not at first obvious, was to define converters for each of the missing attributes.  These converters let us
write a function to perform any type of conversion or lookup to return whatever value we need.  For example:

 Ext.define('PentahoMobile.model.File', {
...
config: {
        fields: [
...
{name: 'leaf', type: 'boolean', mapping: 'file.isDirectory',
convert: function(value, record) {
// convert file.isDirectory into 'leaf'
// maybe check if a folder has children
...
}
},
]
}
...
});

After the store, mappings and converter function have been written, we have a NestedList which works against the
solution repository.  You can add markup to the HTML generated for each entry in the list, such as adding folder icons
or descriptions.

This all served its purpose quite well until our UX department asked about removing the refresh button in the toolbar
and doing the 'pull-to-refresh' that is becoming more common in mobile apps these days.  Fortunately, Sencha has such a
plugin, but I could not find any documentation on how to use it with a NestedList, only to the Ext.dataview.List.  After
a lot of reading and mostly trial/error I found a solution.  I had to 'use the source' in order to see how the NestedList
uses List internally, and maybe I could find some hint.

The key is a listConfig hidden within the config of the NestedList itself.  I basically used this config as if it were
literally the config for the internal List used by NestedList.  Thankfully this worked, after a few moments of wrestling
around with some improper refresh calls to my store I discovered that you can define your own refresh function for the
pull-to-refresh.  As a note for completeness, you can also override the text which displays when you pull/release the
control.  Here's the listConfig that I am using:

config: {
...
listConfig: {
plugins: [{
xclass: 'Ext.plugin.PullRefresh',
refreshFn: function(plugin) {
// refresh repository
}
 }]
},
...
}

Here is a screenshot of it in action.


1 comment:

  1. tried this but got error
    Uncaught TypeError: Cannot call method 'getScroller' of undefined

    when using pull to refresh plugin

    ReplyDelete