Dela via


Chapter 5: Modularity

Introduction | Benefits of Modularity for Hybrid-Design Web Applications - Leveraging Frameworks, Support for an Immersive UI, Support for Team-Oriented Development, Explicit Control over Application Initialization, Ability to Externally Configure Modules, Decoupled Module Interactions, Troubleshooting and Debugging, Code Base Maintenance, Testing | Factors That Influence a Modular Design - Page Layout, The Dashboard Layout, The Details Layout, Animations, Refreshing Data | Functional Roles and Categories for Modules - UI Modules, Behavioral Modules, Infrastructure Modules | JavaScript Module Implementations - Using JavaScript Objects as Modules, Using jQuery Plug-ins as Modules, Using Widgets as Modules, Externally Configuring Modules, Initializing Modules, Communicating Between Modules, Testing Modules | Summary | Further Reading

Introduction

A modular application is divided into functional units named modules that can be integrated into a larger application. Each module provides a portion of the application's overall functionality and represents a set of related concerns. Within a browser-based application, a module can add or remove user interface (UI) elements, add or enhance functionality (or behavior) already available in the UI, or enhance the user experience (UX). Modules can be built independently of one another but still communicate with each other in a loosely coupled fashion. Modular applications can make it easier for you to develop, test, deploy, and extend your application. Modular designs also have well-understood benefits that help you unit test your applications and make them easier to maintain over time.

Achieving a modular design in a complex JavaScript application requires specific techniques that are not immediately obvious if your previous experience is with object-oriented languages such as C# or Microsoft® Visual Basic®. While environments such as the Microsoft .NET Framework allow you to use classes, interfaces, and assemblies to organize application functionality, there are fewer options for creating explicit boundaries in JavaScript. Without a modular approach, JavaScript source files tend to be large and hard to maintain, and often the entire application is found in a single source file with numerous unstructured global variables. You can easily end up with an application that is prone to defects and difficult to troubleshoot and maintain. An application with the feature set of Mileage Stats is much more straightforward to develop and test when its design is modular.

In this chapter you will learn:

  • The benefits of modularity for hybrid-designed web applications.
  • The factors that influence a modular design, such as the module's boundaries.
  • Types of modules to consider for your application, along with their functional roles and categories.
  • When to use a JavaScript object, a jQuery plug-in, or a jQuery UI Widget.

The technologies and libraries discussed in this chapter are JavaScript, jQuery, jQuery UI.

Benefits of Modularity for Hybrid-Design Web Applications

There are a number of benefits of modular application design that apply specifically to hybrid-design web applications. These benefits can be seen through the whole application lifecycle, which includes design, development, testing, troubleshooting, and maintenance.

Modular design provides the following benefits:

  • The ability to leverage frameworks
  • Better support for an immersive UI
  • Support for team-oriented development
  • Explicit control over application initialization
  • The ability to externally configure modules
  • Decoupled module interactions
  • Easier troubleshooting and debugging
  • Easier code base maintenance
  • Easier testing

Leveraging Frameworks

There are a number of approaches to creating modular applications. When writing applications with JavaScript, the simplest way to partition your code into modules is to use JavaScript objects. However, for a truly flexible system, using only JavaScript objects to create modules, as opposed to other options presented in this chapter, will result in a code base that is larger than needed.

Another option is to use a framework that allows for modular designs. The Project Silk team chose the jQuery library because it supports modular designs through plug-ins and widgets and because it helps avoid cross-browser compatibility issues.

  • jQuery plug-ins allow developers to create libraries that extend the functionality of jQuery. There are jQuery plug-ins for everything from simplifying Ajax calls to implementing drag and drop.
  • jQuery UI Widgets are high-level building blocks that are provided by the jQuery UI library. Widgets are a way to create modules within an application that already takes advantage of the jQuery library, and to help impose modular design on client-side code. For more information on widgets, see Chapter 3, "jQuery UI Widgets."

Note

Note: This chapter uses the term module to refer to a JavaScript object, a jQuery plug-in, or a jQuery UI Widget, which contain cohesive sets of functionalities.

Of course, there are other high-quality libraries besides jQuery to choose from, such as Prototype and Script.aculo.us. Finding a framework to act as a starting point for a modular application is important. If the framework allows you to avoid having to implement low-level DOM manipulation yourself and offers cross-browser support (as jQuery does), so much the better. For other examples of JavaScript libraries and frameworks, see "Architectural Alternatives" in Chapter 2, "Architecture."

