Chapter 5. Large-Scale JavaScript

The behaviors supported by JavaScript in a web application form a distinct layer beyond the information architecture defined by HTML and the presentation defined by CSS. Although in many web applications it’s important to consider how the application would operate without the behavior layer (for reasons of accessibility, printing, and search engine optimization, for example), the widespread use of Ajax has made the behavior layer in many large web applications more important than ever. In many cases, the designs for large web applications have advanced to the point that user experience designers, product managers, and engineers will agree that certain parts of an application, or even the entire application, will only make sense with a fully functioning behavior layer. Think of Google Maps, in which tiles are requested from the server as you interact with the map. This application only makes sense with a strong layer dedicated to behavior using JavaScript.

JavaScript has been around for a long time, of course, but early web applications rarely made much use of its powerful object-oriented features. Most developers simply wrote functions to accomplish various tasks, paying little attention to JavaScript’s capabilities for developing systems of loosely coupled, highly encapsulated objects using prototype-based inheritance (see Chapter 2). A more advanced use of object-oriented JavaScript is an important aspect of achieving Tenet 5 from Chapter 1:

Tenet 5: Large-scale JavaScript forms a layer of behavior applied in a modular and object-oriented fashion that prevents side effects as we reuse modules in various contexts.

We begin this chapter by exploring some fundamental techniques for writing modular JavaScript. We’ll look at ways to include JavaScript in an HTML file and establish a scope in which the JavaScript for a module can operate without conflicting with other JavaScript in use within the application. Next, we’ll explore some important methods for working with the DOM, which JavaScript applications have to manipulate in order to handle web pages in the structured manner we need. This includes a discussion of several important methods that all major browsers support as well as a look at methods in some popular JavaScript libraries that standardize other helpful DOM operations. We’ll follow this with a look at techniques for improving event handling in large web applications and some examples of JavaScript animation. Finally, we’ll explore an example of modular JavaScript for implementing chained selection lists.

Modular JavaScript

Once we have divided the components of a web page into reusable modules that reflect the information architecture (see Chapter 3) and have added a layer of presentation with CSS (see Chapter 4), we can focus on writing an additional layer to implement dynamic behaviors with JavaScript. As mentioned in Tenet 5, the JavaScript for a module, like the CSS for a module, should:

  • Have distinct modules of its own that apply to specific parts of the architecture (in other words, it should be well targeted), and

  • Not produce unexpected consequences as a module is reused in various contexts (in other words, it should be free of side effects).

Once you separate most of your JavaScript methods into modules, you’ll find that it takes only a small amount of JavaScript unique to each page to stitch them together.

Including JavaScript

Let’s look at three ways to include JavaScript in a web application: linking, embedding, and inlining. Although linking is generally the most desirable of these options, it is common to find some combination of these methods in use across a large web application, even within a single page, since each method has some benefit depending on the specific situation.

Linking

Linking allows you to place JavaScript in a file, which you then include by placing a script tag with a src attribute in an HTML file. As with CSS files, this has desirable effects: architecturally, multiple pages can share the same JavaScript, while in terms of performance, the browser can cache the file after downloading it the first time. Although it’s common to place a linked JavaScript file in the head section at the top of an HTML file, Chapter 9 presents some very good reasons related to performance for placing linked JavaScript at the end of the body tag (i.e., at the bottom of the page). The following example links a JavaScript file:

<script src="http://.../sitewide.js" type="text/javascript">
</script>

Once you modify a file that browsers may have already cached, you need to make sure browsers know when to bust the cache and download the new copy. Version numbers help you manage this situation. Incorporating a date stamp into the JavaScript file’s name works well, and you can add a sequence number when updating the JavaScript file more than once a day (or you could use the version number from your source control system instead of the date stamp). For each new copy, simply bump up the version number as shown here:

<script src="http://.../sitewide_20090422.js" type="text/javascript">
</script>

Of course, each page that includes a link to the JavaScript file must also be updated to refer to the new version. Chapter 7 presents information on managing this in a centralized way.

Embedding

Embedding places JavaScript directly within a page. One benefit of this approach is that you can conveniently generate and include JavaScript programmatically as you build the page. While it’s common to place embedded JavaScript in the head section at the top of an HTML file, Chapter 9 presents some very good reasons related to performance for placing embedded JavaScript at the end of the body tag (i.e., at the bottom of the page). Example 5-1 illustrates some embedded JavaScript.

Example 5-1. Embedding JavaScript
<head>

...

</head>
<body>

...

<script type="text/javascript">
// Embedded JavaScript is JavaScript contained within script tags.
var GreenSearchResultsModel.MReg = new Array();
var GreenSearchResultsModel.VReg = new Array();

