sencha touch
DESCRIPTION
Talk I did at the Wellington JS Meetup on 24 Feb 2011 on Sencha Touch and PhoneGap.TRANSCRIPT
Craig Walker, Chief Technology Officer www.xero.com
Craig Walker, Chief Technology Officer www.xero.com
http://www.xero.com/signup/
What is Xero?
BlackBerry 6
BlackBerry 6 2nd half 2011
Lots of frameworks…
• Zepto.js• DynamicX• SproutCore• XUI• Appcelerator• iUI• iWebKit• jQuery Mobile• jQTouch• And lots more…
Sencha Touch
What is Sencha Touch?A JavaScript framework for
building rich mobile applications
Why Sencha Touch?
• Cross-platform• Looks native, feels native• Faster, cheaper, easier to build with• Highly customisable• Flexible deployment• HTML5/CSS3 goodness
Yes - but WHY Sencha Touch?
Tap !== Click
Touch Event Manager
• Built on native events• Abstracted for performance• Multi-touch & gesture support
Touch Event Manager
Ext.fly("el").on({ tap: function() { alert("You tapped me"); }, pinch: function() { alert("You pinched me"); }, swipe: function() { alert("Stop touching me...") }});28
Scroll Event Manager
• Scrolling with momentum & bounce physics
• Native & natural• Hardware accelerated
UI Toolkit
Buttons
FormsSliders
Pickers
Lists
NestedLists
ToolbarsTabs
PanelsCarousels
Maps
Overlays
Layouts
• Container layout specifies how its children components are rendered
fit
card
vbox
hbox
ModelsViews
Controllers
Stores
RoutesMVC
Theming
• SASS & Compass – sass-lang.com– compass-style.org
• CSS3 is awesome – SCSS is awesomer
SCSS CSS
$blue: #3bbfce;$margin: 16px;$padding: 4px;
.example1 { border-color: $blue;}
.example2 { margin: $margin; color: $blue;}
.example3 { margin: ($margin / 2px) * $padding;}
/* line 5, variables.scss */.example1 { border-color: #3bbfce;}
/* line 9, variables.scss */.example2 { margin: 16px; color: #3bbfce;}
/* line 14, variables.scss */.example3 { margin: 32px;}
@mixin add-child($color) { color: $color; background-color: lighten($color, 50); .child { padding: 5px; &:first { background-color: darken($color, 10) }; span { color: mix($color, blue); } }}
.example { @include add-child(#F00);}
/* line 18, mixins.scss */.example { color: red; background-color: white;}/* line 5, mixins.scss */.example .child { padding: 5px;}/* line 8, mixins.scss */.example .child:first { background-color: #cc0000;}/* line 12, mixins.scss */.example .child span { color: #7f007f;}
SCSS CSS
@import "compass";
$width: 100px;
.button { width: $width; .round { @include border-radius(5px); } .linear { @include linear-gradient( color-stops(white, #c39 30%, #b7f 70%, #aaa) ); }}
/* line 5, gradients.scss */.button { width: 100px;}/* line 8, gradients.scss */.button .round { -moz-border-radius: 5px; -webkit-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; -khtml-border-radius: 5px; border-radius: 5px;}/* line 12, gradients.scss */.button .linear { background-image: -webkit-gradient( linear, 0% 0%, 0% 100%, color-stop(0%, #ffffff), color-stop(50%, #cc3399), color-stop(100%, #bb77ff)); background-image: -moz-linear-gradient(top, #ffffff 0%, #cc3399 50%, #bb77ff 100%); background-image: linear-gradient(top, #ffffff 0%, #cc3399 50%, #bb77ff 100%);}
SCSS CSS
Let’s look at some code...
Demo: From Desktop to Mobile
<!doctype html><html><head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Xero Help</title> <link rel="stylesheet" href="resources/css/xerohelp.css" type="text/css"></head><body> <script type="text/javascript" src="lib/sencha-touch-debug.js"></script> <script type="text/javascript" src="app/xerohelp.js"></script></body></html>
index.html
// utilsdocument.write('<script type="text/javascript" src="app/utils/string.js"></script>');
// applicationdocument.write('<script type="text/javascript" src="app/routes.js"></script>');document.write('<script type="text/javascript" src="app/app.js"></script>');
// modelsdocument.write('<script type="text/javascript" src="app/models/TOC.js"></script>');document.write('<script type="text/javascript" src="app/models/HelpFile.js"></script>');
// viewsdocument.write('<script type="text/javascript" src="app/views/Viewport.js"></script>');document.write('<script type="text/javascript" src="app/views/TOCPanel.js"></script>');document.write('<script type="text/javascript" src="app/views/HelpCarousel.js"></script>');document.write('<script type="text/javascript" src="app/views/HelpPanel.js"></script>');
// controllersdocument.write('<script type="text/javascript" src="app/controllers/help.js"></script>');
app/xerohelp.js
Ext.Router.draw(function(map) { map.connect("/help/home", {controller: 'help', action: 'home'}); map.connect("/help/:id", {controller: 'help', action: 'show'});});
app/routes.js
Ext.regApplication({ name: "XERO", defaultUrl: '/help/home', defaultTarget: "viewport", icon: 'resources/images/icon.png', glossOnIcon: false, region: "NZ", apiUrl: "http://help.stage.xero.com/api", launch: function() { this.viewport = new XERO.Viewport({ application: this }); } });
app/app.js
XERO.Viewport = Ext.extend(Ext.Panel, {
id: 'viewport', layout: 'card', fullscreen: true,
initComponent: function() { Ext.apply(this, { dockedItems: [{ xtype: "toolbar", dock : "top", title: "Help Center", itemId: "help-toolbar" },{ xtype: "toolbar", dock: "bottom", items: [{ xtype: "button", text: "Index", handler: this.onTOCButtonTap, scope: this },{ xtype: "spacer" }, { xtype: "button", text: "Switch Region", handler: this.onSwitchRegionTap, scope: this }] }] }); XERO.Viewport.superclass.initComponent.apply(this, arguments); }, ...
app/views/viewport.js
setTitle: function(title) { this.down("#help-toolbar").setTitle(title); }, onTOCButtonTap: function() { if(! this.tocPanel) { this.tocPanel = Ext.create({ xtype: "tocpanel" }); } this.tocPanel.show(); }, onSwitchRegionTap: function() { if(! this.regionPicker) { this.regionPicker = new Ext.Picker({ slots: [{ name : 'region', title: 'Switch Region', data : [ {text: 'New Zealand', value: "NZ"}, {text: 'Australia', value: "AUS"}, {text: 'United Kingdom', value: "UK"}, {text: 'Global', value: "INT"} ] }] }); } this.regionPicker.show(); }});
app/views/viewport.js
Ext.regController("help", {
home: function(request) { this.show(Ext.apply(request, { id: "home" })); }, show: function(request) { if(! this.helpCarousel) { this.helpCarousel = this.render({ xtype: 'helpcarousel' }); } XERO.viewport.setActiveItem(this.helpCarousel); XERO.viewport.setTitle("Loading..."); this.helpCarousel.loadHelpPage(request.id, request.historyUrl); }});
app/controllers/help.js
Ext.regModel("HelpFile", { fields: [ { name: "id", type: "int" }, "abstractText", "body", "heading", "helpPage", { name: "createdDateUTC", type: "date", format: "M$" }, { name: "success", type: "boolean" } ] /*, validations: [ { type: "presence", field: "helpPage" } ],
hasMany: [ { model: "Region", name: "regions" } ] */});
app/models/helpfile.js
Ext.regModel("TOC", { fields: [ "id", "helpPage", "href", "text" ], proxy: { type: 'scripttag', url: String.format("{0}/toc/", XERO.apiUrl), reader: { type: "tree", root: "items" } }});
(function() { new Ext.data.TreeStore({ storeId: "TOCStore", model: "TOC", autoLoad: true });}());
app/models/toc.js
XERO.views.HelpCarousel = Ext.extend(Ext.Carousel, {
cls: "cards", layout: "fit",
initComponent: function() { XERO.views.HelpCarousel.superclass.initComponent.apply(this, arguments); }, onRender: function() { XERO.views.HelpCarousel.superclass.onRender.apply(this, arguments); if(this.el) { this.mon(this.el, { tap: this.onTap, click: Ext.emptyFn, delegate: "a", stopEvent: true, scope: this }); this.mon(this, { cardswitch: function() { this.onHelpSwitch(this.getActiveItem().helpPage); }, scope: this }); } },
app/views/helpcarousel.js
loadHelpPage: function(id, href) { var bookmark; if(href.indexOf("$") != -1) { bookmark = href.right(href.length - href.indexOf("$") - 1); } var item = this.getComponent(id); if(item) { this.setActiveItem(item); if(bookmark){ item.scrollToBookmark(bookmark); } } else { var panel = this.add({ xtype: "helppanel", id: id, goToBookmark: bookmark, listeners: { helploaded: this.onHelpSwitch, single: true, scope: this } }); this.doComponentLayout(); this.setActiveItem(panel); } },
app/views/helpcarousel.js
app/views/helpcarousel.js onTap: function(e, target) { var id = target.getAttribute('xero:id'), type = target.getAttribute('xero:type'), bookmark = target.getAttribute('xero:bookmark'), url = target.getAttribute('xero:path'); if(type && type.toLowerCase() == "help") { if(bookmark) { this.getActiveItem().scrollToBookmark(bookmark); } else { Ext.dispatch({ controller: "help", action: "show", id: id, historyUrl: target.href }); } } else { if(target.href.toLowerCase().startsWith("mailto:")) { location.href = target.href; } else { window.open(target.href); } } }});
Ext.reg('helpcarousel', XERO.views.HelpCarousel);
XERO.views.HelpPanel = Ext.extend(Ext.Panel, {
cls: "help-panel", layout: "fit", scroll: "vertical", styleHtmlContent: true, goToBookmark: null, html: '<div class="loading-indicator"> </div>', initComponent: function() { this.addEvents("helploaded"); XERO.views.HelpPanel.superclass.initComponent.apply(this, arguments); }, onRender: function() { XERO.views.HelpPanel.superclass.onRender.apply(this, arguments); this.loadHelpPage(); },...
app/views/helppanel.js
loadHelpPage: function() { Ext.util.JSONP.request({ url: String.format("{0}/help/", XERO.apiUrl), callbackKey: "callback", params: { helpPage: this.id }, callback: function(doc) { this.helpPage = doc; this.title = doc.heading; this.update(this.helpPage.body); if(this.goToBookmark) { this.scrollToBookmark(this.goToBookmark); } this.fireEvent("helploaded", this.helpPage); }, scope: this }); },
scrollToBookmark: function(bookmark) { var el = this.getEl().down(String.format('a[name="{0}"]', bookmark)); if (el) { this.scrollIntoView(el); } }, }); Ext.reg('helppanel', XERO.views.HelpPanel);
app/views/helppanel.js
XERO.views.TOCPanel = Ext.extend(Ext.NestedList, { title: "Index", showAnimation: { type: "slide", direction: "up" }, floating: true,
initComponent: function() { this.store = Ext.getStore("TOCStore"); this.toolbar = {...}; XERO.views.TOCPanel.superclass.initComponent.apply(this, arguments); this.mon(this, { selectionchange: function(list, selection) { var record = selection[0]; if(record.get("leaf") === true) { Ext.dispatch({ controller: "help", action: "show", id: record.get("helpPage"), historyUrl: String.format("/help/{0}", record.get("helpPage")) }); this.hide(); } }, scope: this }); }});
app/views/helppanel.js
Touchable help...
going native
• Cross-platform• Open source• Extensible• Instantiates chromeless
web view• Adds JavaScript access
to native APIs
PhoneGap
Web App
etc…
Native APIs
• Device identification• Network access• Sensors• Camera/image sources• Contacts• File access
Everything else?
• HTML for layout
• JavaScript for accessing device APIs
• CSS for look & feel
• Offline storage for standalone clients
• Ajax, JSONP for syncing to the cloud– Runs on file:// protocol which is exempt from same-origin
policy
• Just use Sencha Touch!
PhoneGap Build
Tips & Tricks
Tools
• Browsers – Safari the best (unfortunately)• Web Inspector• RemoteJS (Android debugging)• Souders’ bookmarklets
– stevesouders.com/mobileperf• Jdrop
– jdrop.org
Object-oriented
• Use namespaces to define your library• Define components – code for reusability• Extend first, write plugins second (not at all
if possible)
Events rock!
• Use events to communicate between components
• Use event delegation
Override appropriately
• Do not edit the library files• DO NOT EDIT THE LIBRARY FILES!• Use an overrides file if you need to override
the framework• Do the same with CSS (but you should be
using cls, ui properties)
Define a directory structure
• Break your code into small files• Use build tools to compile for performance• Use sencha-touch-debug.js during dev (but
never prod!)• Keep the framework up-to-date – upgrade as
often as you can
Worry about performance
• Understand client-side performance rules & use them
• Latency bad• JIT compilers – compilation time relates to
size of file the method exists in• Keep DOM light• Destroy components that aren’t visible• concatenate, minify, compress!
Theming/Layouts
• Use SCSS• Remove unnecessary CSS by only
including required SCSS mixins• Understand XTemplate• Understand doComponentLayout
Sencha.com
Read the forums
Read the docs
Read the source!
Any questions?
www.xero.com