Support for an Immersive UI

Immersive experiences use modern UI design approaches to keep the user in context while inside the defined boundaries of the immersive experience. In context means that users are never confused about where they are in the application. Breadcrumbs are one way to help the user, but they do not match the intuitiveness of an immersive UI. The user expects either instant responsiveness or some indication of progress from an immersive web application. The experience must be fluid. This requirement places a number of nontrivial responsibilities on the client-side code. Some of these responsibilities have to do with Ajax data retrieval and caching, updating UI content, state management, and animating layout transitions.

It is difficult to implement the complex behavior required for an immersive UI without a design that is partitioned into modules with clear boundaries and responsibilities. Dividing functionality into widgets that are associated with specific HTML elements ensures that only a local region needs to be refreshed in the display. This can improve performance.

Support for Team-Oriented Development

Most complex web applications will be written by a team rather than by a single person. Dividing the application into modules allows pieces of the application to be developed in parallel by different teams or individuals. For example, after this approach was decided upon for Mileage Stats, the fill ups and reminders modules were simultaneously developed by separate members of the development team.

Explicit Control over Application Initialization

One of the challenging parts of a complex application is initializing and cleaning up various parts of the application at the appropriate times. A modular design can help to orchestrate these tasks. Fortunately, widgets contain several hooks for controlling what happens during initialization and cleanup. See "Initializing Modules" later in this chapter for an example of how Mileage Stats uses these hooks.

Ability to Externally Configure Modules

Modern applications often defer the configuration of components until the application is deployed. If you design your application in a modular way, you can implement ways to externally configure your modules. Without external configuration you must use hard-coded component dependencies, which result in brittle code that is hard to test and maintain.

Decoupled Module Interactions

A modular design allows you to formalize the interactions between the components of your system. For example, in Mileage Stats the interface of each widget allows the widget to work with other parts of the UI without unnecessary coupling. The interface of a widget is made up of three things: the options it accepts, the public methods it exposes, and the events it raises. To see an example of how modules in Mileage Stats collectively respond to user actions in a loosely coupled way, see "Communicating Between Modules" later in this chapter.

Troubleshooting and Debugging

Modular applications are easier to troubleshoot and debug. When a problem occurs, it is usually easy to isolate the source of the issue to either a single module or to the communication between modules. Once a faulty module is located, troubleshooting should be straightforward. For communication issues between modules, unit tests should indicate which side of the communication is causing the problem, allowing for a quick resolution.

Code Base Maintenance

A modular design allows your code base to be more maintainable by making it easier to understand. Adding new features and evolving the application over time is easier than it is in a monolithic application. Since each module has a well-defined set of responsibilities, decisions about where a new feature belongs should be straightforward. If the new feature is an addition to the responsibilities that a module owns, then the feature should be added to the module. If the new feature is outside the areas of responsibility for existing modules, there may be a need for a new module. Changes to the code tend to be more localized in applications that were originally designed with a clear and well-motivated modular structure. This makes adding new features less costly.

For example, in the future the Project Silk team may want to add an additional chart to the Mileage Stats application. This chart would show the user's average fuel efficiency for each vehicle compared to the average fuel efficiency of other vehicles of the same make and model. This feature is a type of chart, so extending the charts module to show the new type of data is a good option. However, if the team decides that the responsibilities are too different from those of the existing chart module due to the types of data or how the data should be displayed and partitioned, they could create a new module to handle the retrieval and display of this information. Adding the new module to the application would simply require initializing it at the appropriate time in the mileagestats.js file, and modifying the layout manager to handle any new user interactions and navigation required for the new feature.

Testing

Modular applications can be tested module by module. In addition, it is possible to create test frameworks that isolate modules from their application context. This technique, which is sometimes called sandboxing, allows components of the application to be tested before the entire application is complete. It also makes testing more robust by preventing software defects in one module from blocking or affecting the testing of other modules.

Factors That Influence a Modular Design

Numerous factors come into play when defining the roles and responsibilities of modules within an application. For interactive modules, some questions to consider are:

  • What are the design goals and constraints for the application?
  • How is the module defined visually?
  • What will the module do?

