Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ajax In Action (2006).pdf
Скачиваний:
63
Добавлен:
17.08.2013
Размер:
8.36 Mб
Скачать

Refactoring 545

Figure 13.18 RSSItemView result

(Remember, $() is a function provided by Prototype for retrieving DOM elements by their ID.) Now that we have a good set of abstractions for our Model classes and our View, let’s tackle the RSS reader Controller that ties all the pieces together.

13.7.3RSS reader Controller

The RSSReader class will perform functions related to manipulation of the Model and View classes to coordinate all of the activities associated with the reader. Recall that the RSS reader provides a slideshow-type interface to the feeds where each article is presented for a certain period of time, and then a transition effect is created to move from one article to the next. Buttons are provided to move backward and forward within the articles, as well as pause and resume the slideshow. Finally, a select list and an add button are provided to add supplemental feeds to the initial set of RSS feeds in the list. The RSS reader has to perform five categories of behaviors to implement these features, as outlined here:

Set configuration options

546CHAPTER 13

Building stand-alone applications with Ajax

Constructing objects and initial setup

Creating slideshow functionality

Creating the transition effects

Loading the RSS feeds via Ajax

Updating the UI

To reduce the complexity and amount of code required to do all of this, we’ll use the Prototype library for syntactical brevity, the Rico library to provide the functionality for our transition effects, and the net.ContentLoader for the Ajax support. Let’s tackle the initial construction and setup first.

Construction and setup

The de facto starting point for our component development has been constructors. Let’s stick with that practice here and start by defining our constructor. The constructor in this case is a simple method to set the initial defaults for some of the component state and, as in other examples, to set our configuration options and perform behavior initialization. With that in mind, our RSSReader constructor is defined as shown in listing 13.27.

Listing 13.27 The RSSReader constructor

RSSReader = Class.create();

RSSReader.prototype = {

initialize: function( readerId, options ) {

this.id

= readerId;

 

this.transitionTimer = null;

b Set default

this.paused

=

false;

values

this.visibleLayer

=

0;

 

this.setOptions(options); c this.start(); d Initialize behavior

},

...

};

The constructor takes two arguments: an ID and an options object. The ID is used as a unique identifier for the reader and is used as a prefix for the IDs of the buttons that it will need to identify from the DOM. This will be shown shortly in the applyButtonBehaviors method. The first thing the constructor does b is to set the default values for its state. Next, the options object, as with most of the components we’ve written, is used c to specify configuration options to the

Refactoring 547

component. This is done via the setOptions method. Finally, everything that needs to happen to bring the component to life happens in the start method d. Let’s ponder configuration first, and then we’ll move on to behavior.

Our boilerplate configuration idiom, setOptions, shown in listing 13.28, provides the configuration. Let’s establish the setOptions implementation now and talk about our configuration options.

Listing 13.28 The setOptions method

setOptions: function(options) { this.options = {

slideTransitionDelay: 7000,

fadeDuration

: 300,

errorHTML

: '<hr/>Error retrieving content.<br/>'

}.extend(options);

 

},

The properties we’ve decided to make configurable in our RSS reader are indicated within the setOptions method shown here. The slideTransitionDelay property specifies the number of milliseconds an article’s “slide” is visible before transitioning to the next one. The fadeDuration property specifies the amount of time in milliseconds it takes to fade out and subsequently fade in the next slide. Finally, if an error occurs while loading an RSS feed, the errorHTML property specifies the HTML to display as an error message. The defaults for these values, if not explicitly overridden by the user, are shown in this code. It’s worth noting here that the component will expect an rssFeeds property of the options object to be passed in as the initial set of feeds to peruse. Because we can’t really assume a reasonable default for this value, it’s not defaulted within the setOptions method. The intent is that a reader will be created with an options object similar to the example shown here:

var options = {

rssFeeds: [ "http://radio.javaranch.com/news/rss.xml", "http://radio.javaranch.com/pascarello/rss.xml", "http://radio.javaranch.com/bear/rss.xml", "http://radio.javaranch.com/lasse/rss.xml" ] };

var rssReader = new RSSReader('rssReader', options );

With creation and configuration quickly coded, it’s time to peek behind the curtain at the magical start method that kicks everything off. We’ll look at it briefly and cover its implications in the relevant sections that follow. Let’s start by illustrating the implementation shown in listing 13.29.

548CHAPTER 13

