Dela via


Chapter 9: Navigation

Introduction | Supporting the Single-Page Interface Pattern - Browser History and the Back Button, The BBQ Plug-in | Modifying Hyperlinks - Modifying URLs When the Application Loads, Modifying URLs When the User Activates a Link | Responding to User Actions | Animating Screen Transitions - The Layout Manager, Coordinating Transitions, Animating Vehicle Tiles, Calculating Coordinates | Summary | Further Reading

Introduction

This chapter describes how the Mileage Stats Reference Implementation (Mileage Stats) manages navigation as users interact with the application, and how it uses animated transitions from one state to another to provide an attractive and immersive user experience.

Traditional web applications and websites implement navigation between pages and states by loading separate pages from the server. Hyperlinks and other controls such as buttons typically initiate a request to the server, which sends back the next page or an updated version of the same page. This is the static or server-rendered model.

In some cases the pages may contain client-side code that adds interactivity to the page without initiating a request to the server, or by reloading only a part of the page. This is the hybrid model; it provides islands of interactivity within the site that do not require a full-page reload.

Mileage Stats uses the hybrid model. However, the majority of user interactions occur within a single page, portions of which are updated with data retrieved from the server using background requests. This is the Single-Page Interface (SPI) pattern. When a user performs an action such as selecting a hyperlink or clicking a button, the application modifies the current web page without reloading the entire page, resulting in a more responsive, less jarring experience.

Adherence to the SPI pattern presents a number of navigation challenges. These challenges include managing the browser's history mechanism so that the back button works as expected, providing direct access to a specific layout or state of the page (deep linking), and implementing anchors that allow users to bookmark the application so that it displays in a specific state.

Fortunately, these challenges can be addressed by using some of the features of modern browsers, by using JavaScript, and by taking advantage of some useful libraries to progressively enhance the way that browsers manage navigation. The approach used in Mileage Stats, and described in this chapter, augments the way the browser handles history and hyperlinking.

In a few cases, navigation in Mileage Stats triggers an animation sequence that transitions between screen layouts. Performing these layout changes requires coordination of the necessary transitions, manipulation of the document object model (DOM) elements that make up portions of the screen, and appropriate modularity to manage the complexity of the animation code.

In this chapter you will learn:

  • How to keep your users within a single-page interface
  • How to manage hyperlinks and respond to user actions
  • When to rewrite URLs and when to handle the click event
  • How to ensure that deep linking, bookmarking, and the back button work as expected
  • How to coordinate and animate screen transitions

Supporting the Single-Page Interface Pattern

To support the SPI pattern, an application must handle user interactions that would, in a traditional web application, cause a request to the server and a full reload of the page to occur. The way that the interaction is handled for both hyperlinks and buttons varies depending on the behavior and requirements of the link. This figure shows the types of links used in Mileage Stats.

Comparison of different kinds of links used in Mileage Stats

Hh404077.dc1cc82c-8100-4e12-993f-e163213c7d86(en-us,PandP.10).png

As you can see, there are three different types of links. Links in the vehicle tiles on the left must specify the identifier of the vehicle to which that tile applies, which doesn't change throughout the user's session. Links in the vehicle details panel on the right, which change based on the selected vehicle, must specify both the identifier of the currently selected vehicle and the currently selected view tab (Details, Fill ups, or Reminders) so that deep linking and bookmarks work correctly.

You can also see that the page includes links that are not added to the browser's history list. When links are added to history, users can return to the previous page or any page in the list using the back button or the browser's history explorer. From a user experience perspective, it is not likely users would want to use the back button to return to a previously viewed reminder or fill up on the same view tab.

Applications that partially or fully apply the SPI pattern create page transitions by using JavaScript to override default user navigation actions and control the resulting action. Even though the application is taking over responsibility for the action, it must help the browser understand the important details of the action so the browser will continue to behave as expected.