Using the UI elements to determine the responsibilities of a module requires answers to a few questions:

  • What UI element or elements define a visual boundary for the module?
  • Does the module need to create, insert, remove, or replace elements in the UI?
  • Will the module contain other modules? If so, will it be responsible for managing the lifecycle of the child modules?

In addition to defining the visual boundaries of a module, it is important to define what the module does. Several questions to consider are:

  • What behaviors or interactions should the module add to the UI elements?
  • What animations or screen transitions is the module responsible for?
  • What data responsibilities does the module have?
  • How does the module interact with other modules and the rest of the application?

To determine the boundaries of the modules in your application, a few techniques can be used. If you applied an approach for defining the UI and UX of your application, such as the one described in Chapter 4, "Design and Layout," you should already have the assets you need to begin defining application modules. After applying UX considerations you should have flow diagrams that illustrate user navigation and transitions throughout the application. After applying UI considerations you should have wireframes and various mockups that show the dimensions and relationship between UI elements. These assets can be used to influence the boundaries of modules. You can identify the modules in your application by evaluating the following three aspects:

  • Layout. Wireframes and mockups are used to guide the design of UI modules that are made up of groups of related elements.
  • Animations. Flow diagrams and navigation transitions are used to identify animations that are independent of the UI elements that the animation affects. Animations can influence the need for behavioral modules.
  • Data Refreshes. UX considerations identify how user actions that update the data in portions of the UI will affect the design of modules. The boundaries of these updates can indicate the possible boundaries of additional UI modules as well as infrastructure modules to accommodate the refreshing of data.

The Mileage Stats team used the assets from the design phase to initially define module boundaries. As a result, the team found that most of these modules remained unchanged throughout the building and refactoring of the application. The remainder of this section illustrates how applying these techniques influenced Mileage Stats.

Page Layout

The UI layout gave the developers a good indication of how to begin defining widgets, most of which would be associated with visual elements. In Mileage Stats, a user can view statistics for each vehicle at three levels of granularity. A top priority was that users never see any page refreshes while switching from the dashboard to the details and charts layouts. In other words, these levels define the boundaries of an immersive experience. The following figure illustrates the three main layouts.

The three main screen layouts in Mileage Stats

Hh404079.eacf2677-948a-4f1d-b499-f4cceac1559b(en-us,PandP.10).png

The dashboard contains a summaryPane region and the vehicleList region. The details layout contains vehicleList and infoPane regions, and the charts layout uses a single region.

The Dashboard Layout

The following figure shows the Mileage Stats dashboard.

The Mileage Stats Dashboard

Hh404079.dda2d038-01a4-4383-b351-7508f378c716(en-us,PandP.10).png

The information presented to the user includes:

  • Site navigation links
  • A registration form
  • A list of vehicles that the user has entered in the system
  • Statistics aggregated across all the vehicles
  • A list of reminders that are overdue or nearly due
  • Status messages about actions the user has taken

After partitioning the screen based on its layout, the team arrived at the following module boundaries.

Dashboard regions

Hh404079.7a5aa03b-c186-4101-b246-725537a5fd7c(en-us,PandP.10).png

As you can see, the main components of the dashboard are the summaryPane region and the vehicleList region. The summary pane, like the vehicle list, is a parent widget with three child widgets. After registration is completed, the registration widget is no longer shown. The status and header regions appear in all layouts.

The Details Layout

The details layout, shown in the following figure, is another main screen that provides one of the three levels of granularity. The details layout is divided into the vehicleList region (which is shared with the dashboard layout) and the infoPane region. As you can see, the tile widget is applied to each child in the vehicle list while the vehicle widget is applied only to vehicle tiles. Only the tile widget is applied to the Add Vehicle button at the bottom of the vehicle list. The responsibilities of these widgets are described in the "Animations" section that follows.

Details regions

Hh404079.f54bbf65-5c8e-427f-880b-fa7845c352c5(en-us,PandP.10).png

Note

Because the vehicleList region is used in both the dashboard and details layouts, users never lose sight of the selected vehicle, which keeps them in context.

There are no full-page refreshes when users navigate to the different layouts, so each region of the page must know how to respond to show, hide, and animate directives. These requirements are good indications that the regions should be widgets. Also, some module must be responsible for telling each of these widgets to show, hide, or animate. This is the role of the layoutManager. This module has no UI, but it controls the operation of other widgets. For more information about the layoutManager, see Chapter 9, "Navigation."

