Site Meter
 
 

Full-height app layouts: Navigation and History

This is the third in a series of posts about web app layouts, i.e., giving your web-based application a UI comparable to a desktop or mobile/tablet app. Posts so far:

  1. Layout basics – A CSS technique for slicing up the browser window into arbitrarily nested panes both horizontally and vertically. You know, like a proper application, and not like an infinite-height document…
  2. Animated transitions – How to put more than one content block into a given pane, and then switch between them with hardware-accelerated animations
  3. This post – Keeping track of what content has appeared in each pane, so the visitor can navigate back and forwards. And supporting deep linking. And injecting new panes dynamically.

Disclaimer: To be clear, this post series just represents my own experiments in lightweight, flexible web app layouts and is not an official part of any Microsoft technology stack. No guarantees or warranties or SLAs, blah blah blah.

The goal

It’s very common on mobile/tablet-like UIs for users to navigate through some structure of content within a given pane. For example, in a tablet-like app, you’ll often want to have a left-hand pane representing navigation through a hierarchy, with the main section of the screen representing the currently selected item or folder:

image

So, you’ll want to be able to:

  • Keep track of where the user has been, letting them go back and forwards, with each pane able to maintain its own independent history.
  • Perform smooth animated transitions within each pane when any navigation event occurs.
  • Support deep-linking to arbitrary locations in your content structure
  • Fetch and render content dynamically

Basic navigation

Continuing from previous posts that introduced pane hierarchies and panes.js, it’s pretty easy to keep track of where a visitor has been. First you might set up a basic pane layout with a header/body/footer like this:

<div class="page">
    <div class="header row">
        Header will go here
    </div>
    <div class="body row">
        Body contents will go here
    </div>
    <div class="footer row">
        My footer. Could put icons here.
    </div>
</div>

… and then put multiple panes into the body row:

<div class="body row">
    <div id="location-continents" class="pane">
        <h3>Continents</h3>
        <ul>
            <li><a href="#america">America</a></li>
            <li><a href="#europe">Europe</a></li>
        </ul>
    </div>
 
    <div id="location-america" class="pane">
        <h3>Countries in the Americas</h3>
        <ul>
            <li><a href="#canada">Canada</a></li>
            <li><a href="#usa">USA</a></li>
        </ul>
    </div>
 
    <div id="location-canada" class="pane">
        <h3>Cities in Canada</h3>
        <ul>
            <li>Vancouver</li>
            <li>Toronto</li>
            <li>Edmonton</li>
        </ul>
    </div>
 
    <!-- ... etc ... -->
</div>

Now you can begin history tracking by adding a bit of JavaScript to instantiate a PaneHistory object and navigate to an initial pane:

// Navigation
var paneHistory = new PaneHistory();
paneHistory.navigate("location-continents");

… and perform pane navigations each time the visitor clicks on one of the links:

$("a").click(function (evt) { 
    var dest = (evt.srcElement || evt.target).href.split("#")[1]; 
    paneHistory.navigate("location-" + dest); 
    return false; 
});

This code will intercept clicks on the links, figure out which pane they are trying to get to, and then animate that destination pane sliding smoothly into the body area from the right.

If you also want to let the visitor go “back” through the pane’s history stack, add a button perhaps into the page header:

<div class="header row">
    <p><button class="goBack">&lt; Back</button></p>
</div>

… and handle clicks by instructing the PaneHistory instance to perform a reverse navigation animation:

$(".goBack").click(function () {
    paneHistory.back();
});

That’s it. Try it out. Live example:

Also: Run full screen (e.g., to try it on a phone)

Supporting the back/forward buttons and deep-linking

If your app will be deployed to the web (as opposed to using something like PhoneGap to package for an appstore), then you will almost certainly want to respect the browser’s native back/forward buttons and allow deep-linking to specific locations in your virtual navigation system.

There are many JavaScript libraries for working with browser history and the HTML5 pushState feature. Currently my favourite is history.js (license: New BSD) by Benjamin Lupton, because of its robustness, great pushState support, support for older browsers, and because it has no dependencies.

panes.js integrates with history.js, so once you’ve added a reference to history.js, you can start using UrlLinkedPaneHistory instead of PaneHistory. Specify the name of one or more URL parameters that the pane will use to represent what data it is showing. In this case, I’ll call my parameter “location”:

var paneHistory = new UrlLinkedPaneHistory({
    params: { location: 'continents' }
});

Then, update your paneHistory.navigate call so that it specifies which URL parameter is to be updated (in this case, my only parameter, “location”):

$("a").click(function (evt) {
    var dest = (evt.srcElement || evt.target).href.split("#")[1];
    paneHistory.navigate({ location: dest });
    return false;
});

And finally, since it no longer makes sense for “back” clicks to affect only a single pane (the URL history is global), update your “back” button handler so that it performs a global browser “back” navigation:

$(".goBack").click(function () {
    History.back();
});

… and you’re done. Now if you run the follow demo full-screen (click the link below the fake phone UI), you’ll see the URL update as you navigate around, and you can use the back/forward buttons (still getting the animated transitions), and can bookmark or refresh the page without losing your position. If your browser supports HTML5 pushState, for example recent versions of Chrome and Firefox, then your URLs will be updated with real querystring parameters as you go. If your browser doesn’t support pushState, history.js will gracefully degrade to using a URL hash.

Try it:

Note: If you want to be able to see the URL updating,
and to use the back/forward buttons, view this example full screen

Dynamically generating panes

If you do a view HTML source on either of the two previous demos, you’ll see that there’s a long list of pre-prepared <DIV> elements – one for each pane that you might visit (Europe, Canada, Vancouver, Toronto, etc….). That’s OK if your app only has a small and finite set of visitable panes (e.g., because those panes represent a fixed number of tabs), but it’s awkward if there are a lot of panes, and useless if the number is effectively infinite because panes represent navigation through data in some large external database.

Fortunately, this is easy to fix. Because panes.js doesn’t modify your DOM structure in any way, and requires no initialisation step to start using newly-inserted DOM elements, it composes perfectly with any external mechanism for updating the DOM. In this example, I’m going to use Knockout.js to inject new panes dynamically as data is fetched from some external source.

The trick here is to make the UrlLinkedPaneHistory a property of your Knockout view model. For example:

function AppViewModel() {
    this.paneHistory = new UrlLinkedPaneHistory({
        params: { location: null },
 
        // Making the set of history entries observable, so we can dynamically generate corresponding DIVs
        entries: ko.observableArray([]),
 
        // Each time the visitor navigates forwards, this will be called to fetch data for the new pane
        loadPaneData: function (params, callback) {
            // Here I'm loading data from some external source, asynchronously
            locationInfoService.getLocationInfoAsync(params.location, callback);
        }
    });
};
 
ko.applyBindings(new AppViewModel());

(Note: do a “view source” on the live example below if you want to see the finished code and what JS libraries you must reference to make all this work.)

Now the set of history entries is observable, we can ask Knockout to generate corresponding DIVs dynamically, by using a “foreach” binding:

<div class="body row" data-bind="foreach: paneHistory.entries">
    <div id="location-continents" class="pane scroll-y" data-bind="attr: { id: paneId }">
        <h3 data-bind="text: paneData.name"></h3>
    </div>
</div>

Notice that Knockout will assign an ID property to each pane <DIV> corresponding to the navigation parameters, so it can associate navigation events with the <DIV> you want to slide into view.

Of course, you don’t just want to display the “name” property of each item you’re visiting – you want to generate some more UI for each pane. Let’s generate a list of child locations that the user can navigate to:

<div class="body row" data-bind="foreach: paneHistory.entries"> 
    <div id="location-continents" class="pane scroll-y" data-bind="attr: { id: paneId }"> 
        <h3 data-bind="text: paneData.name"></h3> 
        <ul data-bind="foreach: paneData.children"> 
            <li data-bind="text: name, 
                            click: $root.navigate, 
                            css: { navigable: childLocations.length }"></li> 
        </ul> 
    </div> 
</div>

Each of the <li> elements there will try to invoke a viewmodel method called “navigate” when you click it. Implement that by adding a “navigate” method to your viewmodel:

function AppViewModel() {
    // Rest of class unchanged
 
    this.navigate = function (evt) {
        var destination = ko.dataFor(evt.srcElement || evt.target);
        if (destination.childLocations.length)
            this.paneHistory.navigate({ location: destination.id });
    } .bind(this);
};