Building stand-alone applications with Ajax

Listing 13.29 The start method

start: function() { this.applyButtonBehaviors();

new Effect.FadeTo( this.getLayer(1), 0.0, 1, 1, {} ); this.loadRSSFeed(0,true);

this.startSlideShow(false);

},

The applyButtonBehaviors method sets up the onclick handlers for the previous, pause, next, and Add Feed buttons. This is the next method we’ll discuss. The fade effect on the second line fades out the visible div element so that when the first slide is loaded it can be faded in. Note that in this implementation, we’re using an effect provided by Rico rather than writing our own, which reduces the amount of code we have to write, debug, and maintain. The loadRSSFeed method initiates the Ajax request to load in the first feed, and the startSlideShow method starts a timer with the value of the slideTransitionDelay to initiate the slideshow. The loadRSSFeed method will be explored in more detail in the “Loading RSS feeds with Ajax” section (page 556), and the startSlideShow method will be dissected in the “Slideshow functionality” (page 549) section. As promised, we’ll close our discussion of construction and setup by looking at the applyButtonBehaviors method in listing 13.29.

The applyButtonBehaviors method, as mentioned previously, hooks the buttons up to methods that implement their behaviors. The implementation is shown in listing 13.30.

Listing 13.30 The applyButtonBehaviors method

applyButtonBehaviors: function() {

 

 

$(this.id + '_prevBtn').onclick

= this.previous.bind(this);

$(this.id + '_nextBtn').onclick

= this.next.bind(this);

$(this.id + '_pauseBtn').onclick

= this.pause.bind(this);

$(this.id + '_addBtn').onclick

= this.addFeed.bind(this);

},

 

 

 

 

 

 

 

 

Let’s start with some refresher notes about the syntax and idioms being used here. We’re using a couple of syntactical elements of the Prototype library. First, the $ method, as you will recall, can be thought of as a call to document.getElementById. Second, the bind method implicitly creates a closure for us so that the onclick handler for each button can call first-class methods of our component. Now to the details of the implementation.

Refactoring 549

The implementation reveals an implicit contract between the component and the HTML markup for the reader. The component is constructed with an ID that it stores in its this.id attribute. The ID is then used as a prefix to find various elements within the markup. In this case, the IDs of the buttons are assumed to be the ID passed into the constructor, followed by _prevBtn, _nextBtn, _pauseBtn, and addBtn. To illustrate this, in the example construction just mentioned that uses rssReader for the ID, the component expects the buttons to be specified as follows:

<input type="button" id="rssReader_prevBtn"

value=" <<

"

/>

<input type="button" id="rssReader_pauseBtn"

value="

| | " />

<input

type="button"

id="rssReader_nextBtn"

value="

>>

"

/>

<input

type="button"

id="rssReader_addBtn"

value="Add

Feed" />

Now that our RSSReader controller is starting to take shape, let’s take a look at the implementation details of providing the slideshow behavior.

Slideshow functionality

Now would probably be a good time to talk about a change in semantic from our previous version of the script. In our first version of the RSS reader, we loaded all of the RSS feeds into memory at start time and then just transitioned through our in-memory representation. This had the advantage of simplicity but the decided disadvantage of not being very scalable. If we have dozens or even hundreds of RSS feeds that we read on a regular basis, each with dozens of articles, preloading them all would bring our browser to its knees. So in this refactoring, we’ll take the opportunity to improve the scalability and performance of our RSS reader by changing our semantic to load only a single RSS feed into memory at a time. All of the RSSItems of a single feed will be in memory, but only a single RSSFeed will be in memory at a time. Three attributes of the Controller keep track of where the slideshow is in its list of displayable content. These are outlined in table 13.4.

Table 13.4 Attributes of the Controller

Attribute

Purpose

 

 

this.currentFeed The RSSFeed instance currently loaded into memory.

this.feedIndex The index of the currently visible feed. This is an index into the this.options.rssFeeds array.

this.itemIndex The index of the currently visible item. This is an index into the currently visible RSSFeed object’s internal items array.

550CHAPTER 13

Building stand-alone applications with Ajax

With that overview of semantic change, let’s ponder navigation. There are a number of methods that we must contemplate in order to navigate through each of the articles (item elements) of each one of the RSS feeds. Let’s consider previous/ next method pairs. A mechanism for moving forward and backward is needed not only to provide the implementation for the explicit button events but also for the passive perusal via the automated slideshow.