Animations

Animations are another factor that can influence the need for a module. Animation modules can be implemented as widgets, which permit them to focus on the details of the animation. This separates the animation code from the code that manages the state of the UI elements, such as data refreshes.

In Mileage Stats, the region that shows the list of vehicles appears in both the dashboard and details layouts. Transitions between the dashboard and details layouts are animated: the summary pane and the vehicle list regions enter and exit at the left side of the screen, and the info pane and chart regions enter and exit at the right. This animation influenced the decision to create the tile and vehicle widgets.

The vehicle list widget contains two types of elements: vehicle tiles and a tile that holds the Add Vehicle button. When a user transitions to and from the dashboard and details layouts, the animation uses a two-step process to move the vehicle tiles so that they are displayed in either one or two columns. At the same time, the vehicle tiles that were not selected shrink to a smaller size. These are two distinct cases, which indicate that there should be a widget for each type of animation.

Mileage Stats uses the tile widget to animate the position of all boxes horizontally and vertically, because both the vehicle boxes and the Add Vehicle box need that behavior. The vehicle widget expands and collapses the vehicle boxes, because only they need that behavior. The following figure illustrates the transition from the dashboard layout to the details layout.

Transitioning from the dashboard to details

Hh404079.da096bcf-4793-43b2-85cd-e6e18542c11c(en-us,PandP.10).png

Modularizing these animations into widgets results in pieces of code that have clear boundaries and responsibilities.

Note

It would also be possible to separate the animation logic into its own widget that the vehicleList widget could then apply to itself. This would be particularly useful if you needed to apply the animation elsewhere or needed the ability to easily change the animation applied to the vehicleList widget.

Refreshing Data

When all data updates happen through Ajax calls, the various parts of the UI must know how and when to request updates from the server and how to apply any necessary changes to the UI. Depending on the user's action, only parts of the UI, rather than all of the data on the page, may need to be updated. The boundaries that delineate the data to be refreshed and the data that remains the same can influence the boundaries of modules.

The statistics and imminent reminders regions of the summary widget are themselves widgets. They both must be able to request updates from the server and apply them. The following figure illustrates the summary widget, with two of the widgets it is responsible for.

The summary widget

Hh404079.eec2ff04-6049-4a6b-b46e-9499bda7aa91(en-us,PandP.10).png

The statistics and imminent reminders regions each know how to request their own relevant data and update their content when changes in the vehicle, fill ups, or reminder data are detected. Many of the other widgets in Mileage Stats are also responsible for retrieving and applying updated content. However, the code that actually makes the requests and adds caching functionality is implemented in a separate module as a JavaScript object. To learn more about data abstraction in Mileage Stats, see Chapter 6, "Client Data Management and Caching."

Functional Roles and Categories for Modules

The role of a given module generally falls into one of three categories, each with a specific focus:

  • UI modules are responsible for adding, removing, and modifying UI elements within the boundaries of the module.
  • Behavioral modules are responsible for applying behavior to the elements within the module. Animation is an example of a behavior.
  • Infrastructure modules are responsible for application-wide requirements that are not specific to the UI, such as data access and communication.

UI Modules

UI modules, such as the statistics and imminentReminders widgets in Mileage Stats, are responsible for the visual representation of an element. UI modules can be commonly used controls such as date and time pickers, combo boxes, or tab controls. They can also be application specific, which is true of the Mileage Stats widgets.

Some of the Mileage Stats widgets rely on HTML and CSS for their appearance, and may correspond to elements with child elements. Alternatively, a widget may be applied to an element that has no child elements. In this case, the widget is responsible for adding the elements that make up the UI. This situation commonly occurs when the initial response from the server doesn't contain all of the necessary elements. When this happens, the widget may have to request the elements from the server or apply a data template. An example of this type of widget is the infoPane widget.

A widget can also act as a container for other widgets. An example of a container in Mileage Stats is the summary widget, which contains the registration, statistics, and imminentReminders widgets. Containers can have knowledge of their children because they are often responsible for creating those children, attaching children to the correct elements, and responding to events from their children.

Note

You should avoid creating children in container widgets that have knowledge of their parent because the resulting bidirectional dependencies make it more difficult to create a layered application. Bidirectional module dependencies also make the application harder to test.

Behavioral Modules