Now the visitor will be able to navigate forwards through the hierarchy of locations. What about navigating backwards too? You can put a “back” button into the header pane, and use a Knockout binding to make its text update to show the name of parent location (e.g., “< Europe”):

<div class="header row">
    <p data-bind="with: paneHistory.currentData().parent">
        <button data-bind="click: $root.navigate">&lt; <span data-bind="text: name"></span></button>
    </p>
</div>

Done. Your visitor can now navigate through an arbitrarily deep hierarchy, with the data being pulled from the server as needed and dynamically rendered on the client. The browser’s back/forward buttons still work, deep linking works, and there are smooth animated transitions. And you wrote about 15 lines of JavaScript Smile. Have a go with it:

Note: If you want to be able to see the URL updating,
and to use the back/forward buttons, view this example full screen

What’s next?

Well, what are you interested in? Possible next posts in this series:

  • Using all this stuff to build a complete application with meaningful functionality
  • Making this kind of Single Page Application (SPA) work offline via HTML5 offline support
  • Packaging and selling such apps on mobile appstores via PhoneGap/Callback
  • Data access: editing collections of entities, and letting the user track, synchronise, and revert their changes

If you’re building single page applications, whether for mobile or desktop or both, please let me know what sort of technologies you’re using, what challenges you face, and what kinds of features you’d like to see baked into the ASP.NET/MVC stack.

Full-height app layouts: Animated transitions within panes

In the previous post, I showed a simple yet very useful and robust CSS technique for creating full-height layouts for web-based applications. To recap, it means you can subdivide both the width and height of the screen into a arbitrarily nested set of panes, each of which can scroll independently. This is useful whether you’re building apps for desktop browsers, for tablets, or for phones.

image

Not amazingly exciting just yet, but there’s more… :)

One of the other reasons for defining pane positioning using those particular CSS rules is that it becomes trivial to have multiple different content panes that can populate any given pane container, then to change which pane is visible within a container, and even to animate the transitions during those changes.

For example, here’s a layout involving a fixed header, a fixed footer, and two scrollable body regions (laid out using the same CSS rules as defined in the previous post):

<div class="header row">
    <h2>My header</h2>
</div>
<div class="body row">
    <div class="first pane scroll-y">
        <p>This is the first body view</p>
    </div>
    <div class="second pane scroll-y">
        <p>This is the second body view</p>
    </div>
</div>
<div class="footer row">
    <p>My footer. Could put icons here.</p>
</div>

This will lay out as follows, with the first body pane and second body pane occupying the same area on the screen:

image

Then, you can trivially switch between the two body panes just by controlling their visibility (e.g., by using $(".first.pane").hide() and $(".second.pane").show()).

Animating the transitions

Instead of just hiding and showing panes instantly, you can cause them to fade in/out or even to slide in and out. This pretty well replicates an aspect of the UI experience familiar to users of touch-based smartphones and tablets.

However, JavaScript-based animation mechanisms (such as $.animate in jQuery) aren’t as smooth as I’d like. They tend to give low frame rates, as the browser has to run custom JavaScript to compute the positions of elements for each frame. Fortunately since we’re controlling the layout purely using CSS, it’s both easy and robust to use pure CSS 3 animated transitions, giving silky-smooth results on devices that support hardware accelerated CSS transforms (e.g., iPhone and iPad), exactly like a native application.

To make this easy to reuse, I created a small bit of JavaScript boilerplate (about 1kb gzipped) that exposes functions for triggering pane transitions, being sure to take full advantage of hardware acceleration where available. For example, you can write:

// Initial pane visibility
$(".first.pane").showPane();
 
// Navigation between panes
$(".first.pane button").clickOrTouch(function () {
    $(".second.pane").showPane({ slideFrom: 'right' });
});
$(".second.pane button").clickOrTouch(function () {
    $(".first.pane").showPane({ slideFrom: 'left' });
});

… and then if you click any button in the first body pane, the second body pane will slide in smoothly from the right, and vice-versa. Note that although this code uses looks as if it uses jQuery, it doesn’t – it actually uses XUI instead (a very lightweight JavaScript framework that implements a fraction of the jQuery API in a fraction of the space), but you could equally do similarly with jQuery.