Let’s start by looking at the boolean method pair that tells the reader whether it can move forward or backward. These two methods, hasPrevious and hasNext, are shown in listing 13.31.

Listing 13.31 The hasPrevious/hasNext method pair

hasPrevious: function() {

return !(this.feedIndex == 0 && this.itemIndex == 0);

},

hasNext: function() {

return !(this.feedIndex == this.options.rssFeeds.length - 1 && this.itemIndex == this.currentFeed.items.length - 1);

},

These methods will be used in the previous and next processing to determine whether a previous or next slide is available. As implemented here, a previous slide is available unless we are on the first item of the first feed, and a next slide is available unless we are on the last item of the last feed.

Now let’s examine what it means to move backward and forward. Let’s start with the previous() method, shown in listing 13.32.

Listing 13.32 The previous() method

previous: function() {

if ( !this.hasPrevious() ) return;

var requiresLoad = this.itemIndex == 0;

this.fadeOut( this.visibleLayer, Prototype.emptyFunction ); this.visibleLayer = (this.visibleLayer + 1 ) % 2;

if ( requiresLoad )

this.loadRSSFeed( this.feedIndex - 1, false ); else

setTimeout( this.previousPartTwo.bind(this), parseInt(this.options.fadeDuration/4) );

},

Refactoring 551

previousPartTwo: function() { this.itemIndex--; this.updateView();

},

The first thing the previous() method does is to put a guard condition at the beginning of the method. If there isn’t a previous content item, then previous() just returns without performing any action. If the requiresLoad value is true, then the RSS content for the item being navigated to isn’t loaded yet. When moving backward as we are here, a load is required if we’re currently on the first item of a feed. The previous RSS feed will have to be loaded in order to be displayed. The fade-out method, which we’ll examine in the “Transition effects” section on page 554, fades out the visible layer. What the method does next depends on whether or not it needs to load some content before it can display it. If we have to load content, then we initiate the load of that content via the loadRSSFeed() method. The first parameter is the index of the feed to be loaded, and the second parameter is a boolean value indicating a forward direction (false in this case). But if the content is already loaded, we call previousPartTwo() after a delay of one fourth of the overall fadeDuration. The “part two” of the method simply updates the itemIndex property and then calls updateView(), which fades in the appropriate slide.

Confused? Well, what’s going on is that if the content that needs to be displayed isn’t loaded, then the load is initiated immediately, which causes an update of the UI as soon as the response comes back. The time it takes for the response to come back provides a natural delay for the fade-in! On the other hand, if the content is already loaded (that is, we’re looking at a different article in the same RSS feed that’s loaded), then we intentionally delay by a quarter of the fade duration before we fade-in the next slide. Pretty slick, huh?

The next() method, shown in listing 13.33, is an inverse of the algorithm described previously.

Listing 13.33 The next() method

next: function() {

if ( !this.hasNext() ) return; var requiresLoad =

this.itemIndex == (this.currentFeed.items.length - 1); this.fadeOut( this.visibleLayer, Prototype.emptyFunction ); this.visibleLayer = (this.visibleLayer + 1 ) % 2;

if ( requiresLoad )

this.loadRSSFeed( this.feedIndex + 1, true );

552CHAPTER 13

Building stand-alone applications with Ajax

else

setTimeout( this.nextPartTwo.bind(this), parseInt(this.options.fadeDuration/4) );

},

nextPartTwo: function() { this.itemIndex++; this.updateView();

},

Look familiar? The next() method reverses the logic in terms of indexing but otherwise is identical to the algorithm shown previously. Note that the previous()/next() method pairs toggle the visible layer with each transition from one slide to the next with the expression

this.visibleLayer = (this.visibleLayer + 1) % 2;

This just tells the code that ultimately updates the UI as a result of the content load or the explicit call to updateView() into which layer to put the result. Recall that the content area of the reader has HTML markup that looks something like the following:

<!-- Content area -->

<div class="content" id="rssReader_content"> <div class="layer1">Layer 0</div>

<div class="layer2">Layer 1</div> </div>

The visibleLayer is just an integer property that keeps track of into which div to put content. An index of 0 tells the UI update to put the content into Layer 0. A value of 1 indicates to put the content into Layer 1.