Behavior widgets and JavaScript objects add functionality to an existing element. The jQuery UI library calls these pieces of functionality interactions. Commonly used behavioral widgets include the draggable, droppable, resizable, selectable, and sortable widgets. In Mileage Stats, the behavior widgets include tile and layoutManager. There is also a JavaScript object for managing the process of pinning the site to the operating system's taskbar.

Infrastructure Modules

Infrastructure modules provide commonly needed functionality that isn't related to the visual aspects of the application. They don't interact with the UI. Typically, their functionality includes data access, communication, logging, or caching. The infrastructure modules in Mileage Stats are JavaScript objects. They include dataManager, dataStore, and pubsub.

JavaScript Module Implementations

Applications such as Mileage Stats that use JavaScript and jQuery can implement modules in any of the following ways:

  • JavaScript objects. JavaScript objects are a good choice for implementing modules that are not associated with visual elements on the page. JavaScript objects are the most lightweight type of module.
  • jQuery plug-ins. You should consider jQuery plug-ins when you need to extend the functionality of the jQuery framework. Plug-ins can encapsulate notions of UI and behavior.
  • jQuery UI Widgets. When modules are associated with specific HTML elements, consider using jQuery UI Widgets. The jQuery UI framework provides helpful, built-in, functionality that makes widgets behave like user controls. These capabilities include creation, initialization, a property notification system, an event model, and teardown. Associating widgets with UI elements is an easy way to organize the code that supports your UI. For example, you can use a widget to populate a visual element with a new form dynamically in response to a UI event.

The following table shows the suitability of these three types of JavaScript modules for the functional roles or categories described in the previous section. These are not definitive rules, just suggestions on how to choose the appropriate module. The following sections describe the implementation of these JavaScript modules in more detail.


JavaScript objects

jQuery plug-ins

jQuery UI widgets

UI modules

No

OK

Ideal

Behavioral modules

OK

Ideal

OK

Infrastructure modules

Ideal

OK

No

Note

Behavioral modules span a wide array of scenarios, which may or may not need to operate directly on DOM elements. Any of the three implementations may be appropriate. Use the guidance throughout this section to help you decide which module to use.

For consistency, Mileage Stats uses widgets for everything that is associated with an HTML element or adds behavior to HTML elements, and uses JavaScript objects for everything else.

Using JavaScript Objects as Modules

JavaScript objects are the most basic implementation of a module. They can be easy to write for simple modules, but they do not automatically provide the features available to plug-ins and widgets. Implementing a module as a JavaScript object is most appropriate when its functionality is not directly related to the HTML elements in the page. These modules only require logic that can be fulfilled by the language and don't need to extend existing libraries, such as jQuery, that abstract DOM manipulation. When a module extends the functionality of a library, for example to operate on DOM elements, it is more appropriate to use the library's extensibility points.

The following table lists the JavaScript objects that are used in Mileage Stats.

File

Purpose

Functional Category

mstats.pinnedsite.js

Provides the pinned sites implementation for Windows® Internet Explorer® 9

Behavioral

mstats.data.js

Data manager that retrieves and stores data; lets callers know when data is available

Infrastructure

mstats.pubsub.js

Manages subscriptions and publication of events

Infrastructure

mstats.events

Sets the event, which can be any button a user clicks, such as Details, or Add Vehicle

Infrastructure

mstats.vehicle-drop-down-monitor

Displays the Edit form

Infrastructure

Using jQuery Plug-ins as Modules

One of the characteristics of a good framework, such as jQuery, is a robust extensibility mechanism. Creating a plug-in is the recommended way to extend jQuery. In fact, a plug-in that follows the recommendations included in the "jQuery Plug-in Authoring Guidelines" is indistinguishable from the methods in the core library. Many features in jQuery began as external plug-ins that were later added to the library.

Note

For more information on authoring plug-ins, see the "jQuery Plug-in Authoring Guidelines" in the "Further Reading" section at the end of the chapter.

Because plug-ins behave just as other jQuery functions do, they can be invoked on elements by using jQuery selectors. Inside the plug-in, the this keyword is a reference to the set of DOM elements selected when the plug-in is applied. As an added advantage, the each function on this reassigns the this keyword to each DOM element of the selected elements.

The following code shows a plug-in named doubleSizeMe that doubles the size of an element.