The boilerplate code offers the following transitions:

  • slideFrom – causes the target pane to slide in from the specified direction, and simultaneously the current pane to slide out in the opposite direction
  • coverFrom – causes the target pane to slide in from the specified direction. The current pane stays still, getting covered over by the target pane.
  • uncoverTo – causes the current pane to slide out in the specified direction, revealing the target pane underneath
  • default – instantly shows the target pane and hides the current pane

… and it works on IE7+ (including WP7 Mango), iOS, Firefox, Chrome, Safari, Opera, and probably others. For browsers that support hardware-based CSS3 transforms, like Safari on iOS 4+, it will be used and is impeccably smooth; others will use unaccelerated CSS3 transforms, and for those that don’t support CSS3 transforms, it falls back on JavaScript-based animation.

Also notice the “clickOrTouch” event – on browsers that support touch events (e.g., iOS Safari), this fires the instant your finger comes into contact with the screen (and hence is noticably faster than a regular click), whereas on non-touch devices, it’s equivalent to a regular click (i.e., when you release the mouse).

Runnable examples

Here’s a phone-styled example just showing how you can transition the contents within a given pane, or you can transition the contents of the entire screen. Note that this is an iframe you can interact with, not just a pretty picture :)

Also: Run full screen (e.g., to try it on a phone)

Similarly, here’s a tablet-styled example showing how transitions work just the same in arbitrary sub-panes as well.

Also: Run full screen (e.g., to try it on a tablet device)

Note: To be clear, I’m not starting a new open source project here. These are experiments in mobile-friendly app layouts – I’m blogging the results just in case it’s useful to someone else! In the next post, I’ll focus on managing navigation history within each pane, so you can easily let a visitor go “back” and “forwards”.

Full-height app layouts: A CSS trick to make it easier

In HTML layouts, there’s a fundamental difference between width and height. The natural state of affairs is that your page’s width is constrained to the width of the browser window, but its height is unconstrained – it grows however tall is necessary to contain its contents.

image

That makes sense for documents, but it’s a pain for application UIs. Native application UIs tend to slice up the screen both horizontally and vertically into a nested set of panels, some of which may scroll/resize, others being docked against particular edges of their parent panes.

image

So, what’s the robust way, with HTML/CSS, to set up a nested collection of panes that exactly divide both the width and height of the browser window?

Hang on, didn’t we have this one solved back in 1999?

Hmm… this reminds me of something… I remember: the <frameset> tag from HTML 4. Yes, HTML frames do divide the browser window both horizontally and vertically, exactly consuming the available screen area. So why don’t we use them any more? There are loads of reasons, including:

  • What you’re building is logically one page, but technically each frame is a separate HTML document, which makes interactions between them so much more complex
  • It’s not really practical to support deep-linking to (i.e., bookmarking) particular UI states
  • Mobile devices and tablets have very limited support for HTML 4 frames. iOS in particular requires the user to use two-fingered scrolling within panes. This is UX death.

OK, so what’s the 21st century solution?

I’ve used lots of hacks and tricks over the years to get columns and panes into my web applications, often involving JavaScript, $(…).height(…), window.body.clientHeight, and the onresize event. Ugh. Fragile and messy. But at long last this week I learned there is actually an elegant and robust way to set up nested exact-height panes with pure CSS, and it works on all browsers back to IE 7, even on current mobile browsers that don’t support position:fixed.

You know that if you set position: absolute on an element, then you can make it appear at a specified distance from the top, left, right, or bottom from its parent element. Well, it turns out that you can specify both top and bottom, or both left and right, and then it will dock against both edges and always resize to match its parent’s dimensions.

Let’s define some generic CSS rules for panes:

/* Generic pane rules */
body { margin: 0 }
.row, .col { overflow: hidden; position: absolute; }
.row { left: 0; right: 0; }
.col { top: 0; bottom: 0; }
.scroll-x { overflow-x: auto; }
.scroll-y { overflow-y: auto; }

Now it’s easy to set up a fixed-height header, variable-height body, and fixed-height footer:

<body>
    <div class="header row">
        <h2>My header</h2>
    </div>
 
    <div class="body row scroll-y">
        The body
    </div>
 
    <div class="footer row">
        My footer
    </div>
</body>

