Sencha Touch MVC Application – Part 2

In my previous post, I shared presentation on Sencha Touch MVC. That’s all the “talk”, it is time to see it in action. This short tutorial will walk through the implementation of Sencha Touch MVC application. I revamped my old (and long forgotten & messy) USGS apps as example. Before I dive into the code, I will share how to structure the apps.

Structuring Your Apps

Firstly, create the folder structure to support the MVC application. The figure below shows the basic structure required and the codes are available here. I personally think this is a good practice and can be use as coding standard and guideline for Sencha Touch apps. In addition to that, you may want to standardize the file & object naming convention for the controllers, views and data models.

  • m.usgs – the root folder for the application
    • app – folder for your sencha touch code, containing app.js and folders for controllers, models and views.
    • lib – folder for sencha library and other plug-in. (in my example, I moved the sencha library out for sharing with other apps)
    • resources – folder for web resources such as css & images.
    • index.html – the main page to launch the apps.
Sencha Touch - Structuring Your Apps for MVC
Sencha Touch - Structuring Your Apps for MVC

Coding

Step 1: Instantiating the Application

Create app/app.js and add the basic launcher for an Ext.Application instance. In the example it will be namespaced as “usgs”.

Ext.regApplication({
    name: "usgs",
    launch: function() {
	this.views.viewport = new this.views.Viewport();
    }
});

Create a main Viewport view which will house the pages required for displaying USGS Earthquake data. Here we place references to the card instances in the “usgs.views” namespace, so that we can refer to them explicitly in our page flow.

usgs.views.Viewport = Ext.extend(Ext.Panel, {
    fullscreen: true,
    layout: 'card',
    cardSwitchAnimation: 'slide',
    initComponent: function() {
        //put instances of cards into app.views namespace
        Ext.apply(usgs.views, {
        	usgsMenu: new usgs.views.UsgsMenu()
        	,usgsList: new usgs.views.UsgsList()
        	,usgsMap: new usgs.views.UsgsMap()
        });
        //put instances of cards into viewport
        Ext.apply(this, {
            items: [
                usgs.views.usgsMenu
                ,usgs.views.usgsList
                ,usgs.views.usgsMap
            ]
        });
        usgs.views.Viewport.superclass.initComponent.apply(this, arguments);
    },
    layoutOrientation : function(orientation, w, h) {
        usgs.views.Viewport.superclass.layoutOrientation.call(this, orientation, w, h);
    }
});

Step 2: Modelling Data

Like any MVC-based application the next step is to prepare for the data model. In the models folder, create a definition of the USGS model. The data structure match the content structure is USGS website. In order to demonstrate the use of JSON, I used YQL to convert the RSS data from USGS.

Ext.regModel("usgs.models.UsgsData", {
    fields: [
      {name: 'id', 		type: 'int'},
      {name: 'title', 		type: 'string'},
      {name: 'description', 	type: 'string'},
      {name: 'link', 		type: 'string'},
      {name: 'pubDate', 	type: 'date'},
      {name: 'lat', 		type: 'string'},
      {name: 'long', 		type: 'string'}
    ]
});

Create a simple data store to retrieve the data. For cross domain ajax, set the proxy type to ‘scripttag’. It also handles the callback. 🙂

usgs.stores.usgsData = new Ext.data.Store({
    model: 'usgs.models.UsgsData',
    proxy: {
    	type: 'scripttag',
    	url: 'http://query.yahooapis.com/v1/public/yql',
    	extraParams: {
    		format: 'json'
    	},
	  	reader: {
	  		root: 'query.results.item'
	  	}
  	}
});

Step 3: Creating the Views

The data is ready, now we start to create the view for the USGS data. It allows us to test the data store as well. In this example, I have 3 views:

  1. USGS Menu – this view contains list of data we can retrieve from USGS
  2. USGS List – based on the selected menu, this view shows the list of USGS earthquake data.
  3. USGS Map – this view shows the map where the earthquake occurred.

Here I will just show one of the view. Ignore the call to the controller (Ext.dispatch(…)) for now, wire the view when the controller is ready.

usgs.views.UsgsList = Ext.extend(Ext.Panel, {
    dockedItems: [{
        xtype: 'toolbar',
        title: 'USGS',
        dock: 'top',
        items: [{
        	xtype: 'button',
          text: 'Back',
          ui: 'back',
          handler: function() {
            Ext.dispatch({
                controller: usgs.controllers.usgsController,
                action: 'backToIndex'
            });
          },
          scope: this
        }]
    }],
    items: [{
        xtype: 'list',
        emptyText   : 'No data available.',
        store: usgs.stores.usgsData,
        itemTpl: '{title}',
        onItemDisclosure: function (record) {
            Ext.dispatch({
                controller: usgs.controllers.usgsController,
                action: 'showMap',
                data: record.data
            });
        },
        grouped: false,
        scroll: 'vertical',
        fullscreen: true
    }],
    initComponent: function() {
        usgs.views.UsgsList.superclass.initComponent.apply(this, arguments);
    }
});