This partnership between your code and the browser isn't difficult to implement if you have some understanding of how the browser works. Sometimes hyperlinks can be rewritten after the page loads, and other times the user action is better handled dynamically, when the user clicks the link. In either case, you have full control over the response to the user, and can include support for deep linking and bookmarking, as well as for the back button.

Browser History and the Back Button

On a traditional hyperlinked website, the browser can adequately manage the history list and behavior by itself. Each time the browser is instructed by the user or by code to navigate to a unique URL, it registers the URL as a new entry in the browser's history. Clicking the browser's back and forward buttons causes the browser to load the corresponding URL from the history list.

These URLs can contain a fragment identifier (often known as a hash). This is the part after the # delimiter character in the URL. For example, in the URL /article.html#intro, the fragment identifier is intro. This may represent a named location on the page (the name of an anchor element in the page) to support deep linking and bookmarking, or a value that corresponds to the state of the page. A fragment identifier may also include a series of name/value pairs. For example, the following URL contains a fragment that includes two name/value pairs, where layout equals details and vid equals 4.

http://mileagestats.com/Dashboard#layout=details&vid=4

Mileage Stats uses the name/value pairs in the fragment identifier to manage state related to the view tab and the vehicle being displayed.

Note

Do not confuse the fragment identifier with the query segment of a URL. The query segment (or query string) is the part that follows the ? delimiter. It usually contains name/value pairs that the server uses to generate the appropriate output. For more information about the structure of URLs, see "RFC 3986, Uniform Resource Identifier (URI): Generic Syntax" at http://www.ietf.org/rfc/rfc3986.txt.

Most URLs cause the browser to perform a full-page refresh, which conflicts with the requirements of the SPI pattern. However, if the URL added to the history is identical to the previous URL except for the fragment identifier, the browser will not perform a full-page refresh when the user presses the back button. Therefore, URLs such as the one shown above, which refers to the Dashboard resource on the server, and vary only in the content of the fragment identifier, do not result in multiple entries being added to the browser's history list.

When a user selects one of these links, the client-side handler for the click event can perform the necessary actions to update the user interface (UI), but the browser does not treat it as a different URL or add it to the history list.

In some cases, however, it is necessary for URLs to be added to the history list even if they vary only in the content of the fragment identifier (and would not, therefore, be added automatically). Mileage Stats uses the Back Button & Query (BBQ) plug-in for jQuery to achieve this, as well as to help make the appropriate modifications to the URL where required.

The BBQ Plug-in