Of course, you also have to configure the heights and positions of the three rows by using a bit more CSS:

.header.row { height: 75px; top: 0; }
.body.row { top: 75px; bottom: 50px; }
.footer.row { height: 50px; bottom: 0; }

The result? It’s the classic mobile phone app layout. Screenshot with some colours and content added for interest:

image

Try it in your desktop or mobile browser here. Works on IE7+.

More nesting (or, tablet-style layouts)

The reason for setting up the generic CSS rules in that way is that it makes it easy nest panes arbitrarily. For example, you could split the main viewport into two vertical columns:

<body>
    <div class="left col">
        Left col here    
    </div>
    <div class="right col">
        Right col here
    </div>
</body>

… and then subdivide the right column so it has a fixed header, variable-height body, and fixed footer, just by putting some more row divs inside it:

<div class="right col">
    <div class="header row">
        View or edit something
    </div>
    <div class="body row scroll-y">
        Here’s some content that can scroll vertically
    </div>
    <div class="footer row">
        Some status message here
    </div>
</div>

Here’s the CSS to configure the widths/heights/positions of those panes:

.left.col { width: 250px; }
.right.col { left: 250px; right: 0; }
.header.row { height: 75px; line-height: 75px; }
.body.row { top: 75px; bottom: 50px; }
.footer.row { height: 50px; bottom: 0; line-height: 50px; }

Going further, in the right-hand pane’s body, you could also nest a horizontally-scrollable row:

<div class="body row scroll-y">
    <p>Here's a horizontally-scrollable thing:</p>
 
    <div class="scroll-x">
        Any content in here, if it's too wide, becomes independently scrollable
    </div>
 
    <p>That's enough - bye.</p>
</div>

The result of all this? Well, it’s a structure like this:

image

… but that’s pretty boring to look at, so here’s a version where I threw in a rough effort of some iPad-like styling:

image

Here’s a live example. It still renders correctly on very small screens (like a WP7 or iPhone) but this 2-column layout really needs a wider screen to be usable.

Enabling touch scrolling

The scrolling looks and works fine on a desktop browser, but on a mobile browser it varies:

  • On WP7, you’ll see no scrollbars, but you’ll get one-finger touch scrolling, without the lovely inertia/momentum effect. This is kind-of OK, though imperfect.
  • On iOS, you’ll see no scrollbars, and it requires two-finger scrolling (which is horrible), and it won’t use the inertia/momentum effect either. Badness.
  • There are similar problems on Android

With iOS 5, it will be possible to enable fluid, native touch-based momentum scrolling to our divs just by making a tiny tweak to the CSS, thanks to the new “touch scrolling” feature:

.scroll-x, .scroll-y { -webkit-overflow-scrolling: touch; }

I really hope this catches on and becomes a standard.

But in the meantime, for visitors on other mobile OSes, and until iOS 5 becomes prevalent, you can use the open-source iScroll library that provides one-finger momentum scrolling for Webkit (iOS and Android) and Mozilla browsers.

Enabling momentum scrolling on any given element requires only one line of JavaScript (assuming you’ve referenced iScroll.js):

new iScroll(theElementYouWantToEnableItFor);

For my example, I used the following block of JavaScript, which enables touch scrolling on all the .scroll-x and .scroll-y elements:

<!-- Touch scrolling -->
<!--[if !IE]><!-->
<script src="script/iscroll.js" type="text/javascript"></script>
<script type="text/javascript">
    var xScrollers = document.getElementsByClassName("scroll-x");
    for (var i = 0; i < xScrollers.length; i++)
        new iScroll(xScrollers[i], { vScroll: false });
 
    var yScrollers = document.getElementsByClassName("scroll-y");
    for (var i = 0; i < yScrollers.length; i++)
        new iScroll(yScrollers[i], { hScroll: false });                
</script>
<!--<![endif]-->

You could do this in fewer lines if you’re using a library like jQuery or XUI (which is a tiny implementation of a small part of the jQuery API surface, intended for mobiles). Here’s the resulting mobile-style scrollbar:

image

Of course, to see the momentum effect, you’ll need to run it in your Chrome/Safari/Firefox browser.

Credits: Thanks to Rob Swan and FellowshipTech for their articles and projects where I found the CSS positioning trick that underlies this approach to exact-height layout.