// Code example not in Mileage Stats
(function($){
    $.fn.doubleSizeMe = function() {
        return this.each(function() {
            var $this = $(this),
                width = $this.width(),
                height = $this.height();

            $this.width(width * 2);
            $this.height(height * 2);
        });
    };
})(jQuery);

This example code adds the doubleSizeMe method to the jQuery prototype so that it is available when you operate on a wrapped set. For example, to invoke the function on all elements with a class of icon, you would use the following call.

$('.icon').doubleSizeMe();

Note

The example uses the jQuery functions for height and width, which provide cross-browser compatibility.

There is much more functionality that you can add to your plug-ins. However, in some cases, plug-ins may not provide all the functionality you need. If you are writing a module that stores state internally, exposes methods you expect others to call, hides private methods, inherits from another object, or requires per-instance configuration, then a widget may be a better option.

Using Widgets as Modules

A jQuery UI Widget provides you with a number of capabilities that are useful when creating modules. These include features for object construction and destruction, storing state, merging options, and exposing public methods. To learn more about how to build widgets, see Chapter 3, "jQuery UI Widgets" and Chapter 14, "Widget QuickStart."

Mileage Stats uses UI widgets, which you can see on the web page, and behavioral widgets whose effects, such as animations, are visible. The UI widgets are responsible for visual elements and help to implement the application's presentation layer. Behavior widgets add functionality and help to implement the behavior layer of the application. The following table lists the main widgets that are used in Mileage Stats.

File

Purpose

Functional Category

mstats.status.js

Manages and displays user notification messages

UI

mstats.summary.js

Container that manages registration, statistics, and reminders widgets

UI

mstats.registration.js

Contained in summary widget; manages user registration

UI

mstats.statistics.js

Contained in summary widget; displays summary of vehicle statistics

UI

mstats.reminders.js

Contained in summary widget; lists overdue and upcoming maintenance reminders; manages click action when user selects a reminder

UI

mstats.layout-manager.js

Manages navigation requests and coordinates UI layout changes

Behavioral

mstats.vehicle-list.js

Displays vehicle tiles in one or two columns; invokes animation of child widgets; controls their contraction and expansion

UI

mstats.vehicle-list.js

Manages vehicle tiles, which are children of vehicle list widget

UI

mstats.vehicle.js

Displays vehicle information; manages actions of Details, Fill ups and Reminder buttons

UI

mstats.vehicle-details.js

Gathers and displays vehicle details

UI

mstats.charts.js

Creates charts displayed when user clicks Charts button

UI

mstats.info-pane.js

Controls display of fill ups, reminders, and vehicle details widgets, which are created in the same file

UI

mstats.tile.js

Moves vehicle tiles vertically and horizontally

UI

Externally Configuring Modules

In Mileage Stats, some widgets are configured externally to reduce the coupling between them. For example, when the layoutManager widget is constructed in mileagestats.js, its subscribe option is populated with the mstats.pubsub.subscribe method. The rest of the options are references to the modules the layout manager is responsible for coordinating.

// Contained in mileagestats.js
$('body').layoutManager({
    subscribe: mstats.pubsub.subscribe,
    pinnedSite: mstats.pinnedSite,
    charts: charts,
    header: header,
    infoPane: infoPane,
    summaryPane: summaryPane,
    vehicleList: vehicleList
});

Injecting dependencies through options allows the code that creates the widget to supply data, functions, and other modules during creation. This prevents the widget from needing to know how to resolve the dependencies itself, without sacrificing functionality. See "Defining Options" in Chapter 3, "jQuery UI Widgets" for more information about configuring modules.

Initializing Modules

The _create method of a widget contains initialization code, and the destroy method is used to clean up what the widget created. For example, in Mileage Stats, the vehicle list widget initializes the vehicle, tile, and sortable widgets when it is created. These widgets must be cleaned up in the destroy method.

// Contained in mstats.vehicle-list.js
_create: function () {

    dataUrl = this.element.data('list-url');

    this._widgetizeVehicleTiles();
    this._bindEventHandlers();
    this._makeSortable();
    ... 
},        
destroy: function () {
    this.element
        .sortable('destroy')
        .find(':mstats-tile').tile('destroy')
        .find(':mstats-vehicle').vehicle('destroy');

    $.Widget.prototype.destroy.call(this);
}