According to the BBQ project page (http://benalman.com/projects/jquery-bbq-plugin/), the BBQ plug-in "… leverages the HTML5 hashchange event to allow simple, yet powerful bookmarkable #hash history. In addition, jQuery BBQ provides a full deparam method, along with both hash state management, and fragment / query string parse and merge utility methods."

Note

The HTML5 hashchange event is raised from the window object each time any values in the fragment identifier of the current URL change; hashchange is supported in Windows® Internet Explorer® versions 8 and 9. The deparam method can extract the values of name/value pairs from the fragment identifier and coerce them into the appropriate types. Documentation for the BBQ plug-in is available at http://benalman.com/code/projects/jquery-bbq/docs/files/jquery-ba-bbq-js.html.

Mileage Stats only uses a fraction of BBQ's features. For some navigation scenarios, BBQ is used to parse and merge the fragment identifier values when creating or updating hyperlinks that include the fragment identifier. For hyperlinks that must change based on the state of the application, such as the selected vehicle link, BBQ is used in the click event handler to dynamically modify the URL. As you will see in "Responding to User Actions" later in the chapter, BBQ also helps when the browser doesn't support the hashchange event.

Modifying Hyperlinks

Mileage Stats uses two different approaches to modify the URLs sent from the server. The approach depends on the lifetime of the hyperlink. If the URL of the hyperlink doesn't change throughout the lifetime of the user's session, such as the hyperlinks in the vehicle tiles (these look like buttons), the URL is modified in the anchor tag's DOM element when the page first loads.

Note

As you will see in the next section, this approach allows the application to support browsers that do not have JavaScript enabled. This causes the application to send a request for a full-page reload each time the user clicks a link. This is a form of progressive enhancement that provides an improved user experience when JavaScript is enabled.

If the hyperlink's URL is based on the current state of the application, such as the currently selected vehicle, the click event is handled and the URL is applied dynamically, as opposed to using the URL created on the server, which would cause a full-page post back.

The following sections describe the way Mileage Stats implements these different approaches for managing navigation and browser history.

Modifying URLs When the Application Loads

Some of the links in Mileage Stats use a URL that does not change throughout the lifetime of the user's session. These URLs include a fragment identifier that contains the identifier of the vehicle to which they apply. For browsers that do not support JavaScript, Mileage Stats supports a traditional static model of navigation, where every user interaction requires the server to create the updated page. To achieve this, each of the hyperlinks sent from the server refers to a specific resource on the server. For example, the URL that the Details view uses to display the details of the vehicle with identifier 4 is sent in the following REST-style format.

http://mileagestats.com/Vehicle/Details/4

If the browser has JavaScript enabled, these hyperlinks are modified on the client to use the fragment identifier instead. This ensures the correct behavior of the back button. The link shown above will be converted into the following format so that all of the state information is included in the fragment identifier. "Dashboard" is the location Mileage Stats uses as its SPI.

http://mileagestats.com/Dashboard#layout=details&vid=4

This approach means that deep linking, bookmarking, and the back button will continue to work as expected because you are changing the URL in a way the browser understands.

The Details, Fill ups, and Reminders buttons on the individual vehicle tiles are examples of hyperlinks that must be updated only once, when the user starts the application. The code must convert the URL from the REST-style format delivered by the server to the format where the state information is included as name/value pairs in the fragment identifier.

A section of the server-side code that creates the vehicle tiles is shown below. It uses the Url.Action method to generate the REST-style URLs for the links.

<!-- Contained in _TemplateVehicleList.cshtml -->
<a href="@Url.Action("List", "Reminder")/${VehicleId}" 
   data-action="vehicle-reminders-selected" 
   alt="Reminders" title="Reminders">
       <div class="hover"></div>
       <div class="active"></div>
       <img alt="Reminders" 
           src="@Url.Content("~/Content/command-reminders.png")" />
       <div class="glass"></div>
</a>

This creates a URL for the hyperlink in the format http://mileagestats.com/reminder/list/4. Notice that the hyperlink contains a data-action attribute, in this case with the "vehicle-reminders-selected" value.

Note

Attributes that begin with the prefix data- are treated as metadata. The part of the name after the hyphen can be used for querying the value of the attribute using jQuery's data method.

The client-side code in Mileage Stats includes the _adjustNavigation function, part of which is listed below, to modify the URLs in hyperlinks. The code takes advantage of jQuery's support for HTML5 data- attributes to retrieve the value of the data-action attribute and store it in a variable named action. Next, it obtains the identifier of the vehicle from the widget's options collection, then uses the BBQ getState method to read the fragment identifier (if one exists) and store it in an object variable named state. If there is no fragment identifier (which will be the case when the application starts) it stores an empty object literal instead.

// Contained in mstats.vehicle.js
_adjustNavigation: function () {
    var that = this;
            
    this.element.find('[data-action]').each(function () {
        var $this = $(this),
            action = $this.data('action'),
            vehicleId = that.options.id,
            state = $.bbq.getState() || {},
            newUrlBase = mstats.getBaseUrl();

        state.vid = vehicleId;
        switch (action) {
            case 'vehicle-details-selected':
                state.layout = 'details';
                break;
            case 'vehicle-fillups-selected':
                state.layout = 'fillups';
                break;
            case 'vehicle-reminders-selected':
                state.layout = 'reminders';
                break;
            case 'vehicle-add-selected':
                state.layout = 'addVehicle';
                state.vid = undefined;
                break;
        }
        $this.attr('href', $.param.fragment(newUrlBase, state));
    });
},

The code then sets the value of a variable named newUrlBase by calling the getBaseUrl function. This is a helper function defined in mstats.utils.js that retrieves the current base URL without the fragment identifier. The value will usually be /Dashboard in Mileage Stats (but may change depending on how the site is deployed).

Note

The base URL will be different if the site is deployed to a virtual directory versus being deployed to the root of the web server. For example, if the virtual directory is mstats, the base URL (without the domain name) would be /mstats/Dashboard. This is why relying on the server-side Url.Action function is essential, as it takes into account how the application is hosted.

Next, the code sets the vid property of the state object to the vehicle's ID, and then uses a switch statement to set the layout property (when adding a vehicle, the vid property is set to undefined). Finally, the code updates the href attribute of the hyperlink to the required URL using the BBQ fragment method (which extends jQuery.param) to merge the values in the state object with newUrlBase. The result is a URL that is changed from http://mileagestats.com/reminder/list/4 to http://mileagestats.com/dashboard\#layout=reminders\&vid=4.

The preceding section showed how to use the BBQ plug-in and HTML5 data- attributes to modify hyperlinks so they include a fragment identifier that defines the state. This is fine if the links are not likely to change (or will not change very often). However, if the URL can be determined only when the link is activated, it is usually more appropriate to handle the click event.

When a user activates one of these links, the client-side handler for the click event can perform the necessary actions to update the UI and prevent the default click behavior from sending the request to the server. Alternatively, if required, the code can initiate navigation and page reload by setting the location property of the browser's window object within the event handler or in code that is executed from the event handler.

The vertical accordion links for Details, Fill ups, and Reminders are examples of links that change each time a different vehicle is selected because they must include the identifier of the selected vehicle. The following listing shows a section of the server-side code that creates the HTML for the hyperlink in the Fill ups view tab.

<!-- Contained in _TemplateFillups.cshtml -->
<a class="trigger" 
   href="@Url.Action("List", "Fillup")/${VehicleId}" 
   data-info-nav="fillups"></a>

This creates a URL for the hyperlink in the format http://mileagestats.com/fillup/list/1. Notice that the hyperlink contains a data-info-nav attribute, in this case with the "fillups" value.

The info pane widget that implements this section of the UI is responsible for handling the click event of these accordion links and initiating the required transition. The code in the _bindNavigation method (shown below) creates a delegate on the click event, which uses the widget's name as the namespace, for all elements that include a data-info-nav attribute.

Note

Using the widget's name as the namespace and appending it to the event allows the delegation to automatically be cleaned up when the widget is destroyed.

Inside the delegate, the code uses the jQuery data method to read the value of the data-info-nav attribute into a variable named action, and then passes this value to another method named _setHashLayout. It also prevents the default behavior of the hyperlink from initiating navigation.

// Contained in mstats.info-pane.js
_bindNavigation: function () {
    var that = this; // closure for the event handler below
            
    this.element.delegate('[data-info-nav]', 'click.infoPane', function (event) {
        var action = $(this).data('info-nav');
        that._setHashLayout(action);
        event.preventDefault();
    });
},

The _setHashLayout method, also defined within the info pane widget, is responsible for setting the layout according to the user's selection. It uses the BBQ getState method to retrieve the existing navigation details, which are stored as name/value pairs in the fragment identifier of the current URL, as an object named state. After setting the layout property for the new layout, it uses the pushState method to modify the fragment and update the browser's address bar. The value 2, passed as the second argument, tells the pushState method to completely replace any existing fragment identifier values with the contents of the first argument.

// Contained in mstats.info-pane.js
_setHashLayout: function (newLayout) {
    var state = $.bbq.getState() || {};
    state.layout = newLayout;
    $.bbq.pushState(state, 2);
},

The result is a URL that is changed from http://mileagestats.com/fillup/list/1 to http://mileagestats.com/dashboard\#layout=fillups\&vid=1.

Although the server isn't required to do anything special to support these URLs, the client must perform some specific steps to respond to user actions.

Responding to User Actions

At this point, as explained in the preceding sections of this chapter, the URLs in the hyperlinks or in the click event handlers have been updated with the required fragment identifier values. Links where the URL does not change during the session have been updated by code that modifies the anchor tag's href attributes when the main SPI page loads. Links where the URLs do change have a delegate attached to the click event, and this delegate updates the browser's location with the modified URL.

The next stage is to write code that responds when a user clicks on a link. This is done by handling the hashchange event, which is raised when the user clicks a link that contains a fragment identifier, or when code programmatically sets the location property of the window object, which is what BBQ's pushState method does, as shown in the previous code example. The hashchange event can be used for applications that need to maintain a history of user actions without reloading the complete page.

Mileage Stats uses the BBQ plug-in to manage the browser's history in a uniform manner that works across all browsers. When BBQ detects browser support for the hashchange event, it uses the browser's implementation. When it doesn't detect support for the hashchange event, it uses its own implementation of the event. Internet Explorer 8 and Internet Explorer 9 support the hashchange event.

Note

In addition to the hashchange event, HTML5 also defines a mechanism for managing browser session history without using the fragment identifier, which adds pushState and replaceState methods to window.history. Internet Explorer 9 supports this mechanism. You should consider using a library such as Modernizr to determine if the browser supports the HTML5 history mechanism. The BBQ plug-in and similar libraries can still be used for browsers that do not support it.

The hashchange event handler in Mileage Stats, shown below, is defined in mstats.layout-manager.js. When the hashchange event occurs, the handler uses BBQ's deparam method to deserialize the name/value pairs in the fragment identifier into a state object. The true argument instructs the deparam method to coerce numbers true, false, null, and undefined to their actual values rather than the string equivalent. This avoids the need to use the parseInt function to convert the vid property from a string to a number.

// Contained in mstats.layout-manager.js
_subscribeToHashChange: function() {
    var that = this;
    $(window).bind('hashchange.layoutManager', function() {
        var state = $.deparam.fragment(true);
        that._changeLayout(state);
    });
},

The handler then updates the page to the required layout by calling the _changeLayout method, shown next, which is also defined in mstats.layout-manager.js. This method uses the state argument that is passed to it to set the widget's layout option and to navigate to the appropriate layout.

// Contained in mstats.layout-manager.js
_changeLayout: function(state) {
    this._setOption('layout', state.layout || 'dashboard');

    this._setupInfoPaneOptions(state);
    switch (this.options.layout) {
        case 'dashboard':
            this._header('option', 'title', 'Dashboard');
            this._goToDashboardLayout();
            break;
        case 'charts':
            this._header('option', 'title', 'Charts');
            this._goToChartsLayout();
            break;
        case 'details':
            this._setHeaderForSelectedVehicle('Details');
            this._goToDetailsLayout();
            break;
        case 'fillups':
            this._setHeaderForSelectedVehicle('Fill ups');
            this._goToDetailsLayout();
            break;
        case 'reminders':
            this._setHeaderForSelectedVehicle('Reminders');
            this._goToDetailsLayout();
            break;
    }
},

The call to the _setOption method uses either the layout that is passed in, or it defaults to the Dashboard if no layout is specified. The code then executes the _setupInfoPaneOptions method (not shown here), which sets the selectedVehicleId option on the vehicle list and info pane widgets, and sets the activePane option on the info pane widget. The code then uses a switch statement based on the value of the layout option to set the header text option of the widget (in some cases using a private method within the widget) and then it calls another private method to begin animating the transition to the required layout. You will see an example of these animation methods in the next section of this chapter.

Animating Screen Transitions

Mileage Stats uses a widget named layoutManager both for managing navigation and for controlling the animation sequences that are triggered by navigation. For example, if you are on the Dashboard and you select Details for a specific vehicle, you see an animated sequence that shifts the summary pane off the left side of the screen, moves the info pane onto the screen from the right, and adjusts the vehicle list so that the currently selected vehicle is prominently displayed. This requires coordination of the summary, info pane, and vehicle list widgets.

Note

For applications that do not use animations, implementing a layout manager is unnecessary. However, animations can help provide a great user experience by making the application appear more responsive and by allowing users to maintain context as they navigate the application.

The Layout Manager

Early in the development of Mileage Stats, it became clear that something needed to have responsibility for coordinating between the dashboard, details, and charts layouts. Without centralized coordination, each widget that makes up a particular layout would need to know the current layout and when to transition to the next layout. This central coordinator, the layout manager, knows how and when to instruct the appropriate widgets to move to the correct layout, which reduces coupling and simplifies the implementation. By using a layout manager widget, each of the other widgets only needs to know how to perform the transition, not when to do so or what the other widgets must do to complete the transition.

The layout manager is initialized in mileagestats.js with references to all the necessary widgets (such as the summary pane, vehicle list, and info pane) and it knows when to take action based on subscriptions to the following two types of events:

  • Global events. These are events that are not constrained to an isolated part of the application. They are published to the publish/subscribe object and subscribed to with the layout manager's _subscribeToGlobalEvents method. You can learn more about this method in "An Example of Publishing" in Chapter 8, "Communication."
  • hashchange events. These events are raised through user navigation and handled in the _subscribeToHashChange method, as described in the previous section of this chapter.

When these events are raised, the layout manager uses its _changeLayout method, described in the previous section of this chapter, to invoke methods directly on the widgets that will take part in the animation, and to update the header widget with the appropriate title based on the layout and selected vehicle. To learn more about how the layout manager invokes these widgets, see "Method Invocation" in Chapter 8, "Communication."

Coordinating Transitions

One of the more complex transitions is to and from the dashboard and details layouts. The remainder of this section deals with the specifics of this transition.

Transitioning from dashboard to details

Hh404077.0f3001db-78a2-4072-9314-108fc244819f(en-us,PandP.10).png

The following code shows one of the methods that the layoutManager widget uses to update the layout in response to a navigation event. This example, the _goToDetailsLayout method, is used to navigate to the vehicle details layout.

// Contained in mstats.layout-manager.js
_goToDetailsLayout: function() {
    this._charts('moveOffScreenToRight');
    this._summaryPane('moveOffScreen');
    this._infoPane('moveOnScreenFromRight');
    this._vehicleList('moveOnScreen');
    this._vehicleList('goToDetailsLayout');
},

The code calls methods in each of the widgets to move them to the appropriate locations on the page. These methods check the widget's current state before doing anything. For example, calling moveOffScreenToRight on the charts widget won't do anything if the chart is not currently displayed.

The following figure doesn't show all of the necessary widget interactions to complete the transition to details layout, but it illustrates the roles that the vehicle list, vehicle, and tile widgets play in the transition.

Sequence diagram showing a portion of the transition from dashboard to details layout

Hh404077.8f147f3e-62e5-4d4a-912b-b1e6f2db2ab0(en-us,PandP.10).png

When the hashchange event occurs, the _goToDetailsLayout method of the layout manager calls the _goToDetailsLayout method of the vehicle list widget. The vehicle list widget is responsible for coordinating the animation of the list from a two-column to a one-column layout as it moves to the left side of the screen while, simultaneously, all but the selected vehicle widgets shrink to a compact size. The details of these interactions are addressed in the next section.

Animating Vehicle Tiles

During the transition to the details layout, the vehicle list widget directs the vehicle widget to collapse all unselected vehicles, and the tile widget to animate the tiles left and down.

As you can see in the previous diagram, the goToDetailsLayout method of the vehicle list (shown below) starts the process of animating the vehicle tiles. After defining some variables, it calls the private _checkSelectedVehicleId method.

// Contained in mstats.vehicle-list.js
goToDetailsLayout: function () {
    var selectedVehicle,
        vid = 0,
        that = this,
        runningTop = 0,
        animationInfoArray,
        state = $.bbq.getState() || {};
    this._checkSelectedVehicleId();
    selectedVehicle = this.options.selectedVehicleId;
    ...
},

The _checkSelectedVehicleId method compares the selectedVehicleId option with the vid property in the URL. If they are different, it calls the _setOption method to update the value.

// Contained in mstats.vehicle-list.js
_checkSelectedVehicleId: function () {
    var vid,
        state = $.bbq.getState() || {};

    if (state.vid) {
        vid = parseInt(state.vid, 10);
        if (vid !== this.options.selectedVehicleId) {
            this._setOption('selectedVehicleId', state.vid);
        }
    }
},

When the selectedVehicleId option is set, code in the _setOption method calls the _expandVehicle and _collapseVehicles methods.

// Contained in mstats.vehicle-list.js
_setOption: function (key, value) {
    $.Widget.prototype._setOption.apply(this, arguments);
    if (value <= 0) {
        return;
    }
    switch (key) {
        case 'layout':
            this._setLayoutOption(value);
            break;
        case 'selectedVehicleId':
            this._expandVehicle(value);
            this._collapseVehicles();
            break;
    }
},

The _expandVehicle method ensures that the selected vehicle is not collapsed. The _collapseVehicles method loops through all vehicles, comparing their id option with the selectedVehicleId option. When a vehicle's id doesn't equal the selected one, the collapse method is called on the vehicle widget. This is the vehicle widget interaction shown in the sequence diagram above.

// Contained in mstats.vehicle-list.js
_collapseVehicles: function () {
    var selected = this.options.selectedVehicleId;

    this.element.find(':mstats-vehicle').each(function () {
        var $this = $(this);
        if ($this.vehicle('option', 'id') !== selected) {
            $this.vehicle('collapse');
        }
    });
},

Going back to the goToDetailsLayout method, after _checkSelectedVehicleId executes, all vehicle tiles will be collapsed or expanded appropriately. The next step is to animate the tiles into position. The following is a more complete view of the goToDetailsLayout method shown earlier.

// Contained in mstats.vehicle-list.js
goToDetailsLayout: function () {
    var selectedVehicle,
        vid = 0,
        runningTop = 0,
        animationInfoArray,
        that = this,
        state = $.bbq.getState() || {};
        this._checkSelectedVehicleId();
        selectedVehicle = this.options.selectedVehicleId;

    if (!this.options.isCollapsed) {
        this.element.find(':mstats-tile').tile('beginAnimation');

        this.element.find(':mstats-tile').each(function () {
            var $this = $(this);
            vid = $this.find('.vehicle').vehicle('option', 'id');
            animationInfoArray = [{
                position: { top: runningTop },
                duration: animationLength
            }, {
                position: { left: 0 },
                duration: animationLength
            }];

            $this.tile('moveTo', animationInfoArray, function () {
                that.element.find(':mstats-tile').tile('endAnimation');
            });

            // calculate the runningTop for next time around
            if (vid === selectedVehicle) {
                runningTop += 321;
            } else {
                runningTop += 206;
            }
        });

        this._narrowToSingleColumn();
    }

    this._scrollToSelectedVehicle();
    if (state && state.layout) {
        this.options.layout = state.layout;
    }

    this.options.isCollapsed = true;
},

Not to be confused with the vehicle widget, the vehicle list widget also has an isCollapsed option that indicates if it is currently displayed as a single column (collapsed) or as two columns. If it is not collapsed, it immediately calls the beginAnimation method on all tile widgets, as you can see in the sequence diagram above.

Note

The tile widget was created to reduce the complexity of the tile animations. Initially, the vehicle list and vehicle widgets contained all of the animation logic. Creating the tile widget freed up the vehicle widget to focus only on vehicle concepts. In contrast, the vehicle list widget still contains a significant amount of logic specific to the animations. A future refactoring might employ an additional widget that specializes in animating generic tiles between a one-column and two-column layout (or N-columns to N-columns).

Calculating Coordinates

Mileage Stats uses relative positioning for many elements, which means that an element's position is based on that of the preceding element (an element's positioning style, such as relative or absolute, is a CSS property). However, when elements are moving during an animation their position must be absolute so that their location values can be controlled. The beginAnimation method establishes the top and left CSS values based on the current top and left values. These values will be used when positioning is switched to absolute.

