Sencha Inc. | HTML5 Apps

Idiomatic Layouts with Sencha Touch

Published Aug 01, 2011 | James Pearce | Tutorial | Medium
Last Updated Aug 10, 2011

This Tutorial is most relevant to Sencha Touch, 1.x.

Sencha Touch allows you to create applications that work on both mobile phone and tablet devices, as well as use layouts that cater to different screen sizes. In addition to the display differences between types of devices, users also have certain expectations about apps' user-interface conventions.

In this two-part series, we show how, with a single code-base, we can create an app which responds to these conventions, and which, through the use of the Sencha Touch 'application profiles' mechanism, delivers familiar user interfaces to both phone and tablet users. (If you want to skip ahead, part two is here).

The Basics

A modern trend in web design is to build web sites that are 'responsive' - meaning that they employ fluid layouts and techniques such as CSS media queries to adapt to a wide range of screen sizes. Quite apart from whether this allows us to deliver services tapered for particular user contexts is another matter - but the question this article sets out to answer is: can we do something similar for mobile and tablet web apps?

The good news is that Sencha Touch provides a subsystem especially for this purpose, using the Ext.Application class to define, and respond to, multiple 'profiles'. In this article, we'll show how to use application profiles to handle layouts for various screen configurations.

For the purposes of this walk-through, our goal will be be to deliver idiomatic UIs to both phone and tablet users, in both portrait and landscape modes. These are the four profiles that we will define and work with:

Portrait phone

Full-screen menu list with slide transition to detail pages. Back button in toolbar to return.

Landscape phone

Full-screen detail page. Button in toolbar shows floating menu list.

Portrait tablet

Full-screen detail page. Button in toolbar shows floating menu list.

Landscape tablet

Menu list docked to the left of detail page.

Note that Sencha Touch allows you to define as many different profiles as you'd like. You simply need to create the rules that allow the framework to decide which one it is in at any given point. You might like to create different profiles for different operating systems perhaps: removing app-defined back buttons when you know the device has a physical back button, for instance.

Our application is going to be a very simple one, but the principles should hold for more complex implementations. It's the 'Piet Mondrian' app - slightly contrived, admittedly - which shows information about four periods of the painter's life. The data set is going to be burnt into the app, but of course you could easily wire up an app like this to an online data source of some sort.

The key to making everything work is to define our app using the Ext.Application class. This is the standard way to construct consistent MVC-style applications, and although we're not strictly following a fully-fledged MVC pattern here, it's still good practice to use this as an architectural entry point (rather than just an ad-hoc Ext.onReady-style approach) for all Sencha Touch and Ext JS apps.

Before we go any further, you might like to read the detail in the Ext.Application API docs.

Also, you might want to take a sneak peak at the finished application here (with a smartphone, tablet, or WebKit desktop browser) so you know where we are heading. As we go through this tutorial, you can stay abreast of the code by following the step-by-step branches of its associated GitHub repo.

Application Structure

Let's quickly get our Mondrian app's architecture bootstrapped. Make yourself the folder structure shown below, or checkout or download the GitHub repo's first branch, named 1_structure. Copy or symlink the Sencha Touch SDK as touch within the lib directory.

The index.html file links to the Sencha Touch JavaScript the app's two files, app.js and data.js, and a custom stylesheet, mondrian.css:
<!DOCTYPE html>
<html>
    <head>
        <title>Mondrian</title>
 
        <script src="lib/touch/sencha-touch.js" type="text/javascript"></script>
 
        <script src="app.js" type="text/javascript"></script>
        <script src="data.js" type="text/javascript"></script>
 
        <link href="theming/mondrian.css" rel="stylesheet" type="text/css" />
 
    </head>
    <body></body>
</html>

For now, start with a simple application instance in app.js:

new Ext.Application({
    name: 'mondrian',
 
    launch: function() {
        var app = this;
 
        // construct UI
        var viewport = this.viewport = new Ext.Panel({
            fullscreen: true,
            layout: 'card'
        });
 
    }
});

The name property sets up a namespace for the application, and the launch function is our start-up code. In it, we create a handy reference to the application (so we can close over that variable in any other functions defined in launch), and instantiate a fullscreen root Ext.Panel called viewport. We make it a card layout since in one profile at least (for portrait phones), we'll be transitioning between two panes.

data.js you can leave empty for now.

In the theming directory, we'll be using Sass and Compass to compile the app's stylesheet from a single custom Sass file. We'll return to this in part two, but for now, either use the code from the 1_structure branch GitHub repo, or just copy in the standard sencha-touch.css file from the resources/css part of the SDK and rename it to mondrian.css.

If all goes well, your app should load up from the index.html file. Don't get too excited yet - it's nothing more than a light gray screen - but let's move on quickly.

Data and a Basic UI

We're going to display four pages of information about Mondrian, each with a title and some HTML. For this, we instantiate an Ext.data.Store, containing records of a very simple Ext.data.Model with id, title and content fields. We then declare in-line data for the content itself (with attribution to Wikipedia):

mondrian.stores.pages = new Ext.data.Store({
 
    model: Ext.regModel('', {
        fields: [
            {name:'id', type:'int'},
            {name:'title', type:'string'},
            {name:'content', type:'string'}
        ]
    }),
 
    data: [
        {id: 1, title: 'Introduction', content:
            "<p>Pieter Cornelis 'Piet' Mondriaan" +
            ...
        },
        {id: 2, title: 'Cubism', content:
            "<p>In 1911, Mondrian moved to Paris" +
            ...
        },
        ...
    ]
 
});

Note how we can use the mondrian.stores sub-namespace to put this store in. This was created automatically by the name: 'mondrian' configuration of the main Ext.Application. Needless to say, a typical application would probably pull data from an online source.

The full file is available in the GitHub repo's 2_data branch.

Let's also get a simple UI going. In the launch method, add the following component instantiations.

// the page that displays each chapter
var page = viewport.page = new Ext.Panel({
    cls: 'page',
    styleHtmlContent: true,
    tpl: '<h2>{title}</h2>{content}',
    scroll: 'vertical'
});

This is the detail page containing the main text of each page. It has a cls option to set a CSS class on the DOM element that we can use to lightly style it, and styleHtmlContent so basic HTML styling will be displayed. tpl is the template - simply the title and content fields of a model record - and then we want to ensure vertical scrolling of the page.

// the data-bound menu list
var menuList = viewport.menuList = new Ext.List({
    store: this.stores.pages,
    itemTpl: '{title}',
    allowDeselect: false,
    singleSelect: true
});
 
// a wrapper around the menu list
var menu = viewport.menu = new Ext.Panel({
    items: [menuList],
    layout: 'fit',
    width: 150,
    dock: 'left'
});
 
// a button that toggles the menu when it is floating
var menuButton = viewport.menuButton = new Ext.Button({
    iconCls: 'list',
    iconMask: true
});

The menu is a list of the chapter titles, so we use an Ext.List bound to our app's stores.pages store, with the appropriate, simple, itemTpl template. Since you can only view one page at a time, we set two selection mode flags accordingly. We also wrap the list itself in an Ext.Panel container since we will need to float it for the landscape phone and portrait tablet profiles. Lastly, we also need a button, decorated with a 'list' icon, that will toggle it on and off in that mode.

// a button that slides page back to list (portrait phone only)
var backButton = viewport.backButton = new Ext.Button({
    ui: 'back',
    text: 'Back'
});
 
// a button that pops up a Wikipedia attribution
var infoButton = viewport.infoButton = new Ext.Button({
    iconCls: 'info',
    iconMask: true
});

The back button is only used in the portrait phone profile and will slide the detail page back to the list. Its ui option gives us the left-hand arrow styling. Also, a simple information button will appear on all profiles and will pop up the Wikipedia attribution.

// the toolbar across the top of the app, containing the buttons
var toolbar = this.toolbar = new Ext.Toolbar({
    ui: 'light',
    title: 'Piet Mondrian',
    items: [backButton, menuButton, {xtype: 'spacer'}, infoButton]
});

The final part of the jigsaw is the lightly-colored Ext.Toolbar across the top of the application. It hosts our app's title, as well as the three buttons. We use xtype: 'spacer' to push the information button to the far right of the toolbar. Finally, dock the toolbar to the top of the root viewport, ensure the page is part of the card layout (and activate it):

//stitch the UI together and create an entry page
viewport.addDocked(toolbar);
viewport.setActiveItem(page);
page.update('<img class="photo" src="head.jpg">');

The final line just puts a picture of the esteemed artist onto the page panel. You could equally force the first record of the store to be live (the 'Introduction', for example), but this technique will act as a sort of splash screen until the user chooses one of the menu items.

If everything is in order, we should now have something on our screen(s):

This cosmetic car-crash (as well as the head.jpg image file) is available in the GitHub repo's 3_components branch.

Describing the Profiles

Apart from missing icons and an uninspiring blue look, our main issue is that all of the button components are showing on the toolbar - on all devices - and that our menu is nowhere to be seen. Let's define our four profiles and make sure things appear and disappear when they are supposed to.

The profiles are defined as the profiles property of our Ext.Application. Place the following configuration alongside (not inside) the launch function:

profiles: {
    portraitPhone: function() {
        return Ext.is.Phone && Ext.orientation == 'portrait';
    },
    landscapePhone: function() {
        return Ext.is.Phone && Ext.orientation == 'landscape';
    },
    portraitTablet: function() {
        return !Ext.is.Phone && Ext.orientation == 'portrait';
    },
    landscapeTablet: function() {
        return !Ext.is.Phone && Ext.orientation == 'landscape';
    }
}

For each profile we're targeting, we create a unique name and use that as a property containing a function which returns a boolean result. When the application starts up (and when orientation or screen size changes), Sencha Touch will evaluate these functions. When one returns a truthy value, that name becomes the current profile. It's important to note that JavaScript does not guarantee the order of properties in an object, so you can't be sure of the order in which the functions are called. Be careful to ensure that only one of the functions will return a truthy value at any given time.

Hopefully, the rules we've defined here are very self-explanatory. Note that, rather than explicitly testing Ext.is.Tablet, we're using !Ext.is.Phone. This means that the last two profiles will also apply to desktop browser windows too: useful for testing.

Now that we've defined the profiles, we need to get them to affect the components. Sencha Touch will call the setProfile method on each component within your application, if it's present, so we add such functions to the components as required. When modeling the appearance or disappearance of different controls for the four different profiles, you might want to check back with the table at the beginning of the tutorial.

// add profile behaviors for relevant controls
viewport.setProfile = function (profile) {
    if (profile=='portraitPhone') {
        this.setActiveItem(this.menu);
    } else if (profile=='landscapePhone') {
        this.remove(this.menu, false);
        this.setActiveItem(this.page);
    } else if (profile=='portraitTablet') {
        this.removeDocked(this.menu, false);
    } else if (profile=='landscapeTablet') {
        this.addDocked(this.menu);
    }
};

The viewport ('this') as a whole changes for each of the four profiles. The function is called with the name of the profile, so we check to see which is in play and act accordingly. (If you have any more profiles than this, you might prefer to use a switch statement.)

So what is going on here? Portrait phones need the menu to be an active card of the whole viewport, and horizontal phones need to have it removed (so it can float), and the page made active instead. Portrait tablets also need a floating menu, and landscape tablets need it docked. The false argument on the remove and removeDocked methods simply ensures that the menu is not destroyed in either case and is merely removed from its container, so it is ready to float.

It's worth keeping in mind which transitions are likely to occur between profiles, so you can keep this state machine terse. While you should certainly expect orientation changes between portrait and landscape profiles, you'll never see a phone turning into a tablet or vice versa. So in the code above, we only need to have pairs of profiles reversing each other's transitions.

In addition to the viewport as a whole, let's implement similar transitions for the other UI components. Firstly the menu, which we want to have sized and floating for landscape phone and portrait tablet profiles, and not floating (either as a card or a docked sidebar) for portrait phone and landscape tablet profiles.

menu.setProfile = function (profile) {
    if (profile=="landscapePhone" || profile=="portraitTablet") {
        this.hide();
        if (this.rendered) {
            this.el.appendTo(document.body);
        }
        this.setFloating(true);
        this.setSize(150, 200);
    } else {
        this.setFloating(false);
        this.show();
    }
};

Note that we hide the floating menu by default, so it appears only when the user clicks the list icon in the toolbar. (The appendTo line may seem a little cryptic, but it ensures that the element containing the list is at the top level of the DOM and can float freely, rather than having it constrained down inside the viewport element - which can adversely affect its positioning.)

Finally, two simple toggles: the menu button that needs to appear when we know the menu itself is floating, and the back button that only needs to appear when we're using the card transitions on the portrait phone profile:

menuButton.setProfile = function (profile) {
    if (profile=="landscapePhone" || profile=="portraitTablet") {
        this.show();
    } else {
        this.hide();
    }
};
 
backButton.setProfile = function (profile) {
    if (profile=='portraitPhone') {
        this.show();
    } else {
        this.hide();
    }
};

Of course, assuming you had the correct references, it would be possible to alter the entire UI from just one of these setProfile methods. However, by dictating the profile-specific behavior of each component within its own method, we've increased the encapsulation and maintainability of the app as the UI gets more complex.

Fire this up in phone and tablet simulators, and try orienting them. You should see something like this:

Hopefully you can see what is going on, based on the profile events we have implemented above. The code at this point is available in the GitHub repo's 4_profiles branch.

Now onto part two!

(PS: if you want to leave a comment on this article, please do so at the end of part two...)

Share this post:
Leave a reply

Written by James Pearce
James Pearce heads developer relations at Sencha. He is a technologist, writer, developer and practitioner, who has been working with the mobile web for over a decade. Previously he was the CTO at dotMobi and has a background in mobile startups, telecoms infrastructure and management consultancy. James is the creator of tinySrc, the WordPress Mobile Pack, WhitherApps, modernizr-server and confess.js, and has written books on mobile web development for both Wiley and Wrox.
Follow James on Twitter

Commenting is not available in this channel entry.