...

</script>
</body>

Inlining

Inlining JavaScript lets you add small amounts of JavaScript for event handlers to HTML elements within the HTML itself. These stubs of JavaScript usually call upon other JavaScript that has been included using linking or embedding. The following illustrates the inlining of some JavaScript for an onclick handler:

<img class="btnl" src="http://.../slide_arrow_l.gif" width="14"
   onclick="picsld.slideL();" />

Rather than using inline JavaScript, it is almost always preferable to register for events using methods within JavaScript that is linked or embedded (see the discussion later in this chapter about YAHOO.util.Event.addListener).

Scoping with JavaScript

In Chapter 7, we’ll examine techniques for creating modules as self-contained components of the user interface. These modules will encompass everything needed (e.g., HTML, CSS, and JavaScript) to make an independently functioning and cohesive unit that you can use in a variety of contexts across various pages. To accomplish this, the JavaScript for a module needs to form a nicely encapsulated bundle as well. In this section, we’ll look at how the use of objects in JavaScript makes this possible.

Namespaces with JavaScript

JavaScript objects provide a natural way to create namespaces in which to place the data and methods for specific modules. Once you have defined a method for an object, you invoke it directly through the object itself. For example, the following invokes the slideL method on the picsld instance of PictureSlider, an object that implements the behavior layer for a module that displays images in a slideshow or carousel-type filmstrip:

picsld = new PictureSlider();

...

picsld.slideL();

You should notice two important things here:

  • Because slideL is a member of picsld (as opposed to the global window object), you can be sure that this use of slideL will not conflict with any other use in your web application.

  • The slideL method will have access to data members within PictureSlider, which provides better encapsulation and abstraction for the event handler than a global one does.

A good convention for naming JavaScript objects that correspond to modules is to name them the same as the class used to manage the module on the server. For example, in Chapter 7 we’ll look at an example of a slideshow module called PictureSlider, which encapsulates the HTML, CSS, and JavaScript for the module neatly within a PHP class. The PHP class is called PictureSlider, so we’ve created a JavaScript object called PictureSlider to address the behavior layer for the module. This convention works well, but the exact convention is not what is important here; establishing a system of unique qualification that clearly preserves modularity is the key.

The use of objects also gives you a way to group prototype objects into libraries. You can see examples of this in the YUI library, which we’ll present later. When you call YAHOO.util.Dom.getStyle, for example, you’re calling a method that is actually a member of the Dom object, which is a member of the util object, which, in turn, is a member of the YAHOO object. At the end of this chapter, we’ll explore a prototype object called MultiSelect that we’ve placed in the MVC namespace. Whenever you do this, you need to check whether the namespace object already exists so that if it doesn’t exist, you can create it, as shown here:

// Place the component within the MVC namespace; create it if needed.
if (!window.MVC)
{
   MVC = {};
}

...

MVC.MultiSelect = function(text, url, id, name, prev)
{
   ...
}

Accessing a module by ID

Once you have an object that encapsulates the data and methods for a module, that object typically spends most of its time working in just the part of the DOM that contains the elements for that module. Therefore, it’s useful in the constructor for the module object to set data members to whatever parts of the DOM you’ll need access to rather than retrieving them over and over again within various parts of the object. This improves performance and acts somewhat as a means of “binding” a JavaScript object to the HTML elements for which the object is adding a behavior layer.

One of the most important DOM methods for this is document.getElementById. This method returns a reference to the element with the ID you specify. As we saw in Chapter 4, a good way to scope a module for CSS (and JavaScript, too) is to give its outermost div an id attribute. Once you have a reference to this div, you can target all other DOM operations within the proper scope for the module. Thus, Example 5-2 illustrates accessing the DOM for a PictureSlider instance in the constructor for the object. The example also uses other DOM methods to get various elements within the module, which we’ll explore further in the next section.

Example 5-2. The constructor for the PictureSlider object
PictureSlider = function()
{
   // Set up references to the elements needed for the slider and viewer.
   this.slider = document.getElementById("picsld");

   if (this.slider)
   {
      this.tab = this.slider.getElementsByTagName("table");
      this.tab = (this.tab && this.tab.length > 0) ? this.tab[0] : null;
   }

   if (this.slider)
   {
      this.lb = YAHOO.util.Dom.getElementsByClassName
      (
         "btnl",
         "img",
         this.slider
      );

      this.lb = (this.lb && this.lb.length > 0) ? this.lb[0] : null;

      this.rb = YAHOO.util.Dom.getElementsByClassName
      (
         "btnr",
         "img",
         this.slider
      );

      this.rb = (this.rb && this.rb.length > 0) ? this.rb[0] : null;
   }

   this.viewer = document.getElementById("picvwr");



   ...
};