Step 4: Building the Controller

We reached the last step and this is the point where all the action and view wired together. If you look back at the view, the handler dispatches specific action to the controller. The actions are defined in the methods below and the controller will then manage the page flow by activating the card. Switching page is done by calling the setActiveItem() and it is applicable because my Viewport is using card layout.

usgs.controllers.usgsController = new Ext.Controller({

    index: function(options) {
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsMenu, options.animation
        );
    },

    showMap: function(options) {
        var data = options.data;
    	usgs.views.usgsMap.addMap(data);
        usgs.views.viewport.setActiveItem(
           usgs.views.usgsMap, options.animation
        );
    },

    showUsgsList: function(options) {
        var id = parseInt(options.id);
    	usgs.stores.usgsData.getProxy().extraParams.q = usgs.query[id].q;
    	usgs.stores.usgsData.read();
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsList, options.animation
        );
    },

    backToIndex: function(options) {
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsMenu, options.animation
        );
    },

    backToUsgsList: function(options) {
        usgs.views.viewport.setActiveItem(
            usgs.views.usgsList, options.animation
        );
    }
});

If you are not using card layout then the controller will need to render the UI component. In the sample code below the view is rendered using the Ext.Controller render() and listener is added to catch the event bubble from the view.

list: function() {
    this.listView = this.render({
        xtype: 'myMainPanel',
        listeners: {
            scope : this,
            filter: this.onFilter,
            selectionchange: this.onSelected
        }
    }, Ext.getBody()).down('.dataList');
}

The screenshots below are the result from the above tutorial.

Implementing the structure and MVC into Sencha Touch definitely helps to simplify my design. I can easily implement the page flow and not end up with spaghetti code. It also helps to separate the view, data and controller and make the code more maintainable.

Hope it is useful and happy hacking!

Dynamically Loading Ext.NestedList

After developing the USGS earthquake for mobile, I want to extend it to display more information. One of the challenge I faced was how to dynamically load data into the nested list. I did a bit reading in the forum and someone suggested to implement the following.

var nestedList = new Ext.NestedList({
items: []
});

var list = [
{
text: 'Test'
},{
text: 'Test 2'
}];

nestedList.setList(list, true);

I did try out to use setList(), however I noticed a problem when I clicked the back button. The nested list still remember the original “items” which was set to blank or []. It is correct that setList() will rebuild the entire list, however in this instance it is not the right method to use. One thing to highlight is the setList() is called when item is tapped and when we click back button, however everytime it is called the new list is appended to the end of the collection as shown in the code below. As we tap the item and click back, the index counter become wrong.

if (!this.lists.contains(list)) {
this.lists.push(this.add(list));
}

I will cover 2 scenarios related to this topic:

  1. Dynamically load nested list after the UI is launched.
  2. Dynamically load list when item is tapped.

The alternative solution to dynamically set the nested list is to extend the original object and implement a new method “reset” as shown below. Perform exactly as It is in setList() but instead of joining the list, reset it. This will address scenario 1.

Ext.ws.NestedList = Ext.extend(Ext.NestedList, {
resetList : function(list, init) {
//...
if (!this.lists.contains(list)) {
this.lists[0] = this.add(list);
}
//...
},

onItemTap : function(item) {
//...
}
});

In order to address scenario 2, override the onItemTap() as shown below. In this example, I added a new attribute “fid” into the item which helps to decide what data to load. In your implementation you might call JSONP request to load the data.

Ext.ws.NestedList = Ext.extend(Ext.NestedList, {
resetList : function(list, init) {
//...
},

onItemTap : function(item) {
item.el.radioClass('x-item-selected');
if (item.items) {
this.backButton.show();
if (item.fid == 'A') item.items = [ {text: 'List Z.1'} ];
this.setList(item);
this.listIndex++;
}
this.fireEvent('listchange', this, item);
}
});

The following is the full source code used to demonstrate the 2 scenarios. If you have alternative solution to the above, do drop me comment and share it. 🙂

<script type="text/javascript">

Ext.ns('Ext.ws');

