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.
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.
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 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 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.
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
).
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.
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) { ... }
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.
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"); ... };
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.
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.
The following method returns an array of HTML elements
that are img
tags enclosed by the
invoking element (element
):
elements = element.getElementsByTagName("img");
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");
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);
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.
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.
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" );
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" } );
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" } );
The jQuery JavaScript library has especially good documentation. You can download the library and read its complete documentation at http://www.jquery.com.
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" );
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" } );
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" } );
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.
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" );
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" } );
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" } );
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.
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.
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", );
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" );
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" );
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.
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
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.
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.
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); }
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.
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); };
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.
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.
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.
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.
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.
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.
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.
<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.
// 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.
{ "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.
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.
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.