Now that we have the methods in place to provide forward and backward functionality, we can use these to create our slideshow methods. Let’s dissect those now. The startSlideShow method, which you will recall was invoked from our start() method, and its companion nextSlide() are shown in listing 13.34.

Listing 13.34 The slideshow navigation methods

startSlideShow: function(resume) {

var delay = resume ? 1 : this.options.slideTransitionDelay; this.transitionTimer = setTimeout(

this.nextSlide.bind(this), delay );

},

nextSlide: function() { if ( this.hasNext() )

this.next();

loadRSS-

Refactoring 553

else

this.loadRSSFeed(0, true);

this.transitionTimer = setTimeout( this.nextSlide.bind(this), this.options.slideTransitionDelay );

},

Our startSlideShow method just calls nextSlide on a delay. The delay is either the slideTransitionDelay or a single millisecond (effectively immediate), based on whether or not we’re resuming the slideshow after having paused it. The nextSlide method is equally uncomplicated. It just calls our next() method as long as there is another slide available. If we’re at the last slide, Feed(0,true) is called to wrap back to the beginning. It then just sets a timer to repeat the process. Piece of cake!

We mentioned that we can pause the slideshow via the pause button, but we haven’t implemented that method yet. Let’s do that now. This is shown in listing 13.35.

Listing 13.35 The pause method

pause: function() { if ( this.paused )

this.startSlideShow(true); else

clearTimeout( this.transitionTimer ); this.paused = !this.paused;

},

The pause method toggles the paused state of the slideshow. This is tracked by the boolean attribute this.paused. If the slideshow is already paused, the pause method calls startSlideShow, passing true as the resume property; otherwise it clears the transitionTimer attribute, which suspends all slide transitions until the pause button is clicked again.

The final piece related to our slideshow functionality is to allow the slideshow to be augmented with additional RSS feeds via a select box and add button. We saw in the applyButtonBehaviors() function that the add button calls the addFeed method. Let’s implement that to round out our slideshow functionality (see listing 13.36).

554CHAPTER 13

Building stand-alone applications with Ajax

Listing 13.36 The addFeed method

addFeed: function() {

var selectBox = $(this.id + '_newFeeds'); var feedToAdd = selectBox.options[

selectBox.selectedIndex ].value; this.options.rssFeeds.push(feedToAdd);

},

This method also relies on an implicit contract with the HTML markup in terms of a naming convention for the select box of additional RSS feeds. The ID of the select box should be the ID of the reader with the suffix _newsFeeds. The method simply takes the selected RSS feed in the select box and appends it to the end of the this.options.rssFeeds array. Nothing more required! Don’t you love it when adding functionality can happen in just a few lines of code?

This rounds out all of the slideshow-related methods. Let’s now briefly look at the methods supporting our transition effects.

Transition effects

There are a few methods that we’ve already referenced that support our fade transitions between slides. Let’s take a moment to decipher transitions. First, we defined a fadeIn() and fadeOut() method pair, as shown in listing 13.37.

Listing 13.37 The fadeIn()/fadeOut() method pair

fadeIn: function( layer, onComplete ) { this.fadeTo( 0.9999, layer, onComplete );

},

fadeOut: function( layer, onComplete ) { this.fadeTo( 0.0001, layer, onComplete );

},

These two methods both delegate to the fadeTo() method (shown next). They pass to the fadeTo() method an opacity value between 0 and 1—0 indicating the layer is invisible, 1 indicating the layer is completely visible. A value that is mathematically very close to 1 without actually being 1 seemed to cause less flicker in some browsers, which is why we used 0.9999 instead of 1. The layer number (0 or 1) is passed to indicate which layer to fade, and finally a function is passed in that provides a completion callback hook once the fade has completed. The fadeTo() method is implemented as shown in listing 13.38.

Refactoring 555

Listing 13.38 The fadeTo() method

fadeTo: function( n, layer, onComplete ) { new Effect.FadeTo( this.getLayer(layer),

n,

this.options.fadeDuration,

12,

{complete: onComplete} );

},

In a bout of utter laziness, or perhaps as a methodical well-thought-out strategy, we decided not to reinvent a fade effect. Instead, we use the Effect.FadeTo provided by the Rico library to perform the fancy fading magic for us. The Effect.FadeTo parameters are illustrated in table 3.5.

Table 13.5 The Effect.FadeTo parameters

Parameter

Description

 

 

this.getLayer(layer)

The DOM element to fade

 

 

n

An opacity value between 0 and 1

 

 

this.options.fadeDuration

How long it should take the fade to occur

 

 

12

The number of steps in the fade

 

 

{complete: onComplete}

The completion callback to call once done

 

 

We use the helper method getLayer() to get the div element corresponding to the content layer to be faded. The getLayer() method is shown in listing 13.39.

Listing 13.39 The getLayer() method

getLayer: function(n) {

var contentArea = $(this.id+'_content'); var children = contentArea.childNodes; var j = 0;

for ( var i = 0 ; i < children.length ; i++ ) { if ( children[i].tagName &&

children[i].tagName.toLowerCase() == 'div' ) { if ( j == n ) return children[i];

j++;

}

}

return null;

},

556CHAPTER 13

Building stand-alone applications with Ajax

This method simply finds the content area, assuming its ID to be the ID of the reader with the value _content appended to the end. Once it finds the content element, it navigates to the children and finds the nth div child and returns it.

This finishes our treatment of transitions. Let’s now examine the topic of loading our RSS feeds via the magic of Ajax.

Loading RSS feeds with Ajax

We’ve given a fair amount of attention to the topics of creating a component and providing a rich slideshow semantic and fancy DHTML techniques for transitioning between slides. But without Ajax at the core, the fanfare would be for naught. The point is that it’s the synergy between Ajax, with its scalability and finegrained data retrieval, and sophisticated DHTML, with its rich affordances and effects, that provides a superior user experience. Okay, enough of the soapbox. Let’s look at some Ajax, starting with the method in listing 13.40 that loads an RSS feed into memory.

Listing 13.40 The loadRSSFeed method

loadRSSFeed: function(feedIndex, forward) { this.feedIndex = feedIndex; this.itemIndex = forward ? 0 : "last"; new net.ContentLoader(this,

this.options.rssFeeds[feedIndex], "GET", [] ).sendRequest();

},

This method uses our ever-familiar net.ContentLoader to make an Ajax request, passing the URL of an RSS feed as specified in the this.options.rssFeeds array. The forward parameter is a boolean specifying whether or not we’re loading in new content as a result of moving forward. Given this knowledge, the itemIndex property is updated accordingly. Note that itemIndex is given the value of last rather than an integer if we’re moving backward. That’s because we want itemIndex to indicate the index of the last item in the previous RSS feed. The only problem is that we don’t know how many items are in the feed, because it isn’t loaded yet.

You’ll recall that the ajaxUpdate and handleError methods are required as an implicit contract with the net.ContentLoader. We will look next at the ajaxUpdate method, shown in listing 13.41, to see how the implementation resolves our indexing dilemma.

Refactoring 557

Listing 13.41 The ajaxUpdate method

ajaxUpdate: function(request) {

if ( window.netscape && window.netscape.security.PrivilegeManager.enablePrivilege)

netscape.security.PrivilegeManager.enablePrivilege(

'UniversalBrowserRead');

this.currentFeed = RSSFeed.parseXML(request.responseXML.documentElement);

if ( this.itemIndex == "last" )

this.itemIndex = this.currentFeed.items.length - 1; this.updateView();

},

The ajaxUpdate method starts with a check to see if it’s running in an environment that provides a PrivilegeManager. If so, it asks to grant the UniversalBrowserRead privilege. As noted earlier, this is done so that our reader can run locally within a Mozilla-based browser.

The this.currentFeed is an instance of our RSSFeed model object that we defined in the Model section. It corresponds to the single RSSFeed loaded into memory, as populated from the Ajax response. If this.itemIndex has a value of last—as set by the loadRSSFeed method when moving backward—the itemIndex property is updated to contain the actual number of items in the newly loaded RSSFeed. Finally, the UI is updated via a call to updateView().

Let’s not forget to do our due diligence and define a handleError method both to satisfy our contract with the net.ContentLoader and because we really should do something to handle errors. If an RSS feed fails to load, we’ll just provide a “punt” message, as shown in our handleError implementation. More sophisticated implementations are certainly possible—and desirable.

handleError: function(request) { this.getLayer(this.visibleLayer).innerHTML =

this.options.errorHTML;

},

Now that our RSSReader is fully Ajax-ified, the only remaining piece of our puzzle it to write a couple of methods that handle updating the UI.

UI manipulation

Recall that early on we took the time to create Model and View classes to support our refactoring effort. Now that we’ve come to the portion of the Controller that