Ext.ws.NestedList = Ext.extend(Ext.NestedList, {
resetList : function(list, init) {
var items = init ? list : list.items;
if (!list.isList) {
list = new Ext.Container({
isList: true,
baseCls: 'x-list',
cls: 'x-list-flat',
defaults: {
xtype: 'button',
baseCls: 'x-list-item',
pressedCls: 'x-item-pressed',
ui: null,
pressedDelay: true
},
listeners: {
afterrender: function() {
this.getContentTarget().addClass('x-list-parent');
}
},
scroll: 'vertical',
items: items,
text: list.text
});
}

this.lists = this.lists || [];
if (!this.lists.contains(list)) {
this.lists[0] = this.add(list);
}

var isBack = (this.lists.indexOf(list) < this.lists.indexOf(this.activeItem));
if (this.rendered) {
this.setCard(list, init ? false : {
type: this.animation,
reverse: isBack
});
}
this.activeItem = list;
},

onItemTap : function(item) {
item.el.radioClass('x-item-selected');
if (item.items) {
this.backButton.show();
if (item.fid == 'A') item.items = [ {text: 'List Z.1'} ];
this.setList(item);
this.listIndex++;
}
this.fireEvent('listchange', this, item);
}
});

Ext.setup({
onReady: function() {

var list = [ {text: 'List A', fid: 'A', items: [] },
{text: 'List B', fid: 'B', items: [ {text: 'List B.1'} ] },
{text: 'List C', fid: 'C', items: [ {text: 'List C.1'} ] }
];

var nestedList = new Ext.ws.NestedList({
items: [],
fullscreen: true
});

nestedList.resetList(list, true);

} // end onReady
}); // end ext.setup

</script>

Update: the example above was based on Sencha Touch 0.91

USGS Earthquakes Map for Mobile

Another good example of how fast and easy it is to develop a mobile application using Sencha Touch. I spent 2 nights and finally get it up and running. It is a very simple application that read feed from USGS feed and plot the earthquake location into Google Maps. For this application, I used the “kitchen sink” example as the base. It is a really good example to start with. The UI implementation allows the apps to run both on desktop (on HTML5 ready browser) and mobile without compromising the look and feel. See the screenshot below. The mobile version has the iPhone UI touch 🙂

I did encounter one problem during the implementation. The USGS data is in XML format, so I tried using the XMLReader provided in Ext. Somehow, I keep getting error from the XMLReader saying I am accessing property (totalProperty) on undefined object. Not sure whether or not it is a bug. Thanks to YQL, I converted the XML feed to JSON. The SQL like syntax looks friendly, see below

var makeAjaxRequest = function() {
var params = {
rssUrl: 'http://earthquake.usgs.gov/earthquakes/catalogs/shakerss.xml'
};
var usgsRssQuery = new Ext.Template('select * from rss where url="{rssUrl}"');
var query = usgsRssQuery.applyTemplate(params);

Ext.util.JSONP.request({
url: 'http://query.yahooapis.com/v1/public/yql',
callbackKey: 'callback',
params: {q: 'select * from rss where url="<a href="http://earthquake.usgs.gov/earthquakes/catalogs/shakerss.xml&quot;'">http://earthquake.usgs.gov/earthquakes/catalogs/shakerss.xml"'</a>, format: 'json'},
callback: function(data) {
// Process feed data.
}
});
}

The apps UI comprise of 2 part, first is the list that display the feed data and second is the panel that display the map. The main UI is based on the nested lists components and using onListChange event to handle touch or click on the data item. Here is the setup

this.navigationPanel = new Ext.NestedList({
items: this.navigationItems || [],
dock: 'left',
width: 250,
height: 456,
hidden: !Ext.platform.isPhone && Ext.orientation == 'portrait',
toolbar: Ext.platform.isPhone ? this.navigationBar : null,
listeners: {
listchange: this.onListChange
,scope: this
}
});
var pos=new google.maps.LatLng(feedArray[i].lat, feedArray[i].long);
var desc = feedArray[i].description;
var map = new Ext.Map({
title: i,
markerDesc: desc,
markerPos: pos,
mapOptions: {
zoom: 5,
mapTypeId: google.maps.MapTypeId.ROADMAP
,center: pos
},
listeners: {
delay: 500,
afterrender: function(){
addMarker(this.map, this.markerPos, this.markerDesc);
}
}
});
var panel = new Ext.Panel({
items: [map]
});

Similar to the Nearby Tweet, I couldn’t test it on Android. If you do try it on android, drop a comment and share whether or not it works. To try the apps visit http://bit.ly/a7KJj4

Note: For desktop, try on HTML5 ready browser. I used Chrome to test it out.

Update: See the updated version implemented using MVC here.