Working with the DOM

The previous section illustrates how important it is for JavaScript to be capable of referencing an element by ID. There are several other methods for performing DOM operations in large web applications that you can expect to use frequently. This section presents some of the most common methods that are supported across the major browsers. Then we’ll look at a few methods in popular JavaScript libraries that provide additional capabilities with the DOM.

Common DOM Methods

Some of the most important DOM methods supported intrinsically across the major browsers allow you to access elements by tag name, create new elements, insert nodes into the DOM, remove nodes, and change text.

Accessing elements by tag name

The following method returns an array of HTML elements that are img tags enclosed by the invoking element (element):

elements = element.getElementsByTagName("img");

Creating an element

The following method creates an HTML img element. The method returns a reference to the element that was created.

element = document.createElement("img");

The following method creates a text node containing the string "Welcome". The method returns a reference to the node.

textNode = document.createTextNode("Welcome");

Inserting or removing an element

The following method inserts newNode into the DOM at the end of the list of children of parentNode. HTML elements are derived from DOM nodes, so you can use this method to insert an HTML element (e.g., created by document.createElement) wherever you would like it to appear; or use this method to insert a text node (e.g., created by document.createTextNode).

parentNode.appendChild(newNode);

The following method removes oldNode from the children of parentNode. HTML elements are derived from DOM nodes, so you can use this method to remove an HTML element or use this method to remove a text node.

parentNode.removeChild(oldNode);

Changing the text in an element

To change the text for an element that doesn’t contain any other nodes as its children, you can use the innerHTML property, as shown here:

element.innerHTML = "Goodbye";

Several JavaScript libraries provide excellent support for working with the DOM. These include Dojo, jQuery, Prototype, and the YUI library. Of course, the methods provided here are just a tiny sampling of what these libraries offer for working with the DOM, as well as support for event handling, animation, drag and drop, browser history management, user interface components, and Ajax (see Chapter 8). As you’ll see, there are only minor differences in how these libraries support the most common DOM operations.

DOM methods in Dojo

Dojo is a JavaScript library built on several contributed code bases. You can download the library and get complete documentation at http://www.dojotoolkit.org.

Accessing the DOM

The following method returns all img elements contained by the element with the ID picsld as a Dojo NodeList object. The first parameter for dojo.query is almost any CSS selector (see Chapter 4). You can also provide an optional second parameter for the root node at which to start the query. NodeList implements an interface that allows you to call the other methods in this section directly on a NodeList object, too. Omit the first parameter (element) of the other methods in this section in that case:

nodelist = dojo.query
(
   "#picsld img"
);
Working with attributes

The following method provides a uniform way to get the value of an attribute for result (the src attribute in this case) across the major browsers:

value = dojo.attr
(
   element,
   "src"
);

The following method provides a uniform way to set attributes for result (the src and class attributes in this case) across the major browsers:

dojo.attr
(
   element,
   {
      src: "http://...slide_arrow_l.gif",
      class: "btnl"
   }
);
Working with styles

The following method gets the value of a style for result (the color property in this case):

value = dojo.style
(
   element,
   "color"
);

The following method sets a collection of styles for result (the color and backgroundColor properties in this case). Use CamelCase for properties that are hyphenated, as illustrated by the string backgroundColor:

dojo.style
(
   element,
   {
      color: "#0f0f0f",
      backgroundColor: "#f0f0f0"
   }
);

DOM methods in jQuery

The jQuery JavaScript library has especially good documentation. You can download the library and read its complete documentation at http://www.jquery.com.

Accessing the DOM

The following method returns all img elements contained by the element with the ID picsld as a jQuery instance. The first parameter for jQuery is almost any CSS selector (see Chapter 4). You can also provide an optional second parameter for the root node at which to start the query. The jQuery library also defines $, a legal identifier in JavaScript, to do the same thing as the jQuery object:

result = jQuery
(
   "#picsld img"
);
Working with attributes

The following method provides a uniform way to get the value of an attribute for element (the src attribute in this case) across the major browsers:

value = result.attr
(
   "src"
);

The following method provides a uniform way to set attributes for the first element in elements (the src and class attributes in this case) across the major browsers:

result.attr
(
   {
      src:   "http://.../slide_arrow_l.gif",
      class: "btnl"
   }
);
Working with styles

The following method gets the value of a style for element (the color property in this case):

value = result.css
(
   "color"
);

The following method sets a collection of styles for element (the color and backgroundColor properties in this case). Use CamelCase for properties that are hyphenated:

result.css
(
   {
      color: "#0f0f0f",
      backgroundColor: "#f0f0f0"
   }
);

