The City Bars App with Sencha Touch 2
The City Bars Appwith
Sencha Touch 2
A JavaScript framework for building rich mobile apps with web standards
Sencha Touch
http://sencha.com/x/d5http://sencha.com/x/ed
Basically...
Get a WebKit-based desktop browser
Get some emulators & real devices
Download the Sencha Touch 2 PR2 SDK
Develop against a local web server
Optional, but highly recommended!
http://sencha.com/touch
stylesheet
script framework
Introducing theCity Bars App
http://senchalearn.github.com/citybars2
http://sencha.com/x/ee
Pre-requisites
Yelp developer API key: http://www.yelp.com/developers/
Install Sass and Compass: http://sass-lang.com/download.html
http://compass-style.org/install/
http://github.com/senchalearn/citybars2
Development sequence
1 App Architecture
2 UI Structure
3 Data Modeling
4 List Binding
5 List Event Handler
6 Detail Page
7 Customize Theme
Application Architecture
1_app_architecture
application
JS + CSS in SDK
entry point
<!doctype html><html> <head>
<title>City Guide</title>
<script src="lib/touch2/sencha-‐touch-‐all-‐debug.js"></script> <link href="lib/touch2/resources/css/sencha-‐touch.css" rel="stylesheet" />
<script src="app/app.js"></script>
</head> <body></body></html>
index.html
our app
don’t panic
JS + CSS*
*or from CDN
yay! HTML5
var CB;Ext.application({
launch: function() { CB = this; CB.cards = Ext.create('Ext.Panel', { fullscreen: true, html: 'Hello world' }); }
});config object
global namespace
instantiates application
create main UI panel
launch event
app.js
UI Structure
2_ui_structure
CB.cards
listCard detailCard
toolbar toolbar
dataList
click
back
var CB;Ext.application({
launch: function() { CB = this; CB.cards = Ext.create('Ext.Panel', { fullscreen: true, layout: 'card', items: [{ id: 'listCard', html: 'List' }, { id: 'detailCard', html: 'Detail' }] }); }
});
id-based ref
UI children
variable ref
how children lay out
<aside>about layouts
and components
Layoutscard
fit hbox
vbox
Child component patterns I
var list = new Ext.List({ store: store, ...});
var panel = new Ext.Panel({ items: [list, ...], ...});
reference component by var
instantiate component
Child component patterns II
var list = Ext.create('Ext.List', { store: store, ...});
var panel = Ext.create('Ext.Panel', { items: [list, ...] ...});
preferable in ST2
Child component patterns III
var panel = Ext.create('Ext.Panel', { items: [ { xtype: 'list', store: store, ... }, ... ], ...});
deferred creation*
* a lightweight object until then
</aside>
{ // the list card id: 'listCard', layout: 'fit', items: [{ // main top toolbar xtype: 'toolbar', docked: 'top', title: 'Please wait' // will get added once city known }, { // the list itself // gets bound to the store once city known id: 'dataList', xtype: 'list' }]}
* list will be bound to a store later
list*
The list card
list should fill whole card
docked top toolbar
{ // the details card id: 'detailCard', items: [{ // detail page also has a toolbar docked : 'top', xtype: 'toolbar', title: '' }, { // textual detail }]}
detail page to come later...
The detail card
another docked toolbar*
* title will be dynamically set
note:list already
scrollable
Data modeling
3_data_modeling
http://api.yelp.com/business_review_search
?ywsid=YELP_KEY
&term=BUSINESS_TYPE
&location=CITY
The YELP API...
free, rate limited
business type, and city name
...returns a nested JSON array
mmm, json
‘businesses’ array
Apigee API console
"businesses": [ { "rating_img_url" : "http://media4.px.yelpcdn.com/...", "country_code" : "US", "id" : "BHpAlynD9dIGIaQDRqHCTA", "is_closed" : false, "city" : "Nashville", "mobile_url" : "http://mobile.yelp.com/biz/...", "review_count" : 50, "zip" : "11231", "state" : "TN", "latitude" : 40.675758, "address1" : "253 Conover St", "address2" : "", "address3" : "", "phone" : "7186258211", "state_code" : "TN", "categories": [...], ... }, ...]
some fields areuseful for our app
Ext.define("Business", { extend: "Ext.data.Model", fields: [ {name: "id", type: "int"}, {name: "name", type: "string"}, {name: "latitude", type: "string"}, {name: "longitude", type: "string"}, {name: "address1", type: "string"}, {name: "address2", type: "string"}, {name: "address3", type: "string"}, {name: "phone", type: "string"}, {name: "state_code", type: "string"}, {name: "mobile_url", type: "string"}, {name: "rating_img_url_small", type: "string"}, {name: "photo_url", type: "string"}, ]});
give the ‘class’ a name
Create a data model
extending base model
and with these named, typed fields
<aside>
</aside>
Models can be associated
with other models
Fields can also have default values,
conversion functions, and validation
var store = Ext.create('Ext.data.Store', { model: "Business", ...});
Create a model store
Think of a store as a ‘table’ of model instance ‘rows’
create the store
containing thistype of model
var store = Ext.create('Ext.data.Store', { model: 'Business', autoLoad: true, proxy: { // call Yelp to get business data type: 'scripttag', url: 'http://api.yelp.com/business_review_search' + '?ywsid=' + YELP_KEY + '&term=' + escape(BUSINESS_TYPE) + '&location=' + escape(DEFAULT_CITY) , reader: { type: 'json', root: 'businesses' } }});
Configure data sourceloads as soon as possible
construct API URL
source
read array from inside JSON
JSONP
<script> YELP_KEY = 'G3HueY_I5a8WZX-‐_bAAAA'; DEFAULT_CITY = 'San Francisco'; BUSINESS_TYPE = 'Bars';</script>
Create constants
please change this!
We can make the proxy URL dynamic,
which would allow geolocation.
But this requires an async
callback sequence.
getCity: function (callback) { callback(DEFAULT_CITY); // this could now be a geo lookup to // get the nearest city},
getBusinesses: function (city, callback) {
Ext.define("Business", { ... }); var store = Ext.create('Ext.data.Store', { ... });
}
Two-phase async sequence
and this will need to fire the callback with store when it autoloads
the data codewe just wrote
use this in the URL
call whenUI ready
var store = Ext.create('Ext.data.Store', { ... listeners: { // when the records load, fire the callback load: function (store) { callback(store); } }}); fire the callback with store
when loaded
eventlisteners
store
records
cheeky callback
List Binding
4_list_binding
// get the cityCB.getCity(function (city) {
// then use Yelp to get the businesses CB.getBusinesses(city, function (store) {
// then bind data to list and show it CB.cards.query('#dataList')[0].setStore(store);
});});
bind the store to itget dataList by its id
our 2 async functions
but we haz records!
:-(
CB.getCity(function (city) {
cards.query('#listCard toolbar')[0] .setTitle(city + ' ' + BUSINESS_TYPE);
...
now title will always match city
another component query
{ id: 'dataList', xtype: 'list', store: null, itemTpl: '{name}'}
model fields in curly braces
List items are templated
Ext.create('Ext.LoadMask', Ext.getBody(), { store: store, msg: ''});
instantiate mask
Spinner bound to store
over body
will show when store is loading
A more interesting template
itemTpl: '<img class="photo" src="{photo_url}" width="40" height="40"/>' + '{name}<br/>' + '<img src="{rating_img_url_small}"/> ' + '<small>{address1}</small>'
HTML allowed
<style> .photo { float:left; margin:0 8px 16px 0; border:1px solid #ccc; -‐webkit-‐box-‐shadow: 0 2px 4px #777; }</style>
Hack the style
...width="40" height="40" />
seems likea waste
src.sencha.io<img src="http://src.sencha.io/40/{photo_url}" width="40" height="40"/>
4 times smaller
List Event Handler
5_list_event_handler
{ id: 'dataList', ... listeners: { selectionchange: function (selectionModel, records) { // if selection made, slide to detail card if (records[0]) {
CB.cards.setActiveItem(1);
CB.cards.getActiveItem().setData( records[0].data );
} } }}
when list itemsare selected
selection
also fires on deselection
detail card
apply record data...
...to detail page template
A back button
items: [{ // detail page also has a toolbar docked : 'top', xtype: 'toolbar', title: '', items: [{ // containing a back button // that slides back to list card text: 'Back', ui: 'back', listeners: { tap: function () { CB.cards.setActiveItem(0); } } }], ...
back to list
when tapped
arrow style
children of toolbarsare implicitlyxtype: ‘button’
Detail Page
6_detail_page
{ // textual detail styleHtmlContent: true, cls: 'detail', tpl: [ '<img class="photo" src="{photo_url}" />', '<h2>{name}</h2>', '<div class="info">', '{address1}<br/>', '<img src="{rating_img_url_small}"/>', '</div>', '<div class="phone x-‐button">', '<a href="tel:{phone}">{phone}</a>', '</div>', '<div class="link x-‐button">', '<a href="{mobile_url}">Read more</a>', '</div>' ]}]
style this card as regular HTML
CSS class for styling
template fora whole panel
:-(
CB.cards.getActiveItem().setData( records[0].data);
setData does not cascade into child items!
Remember this?
setData: function (data) { this.query('toolbar')[0].setTitle(data.name); this.query('[cls="detail"]')[0].setData(data);},
set title on toolbar
Override setData
apply data to template on inner panel
good
not so much
A little styling
.x-‐html h2 { margin-‐bottom:0;}.phone, .link { clear:both; font-‐weight:bold; display:block; text-‐align:center; margin-‐top:8px;}
.detail { -‐webkit-‐box-‐orient: vertical;}.detail .photo { float:none;}
formattingthe buttons
temporaryfixes
One final tweak
{ // textual detail cls: 'detail', styleHtmlContent: true, ...
...to outer card
move frominner panel...
{ // the details card id: 'detailCard', styleHtmlContent: true,
complete with<h2> </h2>
Development sequence
1 App Architecture
2 UI Structure
3 Data Modeling
4 List Binding
5 List Event Handler
6 Detail Page
7 Customize Theme
Other ideas...
‘Responsive’ Apps
http://sencha.com/x/cv
Packaging
Add to home screen - Icon - Splash screen
Hybrid app; PhoneGap / NimbleKit - Contacts API - Geolocation
http://sencha.com/x/cyhttp://sencha.com/x/de
Geolocation
O!ine app
http://github.com/jamesgpearce/confess
O!ine data
Taking Yelp data o"ine
Taking images o"ine - src.sencha.io to generate cross-origin B64
Detecting network connection changes
http://sencha.com/x/df
Debugging
http://phonegap.github.com/weinre
James Pearce @ jamespearce