The _create method uses private methods to initialize the other widgets. The _widgetizeVehicleTiles method initializes the vehicle and tile widgets and _makeSortable initializes the sortable widget. The destroy method cleans up these widgets by calling their destroy methods, and then calls the base widget's destroy method on the last line.

Communicating Between Modules

In order to coordinate the responses to user actions, modules must be able to communicate with each other. Method calls and events are an effective way to accomplish this. As an example, the following sequence diagram shows some of the public methods that are called when a reminder is fulfilled in Mileage Stats.

Sequence diagram for fulfilling a reminder

Hh404079.d22336be-b6b3-4eb6-b72e-e1794ee39d76(en-us,PandP.10).png

When the Fulfill button on the reminders pane is selected, it publishes its status, makes the Ajax call to save the reminder, and publishes an mstats.events.vehicle.reminders.fulfilled event. At this point, the reminders widget has not yet updated its UI with an updated list of reminders. The layout manager then instructs the summary, info pane, and pinned site modules to retrieve updated reminder data, and they coordinate the interactions with their child widgets. As a result, the reminders widget that initiated the action doesn't update its data until it is told to do so by the info pane.

These interactions illustrate the flexibility of a modular design. For more information on enabling communication between widgets, see Chapter 8, "Communication."

Testing Modules

Even without sandboxing, modular applications are easier to test using techniques such as unit testing. The modules can be tested by invoking each function of the interface that is exposed to other modules. For example, in Mileage Stats the header widget controls the header text it displays through its title option. The following test ensures that the header text changes each time the value of the option changes.

// Contained in mstats.header.tests.js
test('when title option is changed, then it displays new title', function() {
    expect(1);
    var header = $('#header').header();
    header.header('option', 'title', 'test title');
        
    equal($('[data-title]').text(), 'test title', 'header text set properly');
});

When this test runs, it ensures that the _setOption method on the header widget, the code under test, updates the text for the header.

// Contained in mstats.header.js
_setOption: function (key, value) {
    switch (key) {
        case 'title':
            this.element.find('[data-title]').text(value);
            break;
        ...
    }
    $.Widget.prototype._setOption.apply(this, arguments);
}

In order for the tests to run in isolation, each test file must contain a copy of the HTML markup that mirrors the markup to which the code under test will be applied. You can find this markup at the top of the test files.

// Contained in mstats.header.tests.js
module('Header Widget Tests', {
   setup: function () {
       $('#qunit-fixture').append('<div class="header" id="header">' +
           '<div><div><h1 data-title>Dashboard</h1>' +
               ...
           '</div>'
       );
    }
}); 

For more information about unit testing, see Chapter 13, "Unit Testing Web Applications."

Summary

There are several possibilities for imposing a modular structure on complex JavaScript applications. JavaScript objects are a good choice for implementing modules that are not associated with elements on the page. When the boundaries of the module are defined by visual elements of the UI, consider using jQuery UI widgets. You can use jQuery plug-ins to extend the functionality of the jQuery library.

Independent of the types of modules you use, you can expect the same sorts of benefits from modularity that you see in solutions created with object-oriented languages. A modular design makes your code base more maintainable, easier to test, easier to troubleshoot, and suitable for team development.

The boundaries of your modules can be influenced by considering page layouts, animations used during transitions, and the need to selectively refresh parts of the UI based on user actions. When defining these boundaries, consider the various layouts in the application and the regions in those layouts. Also, take animations into account, as well as content that must be updated with Ajax. The modules should each be easily identifiable as belonging to the presentation (UI) layer, the behaviors layer, or the infrastructure layer.

Further Reading

jQuery Plug-in Authoring Guidelines: http://docs.jquery.com/Plugins/Authoring

For more information about how to build widgets, see Chapter 3, "jQuery UI Widgets" and Chapter 14, "Widget QuickStart."

To learn more about data abstraction in Mileage Stats, see Chapter 6, "Client Data Management and Caching."

For more information on enabling communication between widgets, see Chapter 8, "Communication."

For more information about how the tile widget performs its animation, see Chapter 9, "Navigation."

For more information about unit testing, see Chapter 13, "Unit Testing Web Applications."

For other examples of JavaScript libraries and frameworks, see "Architectural Alternatives" in Chapter 2, "Architecture."

For examples of other libraries that support a modular design, see:

Next | Previous | Home | Community