// Contained in mstats.tile.js
beginAnimation: function () {
    var $element = this.element;
    $element.css({
        top: $element.position().top + 'px',
        left: $element.position().left + 'px'
    });
},

Now that the tiles have been prepared, they can be instructed to move. The goToDetailsLayout method loops through each of the tile widgets and constructs an animationInfoArray with properties for position and duration. The runningTop variable is updated within the loop to specify the top of the next tile. This helps with the process of collapsing the two-column list into a single column and ensures that the items in the resulting list do not visually overlap.

The duration, animationLength, is defined in the file's closure at the top of the file with the value 600 (milliseconds). To move the tiles, the moveTo method is called on all tile widgets with two arguments. These are the animationInfoArray, and a callback function that executes when the moveTo method completes. The moveTo method is shown in the sequence diagram above.

The moveTo method calls its _unlock method, which sets the position to absolute, and then loops over the animationInfoArray, calling _animate for each item in the array.

// Contained in mstats.tile.js
moveTo: function (animationInfoArray, callback) {
    var that = this,
        arrayLength = 0;

    this._unlock();

    // if this is an array, iterate over it
    if (animationInfoArray.length) {
        arrayLength = animationInfoArray.length;
        $.each(animationInfoArray, function (index, info) {
            if (index === arrayLength - 1) {
                that._animate(info, callback);
            } else {
                that._animate(info);
            }
        });
    }
    // otherwise just animate one step with the data
    else {
        this._animate(animationInfoArray, callback);
    }
},

Next, the vehicles line up and form a single column. Although these actions seem to be two separate animations, they form a single animation that contains a 400ms delay that separates the two parts (height adjustment and the move left).

When moveTo completes, the code calls the endAnimation method. This method sets the position attribute back to relative, as shown in the following code.

// Contained in mstats.tile.js
endAnimation: function () {
    this.element
        .attr('style', '')
        .css({
            top: 0,
            left: 0,
            position: 'relative',
            float: 'left'
        });
}

This method also removes all style attributes, sets the top and left to zero because these values do not matter when using relative positioning, and sets float to left. This completes the animation sequence.

Summary

Even partially applying the SPI pattern to your application means that your client-side JavaScript must take some responsibility for handling navigation actions. Fortunately, with a little help, the browser is capable of performing most of the work. Depending on the nature of your hyperlinks, such as whether they change during a session, or are set only at the start of a session, and whether they should be added to the browser's history, the URLs can either be sent from the server in the correct format or modified on the client once the DOM is ready. If the links are to be modified on the client, they can either be rewritten when the page loads, or modified dynamically when activated.

Animations can be used to ensure that transitions don't confuse or disorient the user by keeping them in context. However, animations should not be too slow or annoy the users through extended use of the application. Depending on the complexity of the animation, you should look for opportunities to separate animation logic into modules. This will help with code readability and troubleshooting because the modules can be enabled or disabled easily, and they facilitate reusability.

Further Reading

For more information about the BBQ plug-in, see http://benalman.com/projects/jquery-bbq-plugin/.

For more information about Modernizr, see http://www.modernizr.com/.

For the RFC 3986 Uniform Resource Identifier generic syntax, see http://www.ietf.org/rfc/rfc3986.txt.

To learn more about the layout manager's _subscribeToGlobalEvents method and how the layout manager invokes widgets, see Chapter 8, "Communication."

Next | Previous | Home | Community