DOM methods in Prototype

Prototype is one of the earliest of the popular JavaScript libraries. You can download the library and read its complete documentation at http://www.prototypejs.org.

Accessing the DOM

The following method returns all img elements contained by the element with the ID picsld as an array. $$ is a special method in Prototype that returns an array of elements that match a CSS selector. Its parameter is almost any CSS selector (see Chapter 4):

elements = $$
(
   "#picsld img"
);
Working with attributes

The following method provides a uniform way to get the value of an attribute for the first element in elements (the src attribute in this case) across the major browsers:

value = elements[0].readAttribute
(
   "src"
);

The following method provides a uniform way to set attributes for element (the src and class attributes in this case) across the major browsers:

elements[0].writeAttribute
(
   {
      src: "http://...slide_arrow_l.gif",
      class: "btnl"
   }
);
Working with styles

The following method gets the value of a style for the first element in elements (the color property in this case):

value = elements[0].getStyle
(
   "color"
);

The following method sets a collection of styles for the first element in elements (the color and backgroundColor properties in this case). Use CamelCase for properties that are hyphenated:

elements[0].setStyle
(
   {
      color: "#0f0f0f",
      backgroundColor: "#f0f0f0"
   }
);

DOM methods in YUI

The YUI library was developed at Yahoo! for use both within Yahoo! and by the world’s web development community. You can download the library and read its complete documentation at http://developer.yahoo.com/yui.

Note

As this book was being completed, YUI 3 was in beta development. The information below pertains to versions prior to this. One of the big differences between YUI 2 and YUI 3 is the YUI object, which places YUI 3 features in their own namespace. This lets you transition from YUI 2 to YUI 3 without having to change all your code at once.

Accessing the DOM

The following method returns an array of img elements that pass a test implemented by method. You can provide an optional third parameter for the root node at which to start the query. YAHOO.util.Dom.getElementsByClassName lets you specify a class name as the first parameter instead of a method:

elements = YAHOO.util.Dom.getElementsBy
(
   method,
   "img",
);
Working with attributes

The following method provides a uniform way to get the value of an attribute for the first element of elements (the src attribute in this case) across the major browsers:

value = YAHOO.util.Dom.getAttribute
(
   elements[0],
   "src"
);

The following method provides a uniform way to set the value of an attribute for the first element in elements (the src attribute in this case) across the major browsers:

YAHOO.util.Dom.setAttribute
(
   elements[0],
   "src",
   "http://.../slide_arrow_l.gif"
);
Working with styles

The following method gets the value of a style for the first element in elements (the color property in this case):

value = YAHOO.util.Dom.getStyle
(
   elements[0],
   "color"
);

The following method sets a style for the first element in elements (the backgroundColor property in this case). Use CamelCase for properties that are hyphenated:

YAHOO.util.Dom.setStyle
(
   elements[0],
   "backgroundColor",
   "#f0f0f0"
);

Working with Events

Much of the behavior layer in large web applications is dedicated to handling events. Unfortunately, event handling in web browsers can be a source of inconsistency and trouble. In addition, event handlers that perform more than just simple actions tend to be hard to maintain. Event handlers often use too much global data, and many applications would have better modularity if they used custom events to become even more event-driven.

Event Handling Normalization

To address the inconsistencies in the way that web browsers handle events, the YUI library (as well as the other libraries introduced earlier) provides an interface that offers uniform event handling across the different browsers. The following YUI method lets you register an event handler:

YAHOO.util.Event.addListener
(
   element,
   type,
   handler,
   object,
   overrideContext
);

The method accepts up to five parameters. The final two parameters are optional, but we will see in a moment that they provide some very important capabilities for large web applications. Here is a description of each parameter:

element

The element for which the event handler is being registered. This can be an ID, an element reference, or an array of IDs and elements to which to assign the event handler.

type

The type of the event. Specify the event type as the same string as you would use in an HTML element but without the on prefix (i.e., use "click" instead of "onclick").

handler

The method that the event invokes.

object

An object to pass as the second parameter to the handler (the first parameter passed to the handler is always the event object itself).

overrideContext

If set to true, object becomes the execution context for the handler (referenced by this in the handler). If you specify an object for this parameter, this object becomes the execution context for the handler.

A Bad Example: Global Data in Event Handlers

To address the issue of too much global data in event handlers, let’s first look at why this is often the case. Essentially, web developers tend to write event handlers that use a lot of global data because the vast majority of event handlers are defined globally themselves. That is, they are members of the window object instead of an object that is more specific to what they really need to do. This tendency is understandable, because an event handler may be called upon to handle an event at any time as an application runs and its data needs to be in a scope that’s accessible; however, such a broad scope leads to unwieldy code over time. As a starting point, Example 5-3 illustrates the common approach for event handling with global data found in many web applications.

Example 5-3. Global data in an event handler
var State1;
var State2;

...

YAHOO.util.Event.addListener
(
   element,
   "click",
   slideL,
);

...

function slideL()
{
   // Since an event handler can be called at any moment, the tendency
   // is to use global data since it's always in scope.
   alert(State1);
   alert(State2);
}

A Good Example: Object Data in Event Handlers

Now let’s look at a better approach for event handlers, which uses object data. This involves defining an event handler as a method of the object that it affects and defining the data required by that event handler within the object. This is especially beneficial if you consider that many objects in the modular JavaScript we’re presenting in this chapter are created to provide a layer of behavior for a specific module in the user interface. Therefore, modularity is enhanced by implementing the event handlers for that module as part of the object itself. (For JavaScript that affects multiple modules on a page, define an object for the page, which implements its own handlers for that level of event handling.)

Example 5-4 implements an event handler as a method of the PictureSlider object. During event registration with YUI, the code sets up the object itself as the execution context for the event handler so that the event handler has access to all the module’s data members.

Example 5-4. Object data in an event handler
PictureSlider = function()
{
   this.state1 = ...;
   this.state2 = ...;

   ...

   if (this.slider)
   {
      // Create placeholders for left and right buttons, but only if
      // there are more slides than can fit the width given for them.
      this.lb = YAHOO.util.Dom.getElementsByClassName
      (
         "btnl",
         "img",
         this.slider
      );

      this.lb = (this.lb && this.lb.length > 0) ? this.lb[0] : null;

      ...

   }

   YAHOO.util.Event.addListener
   (
      this.lb,
      "click",
      this.slideL,
      this,
      true
   );

   ...
};

PictureSlider.prototype = new Object();

PictureSlider.prototype.slideL = function()
{
   // Because when we registered this event handler we specified that
   // PictureSlider should be the execution context, we have access here
   // to everything encapsulated in the object using the this pointer.
   alert(this.state1);
   alert(this.state2);
};

Event-Driven Applications

A good way to achieve further modularity within the JavaScript of a large web application is to design your application to be more event-driven. This means using events instead of method calls directly to communicate between modules. Event-driven applications define custom events to describe what modules are doing and trigger the proper events at various points to communicate what has happened. Other parts of the application listen for those events and respond by doing whatever they need to do.

The reason that event-driven applications tend to be more modular than applications tied together by method calls directly is that firing events and handling events are independent actions. That is, you can fire a new event or add or remove a handler without directly affecting other parts of the system. In addition, JavaScript and its libraries’ support for event handling, presented earlier, give you everything you need to abstract the tracking of custom events and their delivery to the correct recipients. You just have to define which events you need and write the code to trigger or handle the events at the right places.

Suppose you wanted to use a custom event to indicate that there was a change of location (e.g., a postal code) associated with a web page after it was loaded. This could occur after a visitor types a new location into an input somewhere, or for any number of other reasons. Whatever the case, various modules on the page may want to know about the change so that they can update themselves for the new location (e.g., a list of new cars might need to update itself via Ajax with different listings).

Custom events usually provide a way to pass data (sometimes called a payload) along with the event to provide additional details. For example, in the case of a location change, you would likely want a handler to know what the location was changed to, and possibly what the location was previously. You can learn more about custom events from the YUI library at http://developer.yahoo.com/yui.

Working with Animation

Many web developers think that you need to use Flash to achieve animation effects in web browsers. In actuality, you can accomplish a great number of animated effects using JavaScript. This section presents a few examples of some common types of animations that may be helpful in large web applications.

Motion Animation

A motion animation changes the position of an element over the course of time. You can perform a motion animation with the YUI library using YAHOO.util.Motion. Example 5-5 sets up a motion animation that moves an element specified by element over a span of one second, with easing that starts quickly and decelerates toward the end (YAHOO.util.Easing.easeOut). The example illustrates a few ways to move an element using the parameter for attributes:

attr1

Move the element from its current position to position 450,0 on the page. You can also specify from to use a starting point that is different from the current position.

attr2

Move the element from its current position 450 pixels to the right relative to its current position. You can also specify from to use a starting point that is different from the current position.

attr3

Move the element from its current position to position 450,0 on the page and move via a smooth curve through point 200,200. You can also use from as usual.

Once you have set up the animation using YAHOO.util.Motion, start the animation by calling animate on the object that was returned.

Example 5-5. Setting up a motion animation
attr1 =
{
   points: {to: [450, 0]}
};

attr2 =
{
   points: {by: [450, 0])
};

attr3 =
{
   points: {to: [450, 0], control: [[200, 200]])
};

animation = new YAHOO.util.Motion
(
   element,
   attr1,
   1,
   YAHOO.util.Easing.easeOut
);

...

animation.animate();

Sizing Animation

A sizing animation changes the width and height of an element over the course of time. You can perform a sizing animation with the YUI library using YAHOO.util.Anim. Example 5-6 sets up a sizing animation that resizes an element specified by element over a span of one second, with easing that starts slowly, speeds up, and decelerates again toward the end (YAHOO.util.Easing.easeBoth). The example illustrates a few ways to resize an element using the parameter for attributes:

attr1

Resize the element from its current width to 100 pixels. You can also specify from to use a starting size that is different from the current size.

attr2

Resize the element from its current width up by 100 pixels. You can also specify from to use a starting size that is different from the current size.

Once you have set up the animation using YAHOO.util.Anim, start the animation by calling animate on the object that was returned.

Example 5-6. Setting up a sizing animation
attr1 =
{
   width: {to: 100}
};

attr2 =
{
   width: {by: 100)
};

animation = new YAHOO.util.Anim
(
   element,
   attr1,
   1,
   YAHOO.util.Easing.easeBoth
);

...

animation.animate();

Color Transition

A color transition changes various color properties of an element over the course of time. You can perform a color transition using YAHOO.util.ColorAnim. Example 5-7 sets up a color transition that colors an element specified by element over a span of one second, with easing that starts slowly and accelerates toward the end (YAHOO.util.Easing.easeIn). The example illustrates a few ways to transition the color of an element using the attributes parameter:

attr1

Transition the background color for the element from its current color to #fff. You can also specify from to use a starting color that is different from the current color.

attr2

Transition the foreground color for the element from #fff to #000. This setup demonstrates how to use the from property.

Once you have set up the animation using YAHOO.util.ColorAnim, start the animation by calling animate on the object that was returned.

Example 5-7. Setting up a color transition
attr1 =
{
   backgroundColor: {to: "#fff"}
}

attr2 =
{
   color: {from: "#fff", to: "#000"}
}

animation = new YAHOO.util.ColorAnim
(
   element,
   attr1,
   1,
   YAHOO.util.Easing.easeIn
);

...

animation.animate();

An Example: Chained Selection Lists

One example of highly modular JavaScript is an implementation for chained selection lists. These provide a dynamic way to organize selection lists in which a selection in one list causes a new set of options to be loaded in subsequent lists. For example, imagine selecting a make-model-trim 3-tuple for a car (e.g., BMW, 09 3-Series, 335i) from a set of selection lists. Rather than having a huge list of all make-model-trim combinations, a better user experience is to have three selection lists: one for the makes, one for the models, and one for the trims. Once you select a make, the model selection list is populated with models for just that make. Once you select a model, the trim selection list is populated with trims available for that make-model combination. Figure 5-1 shows three states of chained selection lists for cars: a) the initial state, b) after selecting a make, and c) after selecting a model.

Chained selection lists in three states
Figure 5-1. Chained selection lists in three states

One way to implement chained selection lists is to create multiple instances of the same selection object, which we’ll call MultiSelect, and link them together so that each can respond to changes in the others based on their positions in the chain. Example 5-8 shows the HTML for the chained selection lists in Figure 5-1. It also shows the JavaScript for setting up the chain of selection lists.

Example 5-8. HTML for chained selection lists
<body>

...

<div id="nwcsel">
   <form action="..." method="GET">
      <div id="makesel">
      </div>
      <div id="modelsel">
      </div>
      <div id="trimsel">
      </div>
      <input class="nwcbtnsub" type="submit" value="Go" />
   </form>
</div>

...

<!--
   Before the JavaScript for MultiSelect, you need to link files required
   by the MultiSelect implementation. Chapter 7 presents techniques for
   ensuring that everything a module requires (the HTML, CSS, JavaScript,
   etc.) is able to travel with the module wherever the module is used.
-->
<script src="http://..." type="text/javascript"></script>
...

<script type="text/javascript">
// Set up the name of the service for populating the chained selections.
var proc = "...";

// Create the make selection list.
var makeSelect = new MVC.MultiSelect
(
   "Select Make",
   proc + "?req=mk",
   "makesel",
   "mk"
);

// Chain the model selection list.
var modelSelect = new MVC.MultiSelect
(
   "Select Model",
   proc + "?req=md",
   "modelsel",
   "md",
   makeSelect
);

// Chain the trim selection list.
var trimSelect = new MVC.MultiSelect
(
   "Select Trim",
   proc + "?req=tr",
   "trimsel",
   "tr",
   modelSelect
);

// Once selection lists in the chain have been created, initialize them.
makeSelect.init();
modelSelect.init();
trimSelect.init();
</script>
</body>

The JavaScript for the MultiSelect component uses Ajax and the MVC (Model-View-Controller) design pattern, which are presented in more detail in Chapter 8. MVC and Ajax work together to let us make a new request for data each time a selection list should be updated. This is far better than loading all combinations when the page first loads, since all the combinations require a lot of data and most go unused. Because the MultiSelect component is a generic component that uses MVC, a logical place to include it might be in the MVC library. Therefore, Example 5-9 shows that we have placed the component within the MVC namespace.

Example 5-9. JavaScript for the MultiSelect object
// Place the component within the MVC namespace; create it if needed.
if (!window.MVC)
{
   MVC = {};
}

MVC.MultiSelect = function(text, url, id, name, prev)
{
   // Pass a string for the selection list in text, or pass null for a
   // default label; url is the URL to fetch options, id is the ID of
   // the container in which to place the selection list, name is the
   // select name attribute, and prev is the predecessor list, or null.
   t = (text) ? text : null;
   p = (prev) ? prev.view : null;

   this.model = new MVC.MultiSelectModel(t, url);
   this.view  = new MVC.MultiSelectView(name, p);
   this.view.attach(this.model, id);
}

MVC.MultiSelect.prototype.init = function()
{
   // Call this method once the chain of selections has been set up.
   this.model.init();
}

MVC.MultiSelect.prototype.getSelect = function()
{
   // Return the select element currently in use by the selection list
   // so that you can make whatever customizations are needed (e.g., you
   // can append your own handlers or perform various DOM operations).
   return this.view.getSelect();
}

One of the most important features of the implementation for the MultiSelect component is that it is implemented in a generic and modular fashion. You can chain any number of them together and they work with any type of data, provided the data is returned from the server in a data structure like that shown in Example 5-10. This is an example of JSON (JavaScript Object Notation), presented in Chapter 6. This data structure is simply an array of objects with two members: value and text. Each pair represents one option in the list: value is the hidden value for each option, and text is the displayable string.

Example 5-10. The data structure for loading chained selection lists
{
   "options" :
   [
      {
         "value": "bmw",
         "text":  "BMW"
      },
      {
         "value": "honda",
         "text":  "Honda"
      },
      {
         "value": "toyota",
         "text":  "Toyota"
      }
   ]
}

Examples 5-11 and 5-12 present the implementation details for chained selection lists using JavaScript. Together, the examples illustrate many of the ideas presented in this chapter: the use of objects for namespacing, methods for accessing and modifying the DOM, and event handling using object data instead of global data, among others. Fundamentally, the implementation works by maintaining a model and a view for each selection list in the chain.

Example 5-11 shows the implementation for MultiSelectModel. This is the model responsible for storing the current set of options for one selection list in the chain. Whenever the model changes, it notifies the view attached to the model automatically so that the view can update itself with the new options.

You’ll see in Chapter 8 that in MVC, a model tells a view to update itself by calling the view’s update method. For now, you just need to know that this happens as follows: setState sets the data in the model; it then calls notify in the model; notify then calls update for each view attached to the model. The setState and notify methods are defined by the Model prototype object, so you will not see them defined in the examples here.

Example 5-11. MultiSelectModel object for the chained selection list implementation
MVC.MultiSelectModel = function(text, url)
{
   MVC.Model.call(this);

   // All selection lists use an empty string as the marker for the
   // label appearing in the selection list before an option is chosen.
   this.labelValue = "";

   if (text)
      this.labelText = text;
   else
      this.labelText = "Select";

   // This is the URL to contact for loading options.
   this.proc = url;

   // If the model ends up being the model for the first selection list
   // in the chain, the view with this model will set this member.
   this.firstModel = false;
}

MVC.MultiSelectModel.prototype = new MVC.Model();

MVC.MultiSelectModel.prototype.init = function()
{
   if (this.firstModel)
   {
      // Initialize options for the first selection list in the chain.
      this.setState("GET", this.proc);
   }
   else
   {
      // Initialize other selection lists to an empty array of options.
      // Do the view notification explicitly since we're not using the
      // setState method here (which would do the notification itself).
      this.state.options = new Array();
      this.notify();
   }
}

MVC.MultiSelectModel.prototype.abandon = function()
{
   alert("Timeout occurred while trying to load selection options.");
}

MVC.MultiSelectModel.prototype.recover = function()
{
   alert("Problem occurred while trying to load selection options.");
}

Example 5-12 shows the implementation for MultiSelectView. This is the view object for one selection list in the chain. MultiSelectView defines the update method invoked by a model whenever there is a new set of options to render. It also defines changeHandler, an event handler for whenever the selected value in the selection list changes. When the selection changes, the view sets the next model in the chain to a new set of selection list options. It does this in changeHandler by calling setState for the next model. This, in turn, produces a call to the update method of the view attached to that model. MultiSelectView creates a select element if the view doesn’t already have one in the HTML.

Example 5-12. MultiSelectView object for the chained selection implementation
MVC.MultiSelectView = function(n, p)
{
   MVC.View.call(this);

   this.name = n;

   if (p)
   {
      // The selection list is not first in the chained selections.
      this.prev = p;
      p.next = this;
      this.disabled = true;
   }
   else
   {
      // This selection list has no predecessor, so it's the first one.
      this.prev = null;
      this.next = null;
      this.disabled = false;
   }
}

MVC.MultiSelectView.prototype = new MVC.View();

MVC.MultiSelectView.prototype.attach = function(m, i)
{
   // This method hooks up a view to its data source, which is a model.
   MVC.View.prototype.attach.call(this, m, i);

   // If the view has no predecessor view, it must be first in the chain.
   if (!this.prev)
      this.model.firstModel = true;

   this.container = document.getElementById(this.id);
}

MVC.MultiSelectView.prototype.update = function()
{
   // Called when a change in the model takes place. Render new options.
   var select = this.getSelect();

   // Remove any existing select element not created by the view.
   if (select && !YAHOO.util.Dom.hasClass(select, "mltsel"))
   {
      select.parentNode.removeChild(select);
      select = null;
   }

   // Insert a new select only the first time the view is being managed.
   if (!select)
   {
      select = document.createElement("select");
      YAHOO.util.Dom.addClass(select, "mltsel");
      select.setAttribute("name", this.name);

      YAHOO.util.Event.addListener
      (
         select,
         "change",
         this.changeHandler,
         this,
         true
      );

      // Insert the select element for the selection list into the DOM.
      if (this.container)
         this.container.appendChild(select);
   }

   if (this.disabled)
      select.disabled = true;
   else
      select.disabled = false;

   var o;
   var options;
   var count;

   // Start the options with the model's label for the selection list.
   select.options.length = 0;
   o = new Option(this.model.labelText, this.model.labelValue);
   select.options[select.options.length] = o;

   options = this.model.state.options;
   count = options.length;

   // Load the rest of the selection list remaining with the options.
   for (var i = 0; i < count; i++)
   {
      o = new Option(options[i].text, options[i].value);
      select.options[select.options.length] = o;
   }
}

MVC.MultiSelectView.prototype.changeHandler = function(e)
{
   // Handle changes in one of the selection lists by adjusting others.
   var select = this.getSelect();
   var option = select.options[select.selectedIndex].value;

   if (option == "")
   {
      // The selection list has been set back to its initial state;
      // selection lists beyond it in the chain must be reset as well.
      this.reset(this.next);
   }
   else
   {
      if (this.next)
      {
         // Use Ajax to get options for the next selection in the chain.
         if (this.next.model.proc.indexOf("?") == -1)
            option = "?value=" + option;
         else
            option = "&value=" + option;

         this.next.model.setState("GET", this.next.model.proc + option);
         this.next.enable();

         // Move to the next selection list in the chain and reset all
         // views beyond it (when a choice has been made out of order).
         var iter = this.next;

         if (iter)
            this.reset(iter.next);
      }
   }
}

MVC.MultiSelectView.prototype.reset = function(view)
{
   // Initialize all selection lists after the given one in the chain.
   var iter = view;

   while (iter)
   {
      iter.model.init();
      iter.disable();
      iter = iter.next;
   }
}

MVC.MultiSelectView.prototype.enable = function()
{
   var select = this.getSelect();

   this.disabled = false;

   if (select)
      select.disabled = this.disabled;
}

MVC.MultiSelectView.prototype.disable = function()
{
   var select = this.getSelect();

   this.disabled = true;

   if (select)
      select.disabled = this.disabled;
}

MVC.MultiSelectView.prototype.getSelect = function()
{

   var elements;

   // Retrieve the current select element used by the selection list.
   if (this.container)
      elements = this.container.getElementsByTagName("select");
   else
      return null;

   if (elements.length > 0)
      return elements[0];
   else
      return null;
}

A logical extension of this implementation for chained selection lists is to use it to build highly reusable modules for different types of chained selection lists you might need to support around a large web application. For example, you could build a New Cars Selection module for anywhere you need a make-model-trim 3-tuple for new cars. This module would bundle the generic chaining behavior presented in the preceding examples with the HTML and CSS to make it a fully reusable component. We’ll learn more about this encapsulation for modules in Chapter 7.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset