
Understanding the basic application structure
We already looked at the filesystem structure. It is now time to understand how these files are tied together. You also need to understand what an interaction between code parts is.

Ext.application
is the starting point in our application. As we noted earlier, it might contain the app name, and references to all the models, views, controllers, profiles, and stores. These are explained as follows:
- Profiles: These allow us to customize the application's UI for handsets and tablets
- Models: These represent a type of data in our application
- Views: These actually present data in our application within Sencha Touch components
- Controllers: These handle interactions with our application by listening for user's taps and swipes
- Stores: These store our data, which we display in grids and other elements
You can see the single instance of Ext.application
in the generated www/app.js
file:
Ext.application({ name: 'Travelly', views: [ 'Main' ], // ... launch: function() { Ext.fly('appLoadingIndicator').destroy(); Ext.Viewport.add(Ext.create('Travelly.view.Main')); } // ... });
Where:
- Travelly: This is the name of our generated application. We will use it as a global namespace in different places of our application, for example,
Travelly.controllers.Main
,Travelly.model.Main
,Travelly.view.Main
. - Main: This is the name of our one and only view in the application.
- Launch: This is a method called once the application loader has loaded all the required dependencies.
- .fly: This is Sencha's method used to make a one-time reference to DOM. It is taking HTML element on the
index.html
page withid="appLoadingIndicator"
and destroys it. - Viewport.add: This inserts
Main
view intoindex.html
body.
Tip
Sencha Touch is built using lessons learned from Ext JS (http://www.sencha.com/products/extjs). That is why, in our application, you can see the Ext prefix in many places.
The current file structure inside the app folder looks like this:
├── controller ├── form ├── model ├── profile ├── store └── view └── Main.js
It simply becomes clear that in the controller
folder, we should place our controllers, in view
our views, and so on. Now, all our folders, except view
, are empty. Let's fix it and create several items we will need for our application.
Getting familiar with the Sencha Touch view
Our application has already a defined view in the view
/Main.js
file:
Ext.define('Travelly.view.Main', { extend: 'Ext.tab.Panel', xtype: 'main', requires: [ 'Ext.TitleBar', 'Ext.Button' ], config: { tabBarPosition: 'bottom', items: [ { title: 'Welcome', iconCls: 'home' // ... }, { title: 'Get Started', iconCls: 'action' // ... } ] } });
Let's break the preceding code down.
The Ext.define
function helps us define a class named Main
in the Travelly/view
namespace. All view components are placed within this namespace as per Sencha Touch MVC standards.
The extend keyword specifies that the Main
class is the subclass of Ext.tab.Panel
. So, the Main
class inherits the base configuration and implementation of the Ext.tab.Panel
class.
The xtype
keyword is used to instantiate the class.
The requires
property is used because we use a button in our items array. We indicate the new view to require the Ext.Button
class. At the moment, the dynamic loading system does not recognize classes specified by xtype
, so we need to define the dependency manually.
The Config
keyword helps us initialize the variables/components used in that particular class. In this example, we should initialize the tab panel view with tabs and panels. Tab panels are a great way to allow the user to switch between several pages that are all full screen.
The content of Items
in the Main
view currently has two items with title and iconCls
. The tabBarPosition
property defines where the tab bar is placed, and in our case, it is at the bottom. With title, we can set text on a button in the tab panel, and with iconCls
, we can show the predefined icon. Let's add a button to one of this tab panels.
To add a new element to any container (in our case, it is tab panel), it is enough to assign an array of objects to items property. Let's do this with our first tab panel:
{ title: 'Welcome', iconCls: 'home', items: [ { xtype: 'button', text: 'My button', id: 'myButton' } ] }
You can see that we added some text and assigned id
for the button. This will help us handle button events in the controller. However, we can already handle it right in the view. Here is an example:
xtype: 'button', text: 'My button', id: 'myButton', handler: function() { alert('My button has been clicked!'); }
Once the user has tapped on the button, we will see an alert message right away.
And we are done! We just successfully added the Sencha Touch button component to the first tab of our application. We should remember that our application already has the Ext.Viewport.add(Ext.create('Travelly.view.Main'))
code to add our view on the page.
Now, let's create some controller and handle user interaction on the Main
view.
Creating the Sencha Touch controller
Controllers listen for events fired by the UI and take actions based on the event. Using controllers helps keep your code clean and readable, and separates the view logic from the control logic.
There are two ways to create the controller: using Sencha Cmd or manually. To create the controller with Sencha Cmd, it is enough to execute the following command:
$ sencha generate controller Main
It generates the controllers/Main.js
file for us. It is generic and has minimum content:
Ext.define('Travelly.controller.Main', { extend: 'Ext.app.Controller', config: { refs: {}, control: {} }, launch: function(app) {} });
Our controller is a subclass of Ext.app.Controller
, which is instantiated only once by the application that loaded it. At any time, there is only one instance of each controller. To instantiate the controller automatically, we have to add it into the controllers configuration section in the application in the following way:
controllers: [ 'Main' ]
You can see the launch
method. This method is triggered automatically for every controller, every time the application starts. If you do not need it, simply remove this method from the controller.
The config
section is different from the view's section, but it contains two important properties: refs
and controls
.
The refs
property is an easy way to find components on the page. The control
property is similar to the ref's config
property, but it allows us to define event handlers. Here is an example:
config: { refs: { myButton: '#myButton' }, control: { myButton: { tap: 'doMyButtonTap' } } }, doMyButtonTap: function() { alert('My button has been clicked!'); }
In the refs
section, we are using ComponentQuery
to find our button on the page by id
. Whenever a button of this type fires its tap event, our controller's doMyButtonTap
function is called.
This is mostly what a controller does—listens for events that fire (usually by the UI) and initiates some action.
Using store
When web developers think about storing something for the user, they try to upload data to the server and store it there. However, it is a huge issue in mobile web and hybrid applications development when you do not have stable Internet connection. Sometimes, a device can go offline and go online only in several days. We have to stop building apps with a desktop mindset where we have permanent, fast connectivity. Offline technologies don't just give us sites that work offline; they improve performance and security by minimizing the need for cookies, HTTP, and file uploads. They also opens up new possibilities for better user experience.
HTML5 allows us to make the approach real. Now, it is possible to store data on client side in WebStorage, IndexedDB, and Web SQL Database, which are explained here.
- LocalStorage: This is also known as web storage, simple storage, or by its alternate session storage interface. This API provides synchronous key/value pair storage.
- Web SQL Database: This offers more full-featured database tables accessed via SQL queries.
- IndexedDB: This offers more features than LocalStorage, but fewer than Web SQL.
However, there is a limitation for using these client-side storages with the PhoneGap application. Here is a list of mobile platforms supporting different storages:
- LocalStorage
- All supported by PhoneGap platforms
- WebSQL
- Android
- BlackBerry 10
- iOS
- Tizen
- IndexedDB
- BlackBerry 10
- Windows Phone 8
- Windows 8
Tip
You can learn more about browser storage support at http://www.html5rocks.com/en/tutorials/offline/quota-research/.
Now, it is clear what storages we can use with PhoneGap. It is great news that Sencha Touch already has an abstraction for the storage—Ext.data.Store
.
Before looking deeper into Ext.data.Store
, let's get familiar with Sencha Touch models. These models are very important, because they are the main components of the store.
The Sencha Touch model
This model represents objects of business models from our application. For example, we want to store metadata for pictures we capture with camera. In our Travelly application, we can define model Picture
. Let's generate it as we did for controller:
$ sencha generate model Picture id:int,url:string,title:string,lon:string,lat:string
Where:
model
: This is an attribute for thegenerate
command to generate the modelPicture
: This is a name of our model to generateid:int,url:string,title:string,lon:string,lat:strin
: These are fields for our model with their types
In the model
folder, we should see the Picture.js
file:
Ext.define('Travelly.model.Picture', { extend: 'Ext.data.Model', config: { fields: [ { name: 'id', type: 'int' }, { name: 'url', type: 'string' }, { name: 'title', type: 'string' }, { name: 'lon', type: 'string' }, { name: 'lat', type: 'string' } ] } });
Where I have used the following components in the file:
id
as a unique identifier for the pictureurl
as a link to the picturetitle
of the picturelon
as a longitude coordinate of the place where the picture was takenlat
as a latitude coordinate of the place where the picture was taken
To work with this model in our application, we should add Travelly.model.Picture
to the requires
section. Usually, we do this in controllers or stores. To create an instance of this class, use the following lines of code:
var picture = Ext.create('Travelly.model.Picture', { id: 1, url: 'http://myurl.my/somepicture.jpg', title: 'My picture', lat: '50.450783', lon: '30.523035' });
Where:
Ext.create
: This instantiates a class by either full name, alias, or alternate nameTravelly.model.Picture
: This is the name of our model class
We can access model object values by property names. For example, picture.get('title')
will return the title of our picture.
Let's create a data store and map it to the preceding model. There is no Sencha Cmd command for it. So, we will do this manually:
Ext.define('Travelly.store.Pictures',{ extend:'Ext.data.Store', config:{ model:'Travelly.model.Picture', autoLoad:true, data:[ { id: 1, url: 'http://myurl.my/somepicture1.jpg', title: 'My picture 1', lat: '50.450783', lon: '30.523035' }, { id: 2, url: 'http://myurl.my/somepicture2.jpg', title: 'My picture 2', lat: '50.450783', lon: '30.523035' } ], proxy:{ type:'localstorage' } } });
Where:
autoLoad: true
: This means the store's load method is automatically called after creation. We do not need to call the.load
method before working with data.data
: This is a inline data based on our model.proxy
: This is used to load and save data. We usedLocalStorage
.
The Sencha store provides an easy way to add and retrieve data from the store:
var pictureStore = Ext.getStore('Pictures'); pictureStore.add(picture); pictureStore.sync(); var foundPic = pictureStore.findRecord('id', 1);
Where:
Ext.getStore
gets the instance of thePictures
store that is already defined and initializedpictureStore.add
adds our early created picture object to the storepictureStore.sync
syncs our in-memory store data to LocalStoragepictureStore.findRecord
finds and retrieves thePicture
model instance from the in-memory storage
There are two types of proxies that could be used: client and server.
LocalStorageProxy
: This stores data in LocalStorageMemoryProxy
: This stores data in memory only
Ajax
: This is used to interact with a server on the same domainJsonP
: This uses JSONP to send requests to a server on a different domain
We will use server proxies when we integrate our application with RESTful service on Node.js.
Environment detection
Very often, when we run the application, we need to know on which platform we are running it. With Sencha Touch, we can detect this information:
- Operating system:
Ext.os.is.[iOS, Android, MacOS, Windows]
, and so on - Device:
Ext.os.is.[iPhone, iPad, iPod]
and so on - Browser:
Ext.browser.is.[Safari, Chrome, Firefox]
and so on. - Browser's features:
Ext.feature.has.[Canvas, Css3dTransforms]
and so on.
In our case, it is very helpful to use environment detection when we develop. We can detect whether it's desktop browser or mobile device, and run or avoid device-specific functions. Alternatively, we can run different code for Android and iOS.
Creating device profiles
Very often, you need to make the application behave differently on different devices. We can separate code execution by form factor or by operating system. It doesn't matter which we use.
Let's imagine that we need some specific functionality running only on Android tablet, and on all the other devices, we would like to keep it generic.
To generate a specific profile, let's execute the Sencha Cmd command:
$ sencha generate profile AndroidTablet
It generates the profile/AndroidTablet.js
file for us. It is pretty basic, and I modified it to look like this:
Ext.define('Travelly.profile.AndroidTablet', { extend: 'Ext.app.Profile', config: { views: [], models: [], stores: [], controllers: [ 'Main' ] }, isActive: function(app) { return Ext.os.deviceType == 'Tablet' && Ext.os.is.Android; } });
Once the profiles have been loaded, their isActive
functions are called in turn. If these functions return true, the application loads all of its dependencies—the models, views, controllers, and other classes.
Following the launch process
Each application, profile, or controller can define the launch function. However, they don't have to.
Here is the order of functions called after the application starts:
- Controller's init
- Profile's launch
- Application's launch
- Controller's launch
The init
method is called by the controller's application to initialize the controller. Here, we can place any pre-launch logic. At this stage, we do not have UI ready.
By the time the chain of calls reaches the controller's launch, we have already prepared the UI, because mainly, all UIs are created by the profile's or application's launch functions.
The UI and theming
While Sencha Touch initially favored an iOS-styled interface, its main theme is not platform oriented and does not mimic any of the mobile OSes entirely. Nonetheless, iOS, Android, and Windows lookalike themes are shipped with the entire package. Sencha Touch utilizes the SASS approach to its fullest extent. We can quickly restyle a look and feel for our needs. I do not really like the default Sencha Touch theme. So, let's try to change it to iOS cupertino.
Let's do this using these steps:
- Run the
compass watch
command. Sencha Cmd does it for us, with the following command:$ sencha app watch
- Open up the
/resources/sass/app.scss
file in the text editor - Save a copy of the
app.scss
file and rename it tocupertino.scss
- Modify
cupertino.scss
so that it looks like this:@import 'sencha-touch/cupertino'; @import 'sencha-touch/cupertino/all';
- Link our newly generated
cupertino.css
file in theapp.json
file. Let's make the following changes to it:"css": [ { "path": "resources/css/cupertino.css", "update": "delta" } ]
And we are done! When we started the $ sencha app watch
command, you might have realized that it showed the localhost
URL address to test application in output. In my case, it was presented in the following way:
$ sencha web start Sencha Cmd v5.1.0.26 [INF] Mapping http://localhost:1841/ to .... [INF] --------------------------------------------------------------- [INF] Starting web server at : http://localhost:1841 [INF] ---------------------------------------------------------------
So I opened the http://localhost:1841
URL in the browser and was able to see the application. Similarly, you can use the $ sencha web start
command. However, it is not watching for scss
changes. In the upcoming chapters, we will often use the simple Sencha web server for our testing and debugging needs.