Sencha Touch framework is a little brother of Ext JS. They both have the same creator: Sencha Inc, and they both are built on the same set of core classes. But Sencha Touch is created for developing mobile Web, while Ext JS is for desktop Web applications.
Enterprise IT managers need to be aware of another important difference: Ext JS offers free licenses only for open source projects, but Sencha Touch licenses are free unless you decide to purchase this framework bundled with developers tools.
This chapter is structured similarly to the previous one that described jQuery Mobile - minimum theory followed by the code. The fundamental difference though is that if Chapter 12 has almost no JavaScript, while this chapter will have almost no HTML.
We’ll try minimizing repeating the information you can find in Sencha Touch Learning Center and extensive product documentation, which has multiple well written Guides on various topics. This chapter will start with a brief overview of the features of Sencha Touch followed by the code review of yet another version of the Save The Child application. In this chapter we are going to use Sencha Touch 2.2.1, which is the latest version at the time of this writing. It supports iOS, Android, Blackberry, and recently have announced support of Window Phone 8.
Note
|
If you haven’t read Chapter 6 on Ext JS, please do it now. Both of these frameworks are built on the same foundation and we assume that you are familiar with such concepts as MVC architecture and things like xType , SASS and some others explained in Chapter 6. For the most part Ext JS and Sencha Touch non-UI classes are compatible, but there are some differences that may prevent you from 100% code reuse between these frameworks(e.g. see section "Stores and Models"). The future releases Sencha should come up with some standard solutions to remove the differences in class systems of both frameworks.
|
Let’s start with downloading Sencha Touch from http://www.sencha.com/products/touch/download. If you want to get a free commercial license just specify your email address, and you’ll receive the link to download in the email. Sencha Touch framework comes as a zip file, which you should unzip in any directory - later you’ll copy the framework’s code either into your project directory or in the document root of your Web server.
Warning
|
Commercial license of Sencha Touch doesn’t include charts (you’d need to get either Sencha Complete or Sencha Touch Bundle for the chart support). Because of this we’ll be using the GPL version for the open source Save The Child project, and our users will see a little watermark "Powered by Sencha Touch GPLv3" as shown on Figure A GPL license watermark below. |
After downloading Sencha Touch we’ve unzipped it into the directory /Library/touch-2.2.1, but the code generation process will copy this framework into our application directory.
If you haven’t downloaded and installed the Sencha CMD tool, do it now as described in Chapter 6. This time we’ll use Sencha CMD to generate a mobile version of Hello World. After opening a Terminal or Command Window enter the following command specifying the absolute path to your ExtJS SDK directory and to the output folder, where the generated project should reside.
sencha -sdk /Library/touch-2.2.1 generate app HelloWorld /Users/yfain11/hellotouch
After the code generation is complete, you’ll see the folder hello of the structure shown on Figure CMD-generated project. It follows the MVC pattern discussed in the Ext JS chapter.
To test your newly generated application make sure that the directory hellotouch is deployed under a Web server (simple opening of index.html in the Web browser won’t work). You can either install any Web server or just follow the instructions from section about XAMPP and Apache Web Server in Chapter 6. In the same chapter you can find the command to start the Jetty Web server embedded in the Sencha CMD tool.
Here we are going to use the internal Web server that comes with WebStorm IDE. It runs on the port 63342, and if your project’s name is helloworld, the URL to test it is http://localhost:63342/helloworld.
Note
|
To debug your code inside WebStorm IDE, select the menu Run | Edit Configurations, press the plus sign in the top left corner and in JavaScipt Debug | Remote panel enter the URL http://localhost:63342 followed by the name of your project(e.g. ssctouch) and name your new debug configuration. After that you’ll be able to debug your code in your Chrome Web browser (it’ll ask you to install the JetBrains IDE Support extension of the first run). |
Tip
|
MAC OS X users can install a small application Anvil that can easily serve static content of any directory as a Web server with a URL that ends with .dev. |
Figure Running CMD-generated Hello World shows how the generated Hello World application will look in Chrome browser. It’ll consist of two pages controlled by the buttons in the footer toolbar.
The main application entry is the Javascript file app.js. But if in Ext JS, this file was directly referenced in index.html, Sencha Touch applications generated by CMD tool use a separate microloader script, which starts with loading the file app.json that contains the names of the resources needed for your application including the app.js. The only script included in the generated index.html is this one:
<script id="microloader" type="text/javascript"
src="touch/microloader/development.js"></script>
This script uses one of the scripts located in the microloader folder, which gets the object names to be loaded from the configuration file app.json. This file contains a JSON object with various attributes like js
, css
, resources
and others. So if your application needs to load the scripts sencha-touch.js and app.js, they should be located in the js
array. Here’s what the js
attribute of the app.json contains after the initial code generation by Sencha CMD:
"js": [
{
"path": "touch/sencha-touch.js",
"x-bootstrap": true
},
{
"path": "app.js",
"bundle": true,
"update": "delta"
}
]
Eventually, if you’ll need to load additional JavaScript code, CSS files or other resources add them to the appropriate attribute in the file app.json.
Introducing a separate configuration file and additional microloader script may seem like an unnecessary complication, but it’s not. On the contrary, it gives you the flexibility of maintaining clean separation between development, testing, and production environments. You can find three different loader scripts in the folder touch/microloader: development.js, production.js, and testing.js. Each of them can load different configuration file.
Tip
|
Our sample application includes some sample video files. Don’t forget to include "resources/media" folder in the resources section of the app.json.
|
If you open the source code of the production loader, you’ll see that it uses application cache to save files locally on the device (see section Application Cache in Chapter 2 for a refresher), so the user can start the application even without having the Internet connection.
The production microloader of Sencha Touch offers a smarter solution for minimizing unnecessary loading of cached JavaScript and CSS files than HTML5 Application Cache. The standard HTML5 mechanism doesn’t know which resources have changed and reloads all cacheable files. CMD-generated production builds for Sencha Touch keep track of changes and create deltas, so the mobile device will download only those resources that has been actually changed. To create a production build, open a Terminal or a command window, change to your application directory and run the following command:
sencha app build production
See the section "Deploying Your Application" for more details on Sencha CMD builds. When we start building our Save The Child application, you’ll see how to prompt the user that the application code has been updated. Refer to the online documentation on using Sencha CMD with Sencha Touch for details.
The ability of Sencha Touch to monitor modified pieces of code helps with deployment - just change the SomeFile.js on the server and it’ll be automatically downloaded and saved on the user’s mobile device. This may have some effect on the application modularization decisions you will take.
Reducing the startup latency and implementing lazy loading of certain parts of the application are the main reasons for modularizing Web applications. The other reason for modularization is an ability to redeploy certain portions of the code vs. the entire application if the code modifications are limited in scope.
So should we load the entire code base from the local storage (it’s a lot faster that getting the code from remote servers) or still use loaders to bring up the portion of the code (a.k.a. modules) on as needed basis? There is no general answer to this question - every application is different.
If your application is not too large and the mobile device has enough memory, loading the entire code of the application from the local storage may lower the need for modularization. For larger applications consider the Workspaces feature of Sencha CMD, which allows to create some common code to be shared by several scripts.
Similarly to Ext JS, the starting point of the Hello World application is the app.js script.
Ext.Loader.setPath({
'Ext': 'touch/src', // (1)
'HelloWorld': 'app'
});
Ext.application({
name: 'HelloWorld',
requires: [
'Ext.MessageBox'
],
views: [
'Main'
],
icon: {
'57': 'resources/icons/Icon.png',
'72': 'resources/icons/Icon~ipad.png',
'114': 'resources/icons/[email protected]',
'144': 'resources/icons/[email protected]'
},
isIconPrecomposed: true,
startupImage: {
'320x460': 'resources/startup/320x460.jpg',
'640x920': 'resources/startup/640x920.png',
'768x1004': 'resources/startup/768x1004.png',
'748x1024': 'resources/startup/748x1024.png',
'1536x2008': 'resources/startup/1536x2008.png',
'1496x2048': 'resources/startup/1496x2048.png'
},
launch: function() {
// Destroy the #appLoadingIndicator element
Ext.fly('appLoadingIndicator').destroy();
// Initialize the main view
Ext.Viewport.add(Ext.create('HelloWorld.view.Main'));
},
onUpdated: function() { // (2)
Ext.Msg.confirm(
"Application Update",
"This application has just successfully been updated to the latest version. Reload now?",
function(buttonId) {
if (buttonId === 'yes') {
window.location.reload();
}
}
);
}
});
-
This code instructs the loader that any class that starts with Ext can be found in the directory touch/src or its subdirectories. The classes with names that starts with HelloWorld are under the app directory.
-
This is an interception of the event that’s triggered if the code on the server was updated. The user is warned that the new version of the application has been downloaded. See more on this in the comments to app.js in the section Save The Child With Sencha Touch.
The code of the generated main view of this application (Main.js) is shown next. It extends the class Ext.tab.Panel
so each page of the application is one tab in this panel. Figure Collapsed version of Main.js from Hello World is a snapshot of a collapsed version of Main.js taken from WebStorm IDE from JetBrains, which is our IDE of choice in this chapter.
As you see from this figure the items[]
array includes two objects: Welcome and Get Started - each of them represents a tab (screen) on the panel.
Ext.define('HelloWorld.view.Main', {
extend: 'Ext.tab.Panel',
xtype: 'main',
requires: [
'Ext.TitleBar',
'Ext.Video'
],
config: {
tabBarPosition: 'bottom', // (1)
items: [
{ // (2)
title: 'Welcome',
iconCls: 'home',
styleHtmlContent: true,
scrollable: true,
items: {
docked: 'top',
xtype: 'titlebar',
title: 'Welcome to Sencha Touch 2'
},
html: [
"You've just generated a new Sencha Touch 2 project. What you're looking at right now is the ",
"contents of <a target='_blank' href=\"app/view/Main.js\">app/view/Main.js</a> - edit that file ",
"and refresh to change what's rendered here."
].join("")
},
{ // (3)
title: 'Get Started',
iconCls: 'action',
items: [
{
docked: 'top',
xtype: 'titlebar',
title: 'Getting Started'
},
{
xtype: 'video',
url: 'http://av.vimeo.com/64284/137/87347327.mp4?token=1330978144_f9b698fea38cd408d52a2393240c896c',
posterUrl: 'http://b.vimeocdn.com/ts/261/062/261062119_640.jpg'
}
]
}
]
}
});
-
The tab bar has to be located at the bottom of the screen.
-
The first tab is a Welcome screen.
-
The second tab is the Getting Started screen. It has
xtype: video
, which means it’s ready for playing video located at the specifiedurl
.
This application has no controllers, models or stores. But it does include the default theme from SASS stylesheet resources/sass/app.scss, which was compiled by Sencha CMD generation process into the file resources/css/app.css.
Sencha Touch has a number UI components specifically designed for mobile devices, which include lists, forms, toolbars, buttons, charts, audio, video, carousel and more. The quickest way to get familiar with UI components is by browsing the Kitchen Sink Web site, where you can find the examples of how UI components look and see the source code of these examples.
In general, the process of implementing of a mobile application with Sencha Touch will consist of selecting the appropriate containers and arranging the navigation between them. Each screen that user sees is a container. Pretty often it’ll include a toolbar docked on top or bottom of the container.
Containers can be nested - they are needed for better grouping of UI components on the screen. The lightest container is Ext.Container
. It inherits all the functionality from it ancestor Ext.Component
plus it can contain other components. When you’ll be reviewing the code of the Save The Child application, note that the main view SSC.view.Main
from Main.js extends Ext.Container
. The hierarchy of Sencha Touch containers is shown on Figure Sencha Touch Containers Hierarchy.
The FieldSet
is also a pretty light container - it simply adds the title to a group of fields that belong together. You’ll see several code samples in this chapter with xtype: 'fieldset'
(e.g. Login or Donate screens).
If your containers display forms with such inputs as text field, text area, password, and numbers, the virtual keyboard will automatically show up occupying half of the user’s screen. On some platforms, virtual keyboards will adapt to the type of the input field, for example, if the field has xtype: 'emailfield'
, the keyboard will be modified for easier input of emails. Figure The iPhone virtual keyboard for entering emails is a snapshot taken from the Donate screen of the Save The Child application when the user tapped inside the email field - note the key with the at-sign on the main keyboard, which wouldn’t be shown for non-email inputs.
If the field is for entering a URL (xtype: 'urlfield'
) expect to see a virtual keyboard with the button labeled as ".com". If the input field has xtype: 'numberfield'
the user may see a numeric keyboard when the focus gets into this field.
Tip
|
If you need to detect the environment on the user’s mobile device, use such classes as Ext.os. for detecting the Operating System, Ext.browser for browser, and Ext.feature for supported features.
|
Besides grouping components, containers allow you to assign a Layout
to control its children arrangements. In desktop applications screens are larger and pretty often you can place multiple containers on the same screen at the same time. In mobile world you don’t have such a luxury and typically you’ll be showing just one container at a time. Not all layouts are practical to use on smaller screens, which is the reason why not all Ext JS layouts are supported in Sencha Touch.
Figure The Main.js in a collapsed form illustrates the main container that shows either the tabpanel
or loginform
. The tabpanel
is a container with a special layout that shows only one of its child containers at a time (e.g. About, Donate, et al).
By default, a container’s layout is auto
, which instructs the rendering engine to use the entire width of the container, but use just enough height to display the children. This behavior is similar to the vbox
layout (vertical box), where all components are being added to the container vertically - one under another. Accordingly, the `hbox`will arrange all components horizontally - one next to the other.
Tip
|
If you want to control how much of a vertical or horizontal screen space is given to each component use the flex property as described in Chapter 6 in the section "The flex Property".
|
The fit
layout will fill the entire container’s space with its child element. If you have more than one child element in the container - the first one will fill the entire space, and the other one will be ignored. The card
layout is somewhat similar to fit
, but it can accommodate multiple children while displaying only one at a time. The container’s method setActiveItem()
allows programmatically select the "card" to be on top of the deck.
You can find examples of card
and fit
layouts in the code of Main.js of the Save The Child application. Figure TabPanel’s children in a collapsed form shows card
layout, but if you’ll expand the tabpanel
container, each tab has the fit
layout.
The classes TabPanel
and Carousel
represent two different implementations of the containers with card
layout.
Events can be initiated either by the browser or by the user. Chapter 6 has the section with the same title - it covers general rules of dealing with events in Ext JS framework. Lots of system events are being dispatched during UI component rendering. The online documentation lists every event that can be dispatched on Sencha classes. Look for the Events section on the top toolbar in the online documentation. Figure Events in Online documentation is a snapshot from online documentation for the class Ext.Container
, which has 32 events.
Sencha Touch knows how to handle various mobile-specific events. Check out the documentation for the class Ext.dom.Element
- you’ll find there such events as touchstart
, touchend
, tap
, doubletap
, swipe
, pinch
, longpress
, rotate
, and others.
You can add event listeners using different techniques. One of them is defining the listeners
config property during the object instantiation. This property is declared in the Ext.Container
object and allows you to define more than one listener at a time. You should use it while calling the Ext.create()
method:
Ext.create('Ext.button.Button', {
listeners: {
tap: function() { // handle event here }
}
}
If you need to handle an event only once, you can use the option single: true
, which will automatically remove the listener after the first handling of the event. For example:
listeners: {
tap: function() { // handle event here },
single: true
}
Tip
|
Read the comments to the code of SSC.view.CampaignsMap in Chapter 6 about the right place for declaring listeners.
|
You can also define event handlers using yet another config property control
from Ext.Container
. For example the following code fragment from the Login controller of the Save The Child application shows how to assign the tap
event handler functions showLoginView()
and cancelLogin()
for the buttons Login and Cancel.
Ext.define('SSC.controller.Login', {
extend: 'Ext.app.Controller',
...
config: {
control: {
loginButton: {
tap: 'showLoginView'
},
cancelButton: {
tap: 'cancelLogin'
}
}
},
showLoginView: function () {...},
cancelLogin: function () { ...}
});
Read more about the role of controllers in event handling in the section titled Controller later in this chapter. Online documentation includes the Event Guide - it describes the process of handling events in detail.
Tip
|
If you want to fire custom events, use the method fireEvent() , providing the name of your event. The procedure for defining the listeners for custom events remains the same.
|
Note
|
The Bring Your Own Device is getting more and more popular in enterprises. Sencha offers a product called Sencha Space, which is a secure and managed environment to deploy enterprise HTML5 application that can be run on a variety of devices that employees can bring to the workplace. Sencha Space promises a clear separation between work-related applications and personal data. It uses secure database and secure File API and allows App-to-App communication.For more details visit the Sencha Space Web page. |
The Sencha Touch version of the Save The Child application will be based on the mockup from Chapter 12, section "Prototyping Mobile Version" with some minor changes. This time the home page of the application will be a slightly different version of the About page shown on The Starting/About page.
Important
|
The materials presented in this chapter were tested only with the current version of Sencha Touch framework, which at the time of this writing was 2.2.1. |
Below is the code of the app.js in the Save The Child project (we’ve just removed the default startup images and icons for brevity). For the most part is has the same structure as Ext JS applications.
Ext.application({
name: 'SSC',
requires: [
'Ext.MessageBox'
],
views: [
'About',
'CampaignsMap',
'DonateForm',
'DonorsChart',
'LoginForm',
'LoginToolbar',
'Main',
'Media',
'Share',
'ShareTile'
],
stores: [
'Campaigns',
'Countries',
'Donors',
'States',
'Videos'
],
controllers: [
'Login'
],
launch: function() {
// Destroy the #appLoadingIndicator element
Ext.fly('appLoadingIndicator').destroy();
// Initialize the main view
Ext.Viewport.add(Ext.create('SSC.view.Main'));
},
onUpdated: function() {
Ext.Msg.confirm(
"Application Update",
"This application has just successfully been updated to the latest version. Reload now?",
function(buttonId) {
if (buttonId === 'yes') {
window.location.reload();
}
}
);
}
});
The application loads all the dependencies listed in app.js and will instantiate models and stores. The views that require data from the store will either mention the store name like store: 'Videos'
or will use the get method from the class StoreMgr
, for example Ext.StoreMgr.get('Campaigns');
. After this is done, the launch
function will be called - this is where the main view is created.
In this version of the Save The Child application we have only one controller Login
that doesn’t use any stores, but the mechanism of pointing controllers to the appropriate store instances is the same as for views. The application instantiates all controllers automatically. Accordingly, all controllers live in the context of the Application object.
We don’t use explicitly defined models here - all the data are hard-coded in the stores in the data
attributes.
You’ll see the code of the views a bit later, but we wanted to draw your attention to the onUpdated()
event handler. In the section "Microloader and Configurations" we’ve mentioned that production builds of Sencha Touch applications are watching the locally cached JavaScript and CSS files listed in the JS and CSS sections of the configuration file app.json and compare them with their peers on the server. They also watch all the files listed in the appCache
section of app.json. If any of these files changes, the onUpdated
event handler is invoked. For illustration purposes we decided to intercept this event and Figure [13-12] shows how the update prompt can look like on iPhone 5.
At this point the user can either select working with the previous version of the application or reload the new one.
Our index.html file beside the microloader script includes one more script that supports Google Maps API.
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
Tip
|
If you want your program documentation look as good as Sencha’s use JSDuck tool. |
The code of the UI landing page of this application is located in the views folder in the file Main.js. First, take a look at the screen shot from WebStorm IDE on figure The Main.js in a collapsed form that there are only two objects on the top level: the container and a login form.
The card
layout means that the user will see either the content of that container or the login form - one at a time. Let’s open up the container. It has an array of children, which are our application pages. The figure TabPanel’s children in a collapsed form shows the titles of the children.
The entire code of the Main.js is shown next.
Ext.define('SSC.view.Main', {
extend: 'Ext.Container',
xtype: 'mainview', // (1)
requires: [
'Ext.tab.Panel',
'Ext.Map',
'Ext.Img'
],
config: {
layout: 'card',
items: [
{
xtype: 'tabpanel', // (2)
tabBarPosition: 'bottom',
items: [
{
title: 'About',
iconCls: 'info', // (3)
layout: 'fit', // (4)
items: [
{xtype: 'aboutview'
}
]
},
{
title: 'Donate',
iconCls: 'love',
layout: 'fit',
items: [
{xtype: 'logintoolbar', // (5)
title: 'Donate'
},
{xtype: 'donateform'
}
]
},
{
title: 'Stats',
iconCls: 'pie',
layout: 'fit',
items: [
{xtype: 'logintoolbar',
title: 'Stats'
},
{xtype: 'donorschart'
}
]
},
{
title: 'Events',
iconCls: 'pin',
layout: 'fit',
items: [
{xtype: 'logintoolbar',
title: 'Events'
},
{xtype: 'campaignsmap'
}
]
},
{
title: 'Media',
iconCls: 'media',
layout: 'fit',
items: [
{xtype: 'mediaview'
}
]
},
{
title: 'Share',
iconCls: 'share',
layout: 'fit',
items: [
{xtype: 'logintoolbar',
title: 'Share'
},
{xtype: 'shareview'
}
]
}
]
},
{xtype: 'loginform',
showAnimation: {
type: 'slide',
direction: 'up',
duration: 200
}
}
]
}
});
-
We’ve assigned the
xtype: 'mainview
to the main view so to allow the Login controller refer to it (see its code below). -
Note that the
tabpanel
doesn’t explicitly specify any layout - it usescard
by default. -
Each of the tabs has a corresponding button on the toolbar. It shows the text from the
title
attribute and the icon specified in the classiconCls
. -
Each of the view has
fit
layout, which forces the content to expand to fill the layout’s container. -
Each view will have a Login button on the toolbar. It’s implemented in the LoginToolbar.js shown later in this chapter.
Starting from version 2.2 Sencha Touch can render icons using icon fonts from Pictos library located in the folder resources/sass/stylesheets/fonts. We’ve used icon fonts in the jQuery Mobile version of our application, and in this version we’ll also fonts, which take a lot less memory than images. Below is the content of our app.scss file that includes several font icons used in the Save The Child application.
@import 'sencha-touch/default';
@import 'sencha-touch/default/all';
@include icon-font('IcoMoon', inline-font-files('icomoon/icomoon.woff', woff, 'icomoon/icomoon.ttf', truetype,'icomoon/icomoon.svg', svg));
@include icon('info', '!', 'IcoMoon');
@include icon('love', '"', 'IcoMoon');
@include icon('pie', '#', 'IcoMoon');
@include icon('pin', '$', 'IcoMoon');
@include icon('media', '%', 'IcoMoon');
@include icon('share', '&', 'IcoMoon');
.child-img {
border: 1px solid #999;
}
// Reduce size of the icons to fit 6 buttons in the tabbar; add Share tab
.x-tabbar.x-docked-bottom .x-tab {
min-width: 2.8em;
.x-button-icon:before {
font-size: 1.4em;
}
}
// Share icons
.icon-twitter, .icon-facebook, .icon-google-plus, .icon-camera {
font-family: 'icomoon';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-twitter:before {
content: "\27";
}
.icon-facebook:before {
content: "\28";
}
.icon-google-plus:before {
content: "\29";
}
.icon-camera:before {
content: "\2a";
}
// Share tiles
.share-tile {
top: 25%;
width: 100%;
position: absolute;
text-align: center;
border-width: 0 1px 1px 0;
p:nth-child(1) {
font-size:4em;
}
p:nth-child(2) {
margin-top: 1.5em;
font-size: 0.9em;
}
}
$sharetile-border: #666 solid;
.sharetile-twitter {
border: $sharetile-border;
border-width: 0 1px 1px 0;
}
.sharetile-facebook {
border: $sharetile-border;
border-width: 0 0 1px;
}
.sharetile-gplus {
border: $sharetile-border;
border-width: 0 1px 0 0;
}
// Media
.x-videos {
.x-list-item > .x-innerhtml {
font-weight: bold;
line-height: 18px;
min-height: 88px;
> span {
display: block;
font-size: 14px;
font-weight: normal;
}
}
.preview {
float: left;
height: 64px;
width: 64px;
margin-right: 10px;
background-size: cover;
background-position: center center;
background: #eee;
@include border-radius(3px);
-webkit-box-shadow: inset 0 0 2px rgba(0,0,0,.6);
}
.x-item-pressed,
.x-item-selected {
border-top-color: #D1D1D1 !important;
}
}
The first two lines of the app.scss import the icons from the default theme. We’ve added several more. Note that we had to reduce the size of the icons to fit six buttons in the application’s toolbar. All the @include
statements use SASS mixin icon()
.
If you need more icons use the IcoMoon application. Pick an icon there and press the button Font to generate the custom font (see Figure Generating twitter icon font with IcoMoon application). Download and copy the generated fonts into your resources/sass/stylesheets/fonts directory and add them to the app.scss using @include icon-font
directive. The downloaded zip file will contain the fonts as well as index.html file that will show you the class name and the code of the generated font icon(s).
When you compile the SASS with compass (or build the application with Sencha CMD), the SASS styles are converted into a standard CSS file resources/css/app.css.
Now let’s review the code of the Login page controller. For simplicity, we have not implemented any application login logic here - our controller reacts on the user’s actions performed in the view LoginForm. The name of the controller’s file is Login.js. It’s located in the folder controller, and here’s the code:
Ext.define('SSC.controller.Login', {
extend: 'Ext.app.Controller',
config: {
refs: {
mainView: 'mainview', // (1)
loginForm: 'loginform', // (2)
loginButton: 'button[action=login]', // (3)
cancelButton: 'loginform button[action=cancel]'
},
control: { // (4)
loginButton: {
tap: 'showLoginView'
},
cancelButton: {
tap: 'cancelLogin'
}
}
},
showLoginView: function () {
this.getMainView().setActiveItem(1); // (5)
},
cancelLogin: function () {
this.getMainView().setActiveItem(0); // (6)
}
});
-
Including the
mainView: 'mainview'
in therefs
attribute forces Sencha Touch to generate a getter functiongetMainView()
providing the access to the main view if need be. -
This controller uses components from the LoginForm view (it’s code comes a bit later).
-
The loginButton is the one that has
action=login
. The cancelButton is the one that’s located inside theloginform
and hasaction=cancel
. -
Defining the event handlers for tap events for the buttons Login and Cancel from the LoginForm view.
-
The main view has two children (see The Main.js in a collapsed form). When the use clicks on the Login button, show the second child:
setActiveItem(1)
. -
When the use clicks on the Cancel button, show the main container - the first child of the main view:
setActiveItem(0)
.
Tip
|
Controllers are automatically instantiated by the Application object. If you want some controller’s code to be executed even before the application launch function is called, put it in the init function. If you want some code to be executed right after the application is launched, put it in the controller’s launch function.
|
For illustration purposes we’ll show you a shorter (but not necessarily better) version of the Login.js. The above code defines the reference to the login form and button selectors in the refs
section. Sencha Touch will find the references and will generate the getter for these buttons. But in this particular example we are using these buttons only to assign them the event handlers. Hence, we can make the refs
section slimmer and use the selectors right inside the control
section as shown below.
Ext.define('SSC.controller.Login', {
extend: 'Ext.app.Controller',
config: {
refs: {
mainView: 'mainview',
},
control: {
'button[action=login]': {
tap: 'showLoginView'
},
'loginform button[action=cancel]': {
tap: 'cancelLogin'
}
}
},
showLoginView: function () {
this.getMainView().setActiveItem(1);
},
cancelLogin: function () {
this.getMainView().setActiveItem(0);
}
});
This version of the Login.js is shorter, but the first one is more generic. In both versions the button selectors are the shortcuts for the ComponentQuery
class, which is a singleton used for searching of components.
With MVC pattern, the event processing logic is often located in controller classes. Using refs
and ComponentQuery
selectors allows you to reach event generating objects located different classes. For example, if the user tapped on a button in a view, controller’s code includes the tap
event handler, where it triggers and event on a store class to initiate the data retrieval.
But if the control
config is defined not in the controller, but in a component, the scope where ComponentQuery
operates is limited to the component itself. You’ll see the example of using the control
config inside DonateForm.js later in this chapter.
Let’s do a brief code review of other Save The Child views.
Figure The Login Form View is a snapshot of Login view taken from iPhone 5.
This is how the code of the Login form view looks like - it’s self explanatory. The ui: 'decline'
is the Ext.Button
style that causes the Cancel button have a red background.
Ext.define('SSC.view.LoginForm', {
extend: 'Ext.form.Panel',
xtype: 'loginform',
requires: [
'Ext.field.Password'
],
config: {
items: [
{ xtype: 'toolbar',
title: 'Login',
items: [
{ xtype: 'button',
text: 'Cancel',
ui: 'decline',
action: 'cancel'
}
]
},
{ xtype: 'fieldset',
title: 'Please enter your credentials',
defaults: {
labelWidth: '35%'
},
items: [
{ xtype: 'textfield',
label: 'Username'
},
{ xtype: 'passwordfield',
label: 'Password'
}
]
},
{ xtype: 'button',
text: 'Login',
ui: 'confirm',
margin: '0 10'
}
]
}
});
The Login form will be displayed when the user clicks on the button Login that is displayed on each other page in the toolbar. For example, the Figure The Login Toolbar shows the top portion of the Donate view.
The Login button is added as xtype: 'logintoolbar'
to the top of each view in the Main.js. It’s implemented in the LoginToolbar.js shown next.
Ext.define('SSC.view.LoginToolbar', {
extend: 'Ext.Toolbar',
xtype: 'logintoolbar',
config: {
title: 'Save The Child',
docked: 'top', // (1)
items: [
{
xtype: 'spacer' // (2)
},
{
xtype: 'button',
action: 'login',
text: 'Login'
}
]
}
});
-
The login toolbar has to located at the top of the screen
-
Adding the
Ext.Spacer
component to occupy all the space before the button Login. By default, spacer has flex value of 1, which means take all the space in this case. You can read more about it in Chapter 6 in the section "The flex Property".
Tip
|
If you’ll add the Save The Child application as an icon to the home screen on iOS devices, the browser’s address bar will not be displayed. |
We wanted to make the Donate view look as per our Web designer’s mockup shown on Figure [FIG12-13]. With jQuery Mobile it was simple - the HTML container <fieldset data-role="controlgroup" data-type="horizontal" id="radio-container">
with a bunch of <input type="radio">
rendered the horizontal button bar shown on Figure [FIG12-28]. Here the fragment from the initial Sencha Touch version of DonateForm.js.
config: {
title: 'DonateForm',
items: [
{ xtype: 'fieldset',
title: 'Please select donation amount',
defaults: {
name: 'amount',
xtype: 'radiofield'
},
items: [
{ label: '$10',
value: 10
},
{ label: '$20',
value: 20
},
{ label: '$50',
value: 50
},
{ label: '$100',
value: 100
}
]
},
{ xtype: 'fieldset',
title: '... or enter other amount',
items: [
{ xtype: 'numberfield',
label: 'Amount',
name: 'amount'
}
]
}
It’s also a fieldset
with several radio buttons - xtype: 'radiofield'
. But the result was not what we expected. These four radio buttons occupied half of the screen and looked as on Figure Rendering of xtype radiofield:
After doing some research, we found out that Sencha Touch has the UI component called Ext.SegmentedButton
that allows create horizontal bar with a number of toggle buttons, which is exactly what was needed from the rendering perspective. The resulting Donate screen is shown on Figure Donate form with SementedButton.
This looks nice, but as opposed to regular HTML form with inputs, the SegmentedButton is not an HTML <input>
field and its value won’t be automatically submitted to the server. This required a little bit of a manual coding, which will be explained as a part of the DonateForm code review, which follows.
Ext.define('SSC.view.DonateForm', {
extend: 'Ext.form.Panel',
xtype: 'donateform',
requires: [
'Ext.form.FieldSet',
'Ext.field.Select',
'Ext.field.Number',
'Ext.field.Radio',
'Ext.field.Email',
'Ext.field.Hidden',
'Ext.SegmentedButton',
'Ext.Label'
],
config: {
title: 'DonateForm',
control: { // (1)
'segmentedbutton': {
toggle: 'onAmountButtonChange'
},
'numberfield[name=amount]': {
change: 'onAmountFieldChange'
}
},
items: [
{ xtype: 'label',
cls: 'x-form-fieldset-title', // (2)
html: 'Please select donation amount:'
},
{ xtype: 'segmentedbutton', // (3)
margin: '0 10',
defaults: {
flex: 1
},
items: [
{ text: '$10',
data: {
value: 10 // (4)
}
},
{ text: '$20',
data: {
value: 20
}
},
{ text: '$50',
data: {
value: 50
}
},
{ text: '$100',
data: {
value: 100
}
}
]
},
{ xtype: 'hiddenfield', // (5)
name: 'amount'
},
{ xtype: 'fieldset',
title: '... or enter other amount',
items: [
{ xtype: 'numberfield', // (6)
label: 'Amount',
name: 'amount'
}
]
},
{
xtype: 'fieldset',
title: 'Donor information',
items: [
{ name: 'fullName',
xtype: 'textfield',
label: 'Full name'
},
{ name: 'email',
xtype: 'emailfield',
label: 'Email'
}
]
},
{
xtype: 'fieldset',
title: 'Location',
items: [
{ name: 'address',
xtype: 'textfield',
label: 'Address'
},
{ name: 'city',
xtype: 'textfield',
label: 'City'
},
{ name: 'zip',
xtype: 'textfield',
label: 'Zip'
},
{ name: 'state',
xtype: 'selectfield',
autoSelect: false,
label: 'State',
store: 'States',
valueField: 'id',
displayField: 'name'
},
{ name: 'country',
xtype: 'selectfield',
autoSelect: false,
label: 'Country',
store: 'Countries',
valueField: 'id',
displayField: 'name'
}
]
},
{
xtype: 'button',
text: 'Donate',
ui: 'confirm',
margin: '0 10 20'
}
]
},
onAmountButtonChange: function (segButton,
button, isPressed) { // (7)
if (isPressed) { // (8)
this.clearAmountField();
this.updateHiddenAmountField(button.getData().value);
button.setUi('confirm'); // (9)
}
else {
button.setUi('normal');
}
},
onAmountFieldChange: function () { // (10)
this.depressAmountButtons();
this.clearHiddenAmountField();
},
clearAmountField: function () {
var amountField = this.down('numberfield[name=amount]');
amountField.suspendEvents(); // (11)
amountField.setValue(null);
amountField.resumeEvents(true); // (12)
},
updateHiddenAmountField: function (value) {
this.down('hiddenfield[name=amount]').setValue(value);
},
depressAmountButtons: function () {
this.down('segmentedbutton').setPressedButtons([]);
},
clearHiddenAmountField: function () {
this.updateHiddenAmountField(null);
}
});
-
Defining event listeners for the
segmentedbutton
and the field for entering other amount. When the control section is used not in a controller, but in a component it’s scoped to the object in which it was defined. Hence theComponentQuery
will be looking forsegmentedbutton
andnumberfield[name=amount]
only within the DonateForm instance. If these event handlers would be defined in the controller, the scope would be global. -
Borrowing the class that Sencha Touch uses for all
fieldset
container so our title looks the same. -
The
segmentedbutton
is defined here. By default, its config propertyallowToggle=true
, which allows only one button to be pressed at a time. -
The
segmentedbutton
has no property to store the value of each of its button. But any sublcass ofExt.Component
has a propertydata
. We are extending thedata
property to store the button’svalue
. It’ll be available in the event handler inbutton.getData().value
. -
Since the buttons in the
segmentedbutton
are not input fields, we define a hidden field to remember the currently selected amount. -
This
numberfield
stores the other amount if entered. Note that it has the same nameamount
as the hidden field. The methodsclearAmountField()
andclearHiddenAmountField()
will ensure that only one of the amounts has a value. -
When the
toggle
event is fired it comes with an object that contains the reference to the button that was toggled, and if the button becomes pressed as the result of this event. -
The toggle event is dispatched twice - one for the button that becomes pressed, and another for the button that was pressed before. If the button becomes pressed (
isPressed=true
), clean the previously selected amount and store a new one in the hidden field. -
Change the style of the button to make it visibly highlighted. We use the predefine`confirm` style (see the Kitchen Sink application for other button styles).
-
When the other amount field loses focus, this event handler is invoked. The code cleans up the hidden field and removes the pressed state from all buttons.
-
Temporarily suspend dispatching events while setting the value of the amount
numberfield
to null. Otherwise setting to null would cause unnecessary dispatching of thechange
event. -
Resume event dispatching. The
true
argument is for discarding all the queued events.
Previous versions of the Save The Child application illustrated how to submit the Donate form to the server for further processing. The Sencha Touch version of this application doesn’t include this code. If you’d like to experiment with this, just create a new controller class that extends Ext.app.Controller
and define there an event handler for the button Donate (see the Login controller as an example).
On the tap
event invoke donateform.submit()
specifying the URL of the server that knows how to process this form. You can find details on submitting and populating forms in the online documentation for Ext.form.Panel
- the ancestor of the DonateForm
.
Tip
|
If you want to use the AJAX-based form submission, use submit() , otherwise use the method standardSubmit() , which will do a standard HTML form submission.
|
The charting support is just great in Sencha Touch (and similar to Ext JS). It’s JavaScript based, the charts are live and can get the data from the stores and model. The Figure <<>> shows how the chart looks on iPhone when the user selects the Stats page:
The code that support the UI part of the chart is located in the view DonorsChart that’s shown next. It uses he classes located in the Sencha Touch framework in the folder src/chart.
Ext.define('SSC.view.DonorsChart', {
extend: 'Ext.chart.PolarChart', // (1)
xtype: 'donorschart',
requires: [
'Ext.chart.series.Pie',
'Ext.chart.interactions.Rotate' // (2)
],
config: {
store: 'Donors', // (3)
animate: true,
interactions: ['rotate'],
legend: { // (4)
inline: false,
docked: 'left',
position: 'bottom'
},
series: [
{
type: 'pie',
donut: 20,
xField: 'donors',
labelField: 'location',
showInLegend: true,
colors: ["#115fa6", "#94ae0a", "#a61120", "#ff8809", "#ffd13e", "#a61187", "#24ad9a", "#7c7474", "#a66111"]
}
]
}
});
-
Create a chart that uses polar coordinates.
-
The
Rotate
class allows the user to rotate (with a finger) a polar chart around its central point. -
The data shown on the chart come from the store named Donors, which is shown in the section "Stores and Models".
-
The legend is a bar at the bottom of the screen. The user can horizontally scroll it with a finger.
The Media page displays the list of available videos. When the user taps on one of them, the new page opens where the user have to tap on the button play. The screen uses the Ext.dataview.List
component to display the video titles from the Videos store.
The Media view extends Ext.NavigationView
, which is a container with the card layout, which also allows to push a new view into this container - we use it to create a view for the selected from the list video. The code of the Media view is shown in the next listing.
Ext.define('SSC.view.Media', {
extend: 'Ext.NavigationView',
xtype: 'mediaview',
requires: [
'Ext.Video' // (1)
],
config: {
control: {
'list': {
itemtap: 'showVideo' // (2)
}
},
useTitleForBackButtonText: true, // (3)
navigationBar: {
items: [
{ xtype: 'button',
action: 'login',
text: 'Login',
align: 'right'
}
]
},
items: [
{ title: 'Media',
xtype: 'list',
store: 'Videos',
cls: 'x-videos',
variableHeights: true,
itemTpl: [ // (4)
'<div class="preview"
style="background-image:url(resources/media/{thumbnail});"></div>',
'{title}',
'<span>{description}</span>'
]
}
]
},
showVideo: function (view, index, target, model) {
this.push(Ext.create('Ext.Video', { // (5)
title: model.get('title'),
url: 'resources/media/' + model.get('url'),
posterUrl: 'resources/media/' + model.get('thumbnail')
}));
}
});
-
Sencha Touch offers
Ext.Video
a wrapper for the HTML5<video>
tag. In Chapter 6 we used the HTML5 tag<video>
directly. -
Defining the event listener for the
itemtap
event, which fires whenever the list item is tapped. -
When the video player’s view will be pushed to the Media page, we want its Back button to display the previous view’s title, which is "Media". It’s a config property in the
NavigationView
. -
The list with descriptions of videos is populated from the store Videos using the list’s config property`itemTpl`. This is an HTML template for rendering each item. We decided to use the
<div>
showing the content of store’s propertiestitle
,description
with a background image from the propertythumbnail
, and the video located at the specifiedurl
. The source code of the store Videos is included in the section "Stores and Models" below. -
Create a video player and push it into the
NavigationView
. When theitemtap
event is fired, it passes several values to the function handler. We just use themodel
that corresponds to the tapped list item.
Note
|
A template [Ext.Template ] represents an HTML fragment. The values in curly braces are being passed to the template from the outside. In the above example the values are coming from the store Videos. The class Ext.XTemlate offers advanced templating, e.g. auto-filling HTML with the data from an array, which is used here.
|
Integration with Google Maps is a pretty straightforward task in Sencha Touch, which comes with Ext.Map
- a wrapper class for Google Maps API. Our view CampainsMap
is a subclass of Ext.Map
. Note that we’ve imported Google Maps API in the file index.html as follows:
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
Figure The Events page shows the iPhone’s screen when the button Events is pressed.
Of course, some additional styling would be needed before offering this view in production environment, but our CampaignsMap.js that supports this screen is only ninety lines of code!
Ext.define('SSC.view.CampaignsMap', {
extend: 'Ext.Map',
xtype: 'campaignsmap',
config: { // (1)
listeners: {
maprender: function () { // (2)
if (navigator && navigator.onLine) {
try {
this.initMap();
this.addCampaignsOnTheMap(this.getMap());
} catch (e) {
this.displayGoogleMapError();
}
} else {
this.displayGoogleMapError();
}
}
}
},
initMap: function () {
// latitude = 39.8097343 longitude = -98.55561990000001
// Lebanon, KS 66952, USA Geographic center of the contiguous United States the center point of the map
var latMapCenter = 39.8097343,
lonMapCenter = -98.55561990000001;
var mapOptions = {
zoom : 3,
center : new google.maps.LatLng(latMapCenter, lonMapCenter),
mapTypeId: google.maps.MapTypeId.ROADMAP,
mapTypeControlOptions: {
style : google.maps.MapTypeControlStyle.DROPDOWN_MENU,
position: google.maps.ControlPosition.TOP_RIGHT
}
};
this.setMapOptions(mapOptions);
},
addCampaignsOnTheMap: function (map) {
var marker,
infowindow = new google.maps.InfoWindow(),
geocoder = new google.maps.Geocoder(),
campaigns = Ext.StoreMgr.get('Campaigns');
campaigns.each(function (campaign) {
var title = campaign.get('title'),
location = campaign.get('location'),
description = campaign.get('description');
geocoder.geocode({
address: location,
country: 'USA'
}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
// getting coordinates
var lat = results[0].geometry.location.lat(),
lon = results[0].geometry.location.lng();
// create marker
marker = new google.maps.Marker({
position: new google.maps.LatLng(lat, lon),
map : map,
title : location
});
// adding click event to the marker to show info-bubble with data from json
google.maps.event.addListener(marker, 'click', (function(marker) {
return function () {
var content = Ext.String.format(
'<p class="infowindow"><b>{0}</b><br/>{1}<br/><i>{2}</i></p>',
title, description, location);
infowindow.setContent(content);
infowindow.open(map, marker);
};
})(marker));
} else {
console.error('Error getting location data for address: ' + location);
}
});
});
},
displayGoogleMapError: function () {
console.log("Sorry, Google Map service isn't available");
}
});
-
We just use
listeners
config here, butExt.Map
has 60 of them. For example, if we wanted the mobile device to identify the current location of the device and put it in the center of the map, we’d adduseCurrentLocation: true
. -
This event is fired when the map is initially rendered. We are reusing the same code as in previous chapters for initializing the map (showing the central point of the USA) and adding the campaigns information. The code of the store Campaigns is shown in the section Stores and Models below.
Sencha Touch is a framework for mobile devices, which can be on the move. Ext.util.Geolocation
is a handy class for applications that require to know the current position of the mobile device. When your program instantiates Geolocation
, it starts tracking the location of the device by firing the locationupdate
event periodically (you can turn auto updates off). The following code fragment shows how to get the current latitude of the mobile device.
var geo = Ext.create('Ext.util.Geolocation', {
listeners: {
locationupdate: function(geo) {
console.log('New latitude: ' + geo.getLatitude());
}
}
});
geo.updateLocation(); // start the location updates
In the Sencha Touch version of the Save The Child application all the data is hard-coded. All store classes are located in the store directory (see Figure TabPanel’s children in a collapsed form), and each of them has the data
property. For example, here’s the code of the Videos.js.
Ext.define('SSC.store.Videos', {
extend: 'Ext.data.Store',
config: {
fields: [
{ name: 'title', type: 'string' },
{ name: 'description', type: 'string' },
{ name: 'url', type: 'string' },
{ name: 'thumbnail', type: 'string' }
],
data: [
{ title: 'The title of a video-clip 1', description: 'Short video description 1', url: 'intro.mp4', thumbnail: 'intro.jpg' },
{ title: 'The title of a video-clip 2', description: 'Short video description 2', url: 'intro.mp4', thumbnail: 'intro.jpg' },
{ title: 'The title of a video-clip 3', description: 'Short video description 3', url: 'intro.mp4', thumbnail: 'intro.jpg' }
]
}
});
Warning
|
There is compatibility issue between Ext JS and Sencha Touch 2 stores and models. For example, in the above code fields and data are wrapped inside the config object, while in Ext JS store they are not. Until Sencha will offer a generic solution to resolve the compatibility issues, you have to come up with your own if you want to reuse the same stores.
|
The code of the Donors store supports the charts in the Stats page. It’s self explanatory:
Ext.define('SSC.store.Donors', {
extend: 'Ext.data.Store',
config: {
fields: [
{ name: 'donors', type: 'int' },
{ name: 'location', type: 'string' }
],
data: [
{ donors: 48, location: 'Chicago, IL' },
{ donors: 60, location: 'New York, NY' },
{ donors: 90, location: 'Dallas, TX' },
{ donors: 22, location: 'Miami, FL' },
{ donors: 14, location: 'Fargo, ND' },
{ donors: 44, location: 'Long Beach, NY' },
{ donors: 24, location: 'Lynbrook, NY' }
]
}
});
The Campaigns store is used to display the markers on the map, where charity campaigns are active. Taping on the marker will show the description of the selected campaign as shown on Figure The Events page - we tapped on Chicago marker. The code of the store Campaigns.js is shown next.
Ext.define('SSC.store.Campaigns', {
extend: 'Ext.data.Store',
config: {
fields: [
{ name: 'title', type: 'string' },
{ name: 'description', type: 'string' },
{ name: 'location', type: 'string' }
],
data: [
{
title: 'Lorem ipsum',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
location: 'Chicago, IL'
},
{
title: 'Donors meeting',
description: 'Morbi mollis ante at ante posuere tempor.',
location: 'New York, NY'
},
{
title: 'Sed tincidunt magna',
description: 'Donec ac ligula sit amet libero vehicula laoreet',
location: 'Dallas, TX'
},
{
title: 'Fusce tellus dui',
description: 'Sed accumsan nibh sapien, interdum ullamcorper velit.',
location: 'Miami, FL'
},
{
title: 'Aenean lorem quam',
description: 'Pellentesque habitant morbi tristique senectus',
location: 'Fargo, ND'
}
]
}
});
This concludes the review of the Sencha Touch version of our sample application, which consists of six nice looking screens. The amount of manual coding to achieve this was minimal. In the real world, you’d need to add business logic to this application, but comes down to inserting the JavaScript code to a well structured layers. The code to communicate with the server will go to the stores, the data will be placed in the models, the UI remains in the views, and the main glue of your application is controllers. Sencha Touch did a pretty good job for us, wouldn’t you agree?
In chapters 12 and 13 you’ve learned about two different ways of developing a mobile application. So what’s better jQuery Mobile or Sencha Touch? There is no answer to this question, and you will have to make a decision on your own. But here’s a quick summary of pros and cons for each library or framework.
Use jQuery Mobile if:
-
If you are afraid of being locked up with any one vendor. The effort to replace jQuery Mobile in your application with another framework (if you decide to do so) is a magnitude lower than switching from Sencha Touch to something else.
-
If you need your application to work on most of the mobile platforms.
-
If you prefer declarative UI and hate debugging JavaScript.
Use Sencha Touch if:
-
If you like to have a rich library of pre-created UI.
-
If your application needs smooth animation. Sencha Touch does automatic throttling based on the actual frames per seconds supported on the device.
-
If splitting the application code into cleanly defined architectural layers (model-view-controller-service) is important.
-
If you believe that using code generators add value to your project.
-
If you want to be able customize and extend components to fit your application’s needs perfectly. Yes, you’ll be writing JavaScript, but it still may be simpler than trying to figure out the enhancements done to HTML component by jQuery Mobile under the hood.
-
If you want to minimize the efforts required to package your application as a native one.
-
If you want your application to look as close to the native ones as possible.
-
If you prefer to use software that is covered by the commercial support offered by vendor.
While considering support options do not just assume that paid support translates into better quality. This is not to say that Sencha won’t offer you quality support, but in many cases having a large community of developers will lead to a faster solution to a problem that dealing with one assigned support engineer. Having said this, we’d like you to know that Sencha forum has about half a million registered users who are actively discussing problems and offering solutions to each other.
Even if you are a developer’s manager, you don’t have to make the framework choice on your own. Bring your team into a conference room, order pizza, and listen to what your team members have to say about these two or any other frameworks being considered. We offered you the information about two of many frameworks, but the final call is yours.