This chapter covers
In the last four chapters, we’ve developed a range of neat Ajax applications and shown the client-side and server-side code for each. In this chapter, we don’t need to present you with any server-side code, because we’re not going to write any. This may come as a surprise to most people, but we can run Ajax applications directly against any web server-based stream of data, including those that are generated by third-party applications. We can develop a rich user interface for a web page that acts like a client application.
Many websites offer XML feeds in the formats of Really Simple Syndication (RSS), Resource Descriptor Framework (RDF), or Atom, which are the three most common syndication feeds that we see. The information contained in these syndication feeds can be daily news articles, comics, weblogs (blogs), jokes, weather, and so on. With Ajax we have the ability to get the syndication information without having to visit these websites or buy a client application that will read the XML syndication feeds on our computers.
In this chapter, we develop our own client application, an XML reader that obtains feeds from various websites. It can be run in the browser on any computer that has an active Internet connection.
An XML syndication feed consists of articles that are freely available for the public to read and display on other websites. A blog is good example of a syndicated XML feed for our purposes because it is so widely available. These XML feeds are different from plain old articles on a website since they can be shared and displayed in multiple formats. It is like finding a newspaper or magazine that fits perfectly on your table as you eat your morning breakfast. You can eat and read at the same time without having to shift everything around and spilling your coffee. The syndicated feeds can be formatted in any style that fits your needs, allowing you to obtain just the information you want.
We can view the contents of an XML syndication feed in several ways. For instance, for a single blog, we could go to the website where the XML feed is located. To view several feeds at once, we could go to sites such as JavaCrawl.com, which allow us to view multiple XML feeds without going to multiple sites. We can also view the RSS feed in its raw unedited form by directly opening up the path of the XML file directly in our web browser, or we can use downloadable software to organize and format the feeds we want to see.
A new option is to use Ajax to view the syndication feeds. Without Ajax, we were stuck with downloading a client application or having to visit a website to grab this information, but with Ajax we do not have to download anything or visit a website. Instead we are able to develop a JavaScript-based RSS reader that can be run directly on our desktop with just a browser. In this chapter, we build an application that obtains multiple RSS feeds from multiple sources and allows us to view them in an orderly fashion. We also integrate transition effects to add to the flashiness of the user interface. But before we develop this application with Ajax, we need to understand the format of an XML feed and where we can find XML feeds.
One of the most popular places to find an XML feed is on a blog. Many people today publish blogs, for all kinds of reasons. They can contain information such as a personal diary, personal musings, news articles, jokes, or technology discussions. Let’s look at one example of a blog by Eric Pascerello, in figure 13.1.
Eric’s blog is accessible, which means that it can be linked to by anyone. While many people simply read it on the JavaRanch site, it can also be accessed as a syndication feed. We can read this feed by accessing it directly in its native XML form, or we can go to other websites (such as JavaLobby) that obtain the syndication feed and display it in their format.
In figure 13.1, we see in the upper-right corner of the screen that there are links to RSS, RDF, and Atom. As we mentioned earlier, these are the most common XML syndication feeds that we can find.
Each of the syndication feeds has a different XML specification. That means if we were to look at the different XML files, we would see different naming schemes for the elements. Each feed has a specification clarifying what information must be defined in the feed. Since we are dealing with different formats, the easiest way to address it is to pick a single format and design the reader around it.
One of the most popular syndication feeds is Really Simple Syndication, better known as the RSS feed.
Before we create our Ajax XML reader, let’s examine the structure of the RSS file. Knowing the structure allows us to navigate the XML DOM more efficiently to obtain the information that we want to display. The RSS document has two parts: the channel and the items. The channel gives us the information about where the RSS feed is from, and the items are the articles that we can read.
The channel can be considered to be the header information of the RSS feed. The channel elements tell the user where the RSS feed is coming from, what the title of the document is, when it was last updated, and so on. Only a few items are required by the RSS specification, as shown in table 13.1.
Element |
Description |
Example |
---|---|---|
description | Phrase describing the channel. | Weird thoughts from Eric’s Head |
link | The URL to the HTML website referring to the channel. | http://radio.javaranch.com/pascarello |
title | The name of the channel and how people refer to the service. The name should be related to the name of the website. | Eric’s blog on JavaRanch.com |
The three required elements in table 13.1 give us the basic information about the RSS feed. The RSS feed’s required elements tell us where the RSS channel is from, what the title of the channel is, and what the channel is about. If we want to obtain other information about the RSS feed, we need to check for optional elements.
The RSS feed can contain any number of the optional channel elements. The RSS feed developer can select none, one, or all 16. The optional elements (table 13.2) give us more options to learn about the feed.
Element |
Description |
Example |
---|---|---|
category | Specifies which categories the channel belongs to. | Programming |
cloud | Allows processes to register with a cloud so they can be notified of updates to the channel, implementing a lightweight publish-subscribe protocol. | |
copyright | The copyright notice for the content in the channel. | Eric Pascarello |
docs | A URL that points to the documentation for an RSS feed. | http://backend.userland.com/rss |
generator | A string that indicates what program was used to generate the protocol. | Pebble |
image | Specifies an image that can be displayed along with the feed. | http://pebble.soundforge.net/common/images/powered-by-pebble.gif |
language | The language the channel is written in. | En |
lastBuildDate | The last time the content was changed. | |
managing editor | Email address for person who is responsible for the editorial content. | [email protected] |
pubDate | The publication date for the content. | |
rating | The PICS rating for the channel. See http://www.w3.org/PICS/. | |
skipDays | Informs aggregators what days they can skip checking for updates. | |
skipHours | Informs aggregators what hours they can skip checking for updates. | |
textInput | Specifies a text input box that can be displayed. | |
ttl | Indicates the Time to Live (TTL), or number of minutes the channel can be cached before it is updated. | |
webmaster | Email address for the person who is responsible for technical issues. | [email protected] |
Some of the element options include email addresses in case we have problems with the feed’s content or layout. There is also information that explains when the syndication feed is updated.
The channel’s required and optional elements describe the content of the RSS feed so that we can determine the characteristics of the feed. Just like the channel, the item elements have optional elements as well.
The RSS feed can contain multiple item elements, similar to the way a newspaper consists of multiple articles. Each item element is required to have at least one of the following two elements: the title or the description. Only one is needed, according to the RSS specifications, but both are allowed in an item element.
There are also eight other optional elements that can be added to the item. Returning to the newspaper analogy, an article normally contains the story, the author, the source, and a title. In the same way, each item element in an RSS feed can have separate titles, authors, sources, and so on. All of the optional elements that are available for the item element are shown in table 13.3.
Element |
Description |
Example |
---|---|---|
author | The item author’s email address | [email protected] |
category | Includes the item in one or more categories | Programming |
comments | The URL of the page for comments that relates to this item | http://radio.javaranch.com/pascarello/2005/05/25/1117043999998.html#comments |
description | The item summary | Ajax allows developers to improve the UI by making a web application act like a client application. |
enclosure | Describes the media object that is attached to the item |
<enclosure url="http://radio.javaranch.com/ pascarello/media/TheAjaxInActionSong.mp3" length="5908124" type="audio/mpeg"/> |
guid | A string that is a unique identifier | http://radio.javaranch.com/pascarello/2005/05/25/1117043999998.html |
link | The URL of the item | http://radio.javaranch.com/pascarello/ |
pubDate | The date the item was published | Wed, 25 May 2005 17:59:59 GMT |
source | The RSS channel the item came from |
<source url="http://radio.javaranch.com/ pascarello/blog.xml">Eric's Blog</source> |
title | The title of the element | Ajax Improves UI Development |
The heart and soul of the RSS feed are the title and the description. The title gives us a small insight into what the article is, whereas the description element can be one of two things: a synopsis about the article or the entire article itself. There is no set standard on how the description element is used. To determine how to handle it, we have to look at the individual feeds before we start to write the RSS feed reader. If it’s a synopsis, we can compare it to a blurb on the front of a magazine where it says, “see page 10 for more information.” That is where the link element comes into use. The link is the URL to the entire article on the author’s site.
Most RSS feeds try to utilize as many of the optional elements as possible in order to provide developers, like us, the tools to make our RSS reader as robust as possible. With better data at our hands, we can better display the RSS feed content. For more information about the RSS specification, visit http://backend.userland.com/rss.
Now that we understand the basic elements of the RSS document, we can create our Ajax-based RSS reader.
In this chapter, we create an RSS feed viewer that obtains the XML feeds from websites without using a server-side language or a client application RSS reader.
Ajax allows us to view the information with a web page that is stored on our desktop. This example demonstrates that Ajax does not have to run with a web server that has a server-side language such as .NET or JSP. As long as we have an active Internet connection, we are able to access RSS feeds from any site we desire. (If you are running a Mozilla browser, see section 13.6.1. You must overcome Mozilla’s security restrictions, which we discussed in chapter 7, before you try to execute the code in this project.)
If you find yourself scanning multiple websites for content every day, you will be able to avoid that by running this reader. The viewer will be able to show multiple feeds on one page.
The unique feature of this application is that we are not using any server-side code; we are only obtaining RSS XML documents that are created by the other websites. The complete application resides on a web page saved on our desktop environment or delivered as part of our website.
The first thing is to understand the steps for what we are going to develop. We are developing an RSS reader that is going to set up a slideshow that uses two layers. Each layer will contain one feed, which will be displayed for a set period of time, after which the next feed fades in. In figure 13.2, we can see the control flow of the application.
The process has a lot of steps. The first step is to load our multiple feeds. We will use a master array to hold the information we need from each feed source. We do not require all the optional item elements that we listed in table 13.2.
After we load all the files, we need to create our transition effect of fading in and out. In this case, we’ll use CSS classes to do this. We’ll use a timer to switch between messages and loop through all the messages.
Other features that we want to incorporate into this application are back, forward, and pause buttons. We can also add the ability to insert additional feeds from a selection list. The first step is to create our client-side form and layers.
The biggest part of this project is presentation. We’ll use a series of div and span elements to make a table-like layout that contains a header, a body, and a footer. We can see how this looks in figure 13.3.
We could have used tables to create the layout, but tables were the pre-CSS page layout tool (see chapter 2 for an introduction to CSS). Today, tables should not be used for layout since they require more time to render and they are not as easy to change as a CSS layout. Listing 13.1 shows the markup on which our XML viewer’s layout is based.
The first div that we added is divNewsHolder , our container, which we use to set the overall size of our display window. The next div that we add is divNewsTitle , which is the header in our layout. Inside this div, we add a span that contains a placeholder for our feed count. The other line of text is the title of our feed viewer. We can make that line say anything we want.
The div divAd is our next row. This row is the placeholder for our RSS feed information that we will retrieve later. We insert two more div elements inside of the div divAd. The two new divs, divNews1 and divNews2 , are used to hold the RSS feed information. The CSS properties of these elements will be altered by the JavaScript to create a fading transition.
The footer row is made up of a div divControls . The footer row contains our navigation and our feed management functions. The next, back, and pause buttons are added to the div. The select form element and a button are added that allow a user to select additional XML feeds. This now finishes up the basic framework for the viewer, as seen in figure 13.4.
Figure 13.4 is not visually appealing since we have not formatted our HTML elements. The viewer lacks any structure, but that changes when we add CSS rules to the elements. By looking at figure 13.3, we see that our two divs, divNews1 and divNews2, need to be sitting on top of each other in order for our fading effect to work properly.
Without CSS, our web pages would all look like those in figure 13.4: very boring and unpleasant on the eyes. We’ll apply some CSS to make this example more pleasing. The style allows us to easily edit the properties in the future without having to edit the HTML. The first thing we can apply style to is our holder div and our header row.
The divNewsHolder div mentioned earlier can be considered our container for the viewer. It allows us to position the reader on the page and also set the width of the reader. Since we are using divs for our other rows, they take up 100 percent of the width that is available to them. By setting the width in the holder, we can dictate the width of the other elements, making future updates easier. Listing 13.2 shows how we achieve this using CSS.
#divNewsHolder{ The holder div width: 600px; border: 2px solid black; padding: 0px; } #divNewsTitle{ The title div font-size: 20px; height: 26px; background-color: #BACCD9; line-height: 26px; Height of line } #spanCount{ The count span float: right; Float-based layout font-size: 15px; padding-right: 10px; }
We apply style to our form elements by referencing its ID along with the pound (#) sign . This specifies that the style should be applied to only our div with the id divNewsHolder. For our divNewsHolder, we can assign width and border rules to it and set the padding to 0.
Now that we have set our holder div, we can style our first row. We want to set the height, background color, and font size of div divNewsTitle . The lineheight property is set to the height of the div. This ensures that our single line of text that is 20 pixels high is centered vertically in the div. Without the line height, the text would be located at the top border of the div.
The last step for formatting our header row is to move the spanCount to the right portion of the header instead of it being in front of our title. To do this, we use the float property and set it to right. This right-aligns our text, whatever the width of the containing element, and does not affect our title. The font size can be set to a smaller pixel height so it is not as prominent as our title. The padding-right property moves the text from the right edge so it is not sitting directly on the border. We are now finished with our holder and our header row; see figure 13.5.
In figure 13.5, we can see that the header row is very different compared to the other rows that have not been styled. The word Loading appears on the right side of the div, and our text is centered in the div. The holder div border surrounds the rest of the elements; now we need to work on the content divs.
The next step is to style the middle section or the body of our RSS reader (listing 13.3). The body section will contain our formatted RSS feed information. We position the divs, divNews1 and divNews2, on top of each other for our transition effect to work. The transition effect is to change the opacity of the layer so the layer contained below shows through. By increasing the opacity level of the layer, we are able to create a fading effect.
The first step is to style our div divAd that is the container for our feed spans. The width is set to 100 percent, and the height is set to 400px. We do not want scrollbars to appear for the row; therefore, the overflow property to set to hidden. This means if any content is larger than 400 pixels, it is hidden from the view. The other divs inside this holder allow for scrolling so we do not lose the content. We then set our top and bottom borders styles by setting a solid 2-pixel black line. A side border is not needed since our holder div contains a border thickness. If we applied the borders all the way around our row, it would be 4 pixels wide instead of 2 on the sides and only 2 pixels tall on the top and bottom of the row, which makes it look awkward.
We have to format our two content holder divs, divNews1 and divNews2 . We can style the properties that are the same between them by separating both of their IDs with a comma. The width and height values are set to take up the space of our holder div. By setting our divs’ position to relative, it allows us to position the divs in relation to our parent div divAd, unlike the absolute position, which is in relation to the top-left corner of the browser window. We set the divs’ overflow property to auto, allowing scrollbars to appear if necessary. The last step is to set the left position of the divs to 0 pixels, allowing the div to be flush so there are no gaps around the edges.
We want the two content divs to sit exactly on top of each other. Because we are using relative layout, separate position properties are required to be applied to our two feed divs. The div divNews1’s vertical position is set to 0 pixels. This forces it to sit flush to the top border of the parent div. The divNews2 position is set to –400px. The reason for the negative number is that the second div is positioned lower down the page than the first div, as shown in figures 13.4 and 13.5. Since we set the height of the container div to 400 pixels, we need to move divNews2 up 400 pixels so it is flush on the top of the parent div, just like divNews1. In figure 13.6, we can see how our two divs are now on top of each other, unlike in figure 13.5.
Since the two divs are on top of each other, we are only able to see the content from one of them. In this case, the opacity level is set at 100 percent; therefore, the content underneath is not visible. The level of 100 percent is going to be the last step in our fade transition, but before we can get to that we have to finish styling our reader.
The last section we want to add CSS to is our footer. In this section, we have to set the background color and standardize the form elements so that the section is more structured. To accomplish this, we set the colors, the font sizes, and the size of the buttons (listing 13.4).
We apply the CSS to our footer div divControls so it matches the header row. The background color is added to the div to match the header’s background color. We align the text so the content is centered in the div horizontally. Top and bottom padding is added to the div, which means the content doesn’t have to sit on the border. We don’t have to add a border to our div since the middle row has the top border defined and the holder div has the other three borders covered.
The last step in the CSS for formatting our footer is applying styles to our form elements so they fit in with the style of the reader. The button elements that are located inside the divControl div are referenced with the div’s name and then a space followed by the tag name. That means only the elements within that div tag get these properties assigned to them. Any of the other elements with the same tag name on the page will not.
Since the text in each of our buttons is a different length, we apply the width property to the buttons so they will all be the same width, causing the buttons to look more uniform. We change the background color so it is not the default color of the user’s operating system. The text color and the size of the font for the element’s text can be assigned also. Figure 13.7 shows us how our footer is now styled to match the feel of the RSS reader.
In figure 13.7, we see all of the properties that we applied to the divs. We applied widths, colors, font sizes, borders, padding, and much more. We can customize the CSS properties of these elements so they fit the needs and styles of any website theme or personal taste. The next thing we need to do is get content into our RSS reader!
In this example, we will load files from multiple feeds. We will use our ContentLoader object to do the work, as we have throughout this book. In the first version, we use a series of global variables to quickly develop an RSS feed viewer.
Global variables allow for easy adjustments to our script so we do not have to change the functionality inside the for loops and timers. We will be able to adjust the contents of the global variables to make changes throughout the script and communicate between the different functions. We want to use global variables rather than local variables in this script so they can be shared and we don’t have to pass them from function to function. Later on in this chapter, when we refactor our script, you will see a solution that doesn’t use global variables; but for now, they keep our example simple.
One downfall to JavaScript is that there is no variable type for constants, so we are simulating the effect with global variables. However, global variables can be overwritten, so we must be careful when using them. The variables in listing 13.5 will not be overwritten at any time during the application.
var flipLength = 5000; var timeColor = 100; var strErrors = "<hr/>Error List: <br/>" var arrayRSSFeeds = new Array( "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");
In listing 13.5, we assign the global variables that affect how our viewer performs. The first global variable, flipLength, determines how many milliseconds our current message is displayed before it is replaced with the next message. In this case, the value 5000 represents the total number of milliseconds between messages. Another timer variable is timeColor. This time span is the number of milliseconds between the coloring or fading steps in our script. The larger the number, the longer the transition takes to complete.
The next global variable that we use is strErrors. This line is the heading for any errors we encounter during the loading process. We can change the message or add style parameters to it. The last global variable that is going to affect the outcome of the script is the array, arrayRSSFeeds. In this array, we add the URLs of the RSS feeds that we want to access. In this case, we access four separate RSS feeds from JavaRanch.com’s radio blogs.
The next set of global variables that we declare, in listing 13.6, are used to communicate between our separate functions. These global variables hold the state of the RSS feeder. Their values change depending on the action that is being performed.
var currentMessage = 0; var layerFront = 2; var timerSwitch; var bPaused = false; var arrayMessage = new Array(); var intLoadFile = 0; var bLoadedOnce = false;
The first global variable that we instantiate in listing 13.6 is currentMessage. The variable currentMessage keeps track of the message that is being viewed. It can be considered a counter that is reset when it reaches the maximum number of records. The next global variable is layerFront, which holds the state of our layers. When we designed our RSS reader layout, we had two layers on top of each other. This variable is keeping track of the state of those layers.
The variable timerSwitch holds the timer object that determines when the next frame is going to be loaded. If the user pauses the feeder, we cancel this timer and change the state of our next variable, bPaused. The boolean value that bPaused holds allows us to determine the state of the timer: true if it is paused and false if it is running.
The global variable arrayMessage holds the formatted messages that we retrieved from the RSS items. The array is multidimensional and holds all the information we want to show. As stated earlier, the item elements in the RSS feed hold more information than we may need; therefore, we only grab the few items that interest us and store them in arrayMessage.
The last variable, intLoadFile, leads us into our next section of code. The variable is a counter, which holds the current file count that is being loaded from our array, arrayRSSFeeds, during the preloading process.
Now that all the global variables have been declared, we can see a global picture of where this project is heading. We preload the RSS feeds from an array of URLs. We use a counter to track the status of the preload process. During the preloading, we are only selecting the desired information from each XML file. After the preload process has finished, the messages are displayed with a fading transition to create a slideshow, which we can pause and manipulate. With the global variables that we declared, we are able to control the functionality of the script. This has led us to the starting point of the RSS preloader function.
One of the problems developers face with Ajax is how to preload multiple files without sending too many requests to the external websites and having them step all over each other. One solution is to use queuing, and that is what our Ajax ContentLoader does.
The ContentLoader allows the queuing mechanism to fire the requests in an orderly fashion. In listing 13.7, we take our array (which was populated when the page was loaded in listing 13.5) that contains the URLs to our feeds and prepare them for our ContentLoader.
window.onload = function(){ var loader= new Array() for(i=0;i<arrayRSSFeeds.length;i++){ loader[loader.length] = new net.ContentLoader(arrayRSSFeeds[i], BuildXMLResults,BuildError); } }
The code in listing 13.7 is fired with the onload event handler. As the page is loaded, we prepare an array variable, loader, to hold all the requests to the server. We loop through our array arrayRSSFeeds to obtain all of the URLs from which we want to obtain information. For each iteration, we increment our loader array to hold the new ContentLoader request. We pass in the URL of the feed, the function BuildXMLResults() that formats the content, and the function BuildError() that will be called if there is an error obtaining the feed. Now that we have begun the loading process, we need to format the returned XML feeds.
When the request is made, it is going to call either BuildXMLResults() (if it was successful) or BuildError() (if it encountered any problems). BuildXMLResults() takes the XML feed and formats it into a usable format. BuildError() logs the error to the error list. Both functions update the status so we can see the progress of the loading. Listing 13.8 shows the implementation of this logic.
function BuildXMLResults(){ var xmlDoc = this.req.responseXML.documentElement; var RSSTitle = xmlDoc.getElementsByTagName('title')[0].firstChild.nodeValue; var xRows = xmlDoc.getElementsByTagName('item'), for(iC=0;iC<xRows.length;iC++){ intMessage = arrayMessage.length; arrayMessage[intMessage] = new Array( RSSTitle, xRows[iC].getElementsByTagName('title')[0] .firstChild.nodeValue, xRows[iC].getElementsByTagName('link')[0] .firstChild.nodeValue, xRows[iC].getElementsByTagName('description')[0] .firstChild.nodeValue); } UpdateStatus(); }
The function BuildXMLResults() in listing 13.8 retrieves the XML document by referencing our request object’s responseXML property. With the XML document stored in our local variable xmlDoc, we are able to obtain the RSS title information for the feed. To do this, we reference the title element tag and reference the first child node’s value.
We obtain the item elements and prepare to loop through the resulting array stored in xRows. By looping through the array, we are able to create a multidimensional array, storing it in the next position of our global array, arrayMessage. The global array holds the title of the RSS feed and the title, link, and description of the article. We build this multidimensional array for every item element stored in xRows. After we’ve finished traversing the document, we call the function UpdateStatus() (listing 13.9) to display the current state of the process to the user.
function UpdateStatus(){ intLoadFile++; if(intLoadFile < arrayRSSFeeds.length){ document.getElementById("divNews2").innerHTML = "Loaded File " + intLoadFile + " of " + arrayRSSFeeds.length + strErrors; }else if(intLoadFile >= arrayRSSFeeds.length && !bLoadedOnce){ document.getElementById("divNews2").innerHTML = "Loading Completed" + strErrors; if(arrayMessage.length == 0){ alert("No RSS information was collected."); return false; } bLoadedOnce = true; var timerX = setTimeout("ChangeView()",1500); } }
The function UpdateStatus() performs two services, as shown in listing 13.9. The first service displays the status of the preloader to the user. The second service determines if the slideshow has to be started. We first increment our global variable intLoadFile to update the file count. If intLoadFile is less than the total files we are to load, we display our loading status by setting the innerHTML of our top layer divNews2 with our output string.
If the file count is greater than or equal to the number of files in our array (and also the slideshow has not been started), then we can start the transitions. Before we can start the slideshow, we need to verify that we actually have data to show. We verify the data by checking the length of our formatted message array, arrayMessage. If there are no messages, we notify the user and exit the function by returning false.
If there is data to display, we set bLoadedOnce to true and call the function ChangeView() after a slight pause in time. The slight pause allows the user to read any error messages that we may have encountered. As mentioned previously, if the loader encountered a problem with loading the XML document, it calls our function BuildError() (see listing 13.10).
function BuildError(){ strErrors += "Error:" + "" + "<br/>"; UpdateStatus(); }
BuildError() allows us to display an error to the user. This tells the user that not all of the files were loaded. We just append the error to our global variable strErrors and call our UpdateStatus() function that we just developed to inform the user of the application’s current loading state. We can verify that our preloader works by saving the document and running the web page in our browser (figure 13.8).
When we test the viewer, we should see the status update on the screen. In figure 13.8, the preloader is loading file 2 of 4 and there have been no error messages. When all of the files have loaded, we should see that the files have been loaded successfully and there are no errors in the list. However, there is a JavaScript error indicated in our status bar since we still have not created our function ChangeView(). We’ll do that in the next section, but first we will create the cross-browser fading transition effect.
The code that we have written so far has loaded the files into an array. We now have to take the data stored in an array and build a slideshow. The slideshow is based on DHTML. By changing the content within divs by using innerHTML, we can display the different articles that our preloader has loaded. Changing CSS classes of the elements and altering the z-Index of layers allows us to create fading transition effects with the divs. By placing all of the steps together, we are going to create a dynamic fading slideshow.
When we are creating the fading effect, we need to change the opacity of the top layer. Changing the opacity of the layer lets the content underneath show through. With an opacity level of 0 percent, we are allowing all of the content to show through. An opacity level of 100 percent blocks anything on the layer underneath from showing through.
Now, as always, we have issues with Internet Explorer and Mozilla-based browsers. Both browsers view opacity differently, so in our stylesheet rules we must account for the differences. Mozilla uses opacity, whereas Internet Explorer uses a filter specifying the alpha opacity, as listing 13.11 shows.
.opac0{opacity: .0;filter: alpha(opacity=0);} .opac1{opacity: .2;filter: alpha(opacity=20);} .opac2{opacity: .4;filter: alpha(opacity=40);} .opac3{opacity: .6;filter: alpha(opacity=60);} .opac4{opacity: .8;filter: alpha(opacity=80);}
Listing 13.11 shows that we created a series of style rules that have different opacity levels. Using CSS rules instead of using JavaScript to manipulate the values is a matter of preference. By using CSS rules, we can change other properties; maybe we want the colors to change as the fading occurs, or maybe we want to increase the text size. Using CSS classes allows us to do that without adding any extra JavaScript code, and it also encapsulates the cross-browser differences in a single place.
Now that we have created our classes, we can start the process of loading the RSS feed information into our divs.
In section 13.3.2, we received an error message when testing the code since we had not created the function ChangeView(). ChangeView() initiates the process of the fading in and out of the content divs. For the fading process to work correctly, we alter the CSS classes and position the divs on different z-Index levels. Listing 13.12 shows how this is implemented.
The ChangeView() function has two major roles. The first is to build the HTML to display our data obtained from the RSS feeds. The second role is to prepare our divs for the fading in. Building the HTML is simple since we are using a basic layout. The hardest part is making sure that we keep track of quotes and apostrophes so we do not encounter any errors.
The first line of text we want to display is the RSS channel’s title , which we stored in the first index of the array, arrayMessage. We need to surround the title with a span and assign a CSS class name of RSSFeed. The next step is to display the item element’s title by referencing the second index of the array. By surrounding the title with a span and assigning a CSS class of itemTitle to the span, we are able to apply a separate style to our titles. To allow for a separation between the title and the message body, a horizontal rule is inserted.
The item description was stored in the fourth index of the arrayMessage. We divide the description from our next section, which holds the last item element we collected. The last item is the link ; we assign the value of the URL element to the link’s HREF attribute. The text that is visible to the user is “View Page,” which the user is able to click. The link sends the user to the RSS feed’s website.
We want to update the current message display counter that we built into our RSS header. To do this, we alter the innerHTML of our span spanCount by using the arrayMessage length and our current message counter. We need to prepare the divs for the transition effect. We initialize the div by setting the zIndex so it is on top of the current one and set the class to our first CSS rule for opacity.
After we load the current message into our div, we start the process of fading the div into view. To do this, we need to create a function that loads the CSS classes in order; therefore, we call the function SetClass() .
The process of loading our div into view creates a smooth transition effect between messages instead of an abrupt change. Altering the opacity level of the layer with the CSS classes we created earlier creates the effect. The opacity level allows the layers underneath the div to show through, as if we were looking through a window that was tinted. We increase the opacity level in order to block out all the content that is below the div.
As mentioned in section 13.4.1, we are using five CSS classes to handle the fading in and out. The reason for using the classes is that in the future we can add colors to the fading or anything else that we would like to display in the transition effect. In this case, we loop through the classes. This is illustrated in listing 13.13.
Listing 13.13 shows the function SetClass(), which has a parameter, xClass, passed to it. This parameter allows us to track the current state of our transition effect without using another global variable. We call this function for every step of our transition to update the status until the fading transition is complete.
Since we are dealing with five CSS classes, we need to verify that the current step of our transition has a value under five. If that’s the case, we know that there are still more CSS classes that need to be applied to our transition. If we are below five, we apply the next CSS class to the element. We reference the attribute className and apply the next class to the element.
After we set the new class, we need to create a timer to call the next step. The setTimeout method has two parameters. The first is the function or JavaScript statement to execute, and the second is the amount of time in milliseconds before it is executed. In this case, we are going to call our SetClass() function with the incremented state of our class. The timeout is set to our global variable, flipLength, which we declared in section 13.3.1.
The else portion of our script handles the situation when we have looped through our five CSS classes and applied them to the div. First, we remove the CSS Class from our div. The default opacity is 100 percent and allows the div to cover the other one completely with nothing showing through from the bottom layer.
We increment the currentMessage variable, allowing the next message to be loaded. We check to see if that message number is greater than the number of messages contained in our array arrayMessage. If it is greater, we set the current message back to the start. The timer is restarted to load the next message after our set period of time. The setTimeout method calls our function, ChangeView(), and our global variable, flipLength, determines the length of time. In order for this to execute, we make sure that our global variable, bPaused, is not true. We will be coding the pause feature of this script in section 13.5.2.
The transition effect of the slideshow is now complete. We can test what we have created so far and see if it works. If everything is working correctly, we should see the page-loading counter slowly increasing as the files are being loaded into the script, and the first message should begin to fade in.
As you can see in figure 13.9, there are two different messages in the viewer since the one is slightly transparent. The current message (6) is displayed in the header, and we are able to see that in total 31 messages were loaded. Now, all we have left to do is add the pause, back, forward, and add functionality to our viewer.
The code that we already developed can be used on its own, without the other features, but they can make the script more flexible for users and for us. The first feature that we want to add allows us to import other RSS feeds that are not included in the preload function. Perhaps we want to check a site once in a while for new content, or maybe we want to grab a weather RSS feed. This feature allows us to obtain the syndication feed when we need it. The other features that we can include will let us skip through messages and pause them if we want more time to read them.
Adding additional messages to our feed is easier than you may think. Take a look at figure 13.9 again. The selection list contains the names and URLs of additional feeds we want to check occasionally; we just select a name and click the Add Feed button. We have already built most of the functionality in section 13.3; all we need to do now is execute our ContentLoader, in listing 13.14, which will add the feed selected in the select element.
function LoadNews(){ var sel = document.Form1.ddlRSS; if(sel.options[sel.selectedIndex].className!="added"){ var url = sel.options[sel.selectedIndex].value; sel.options[sel.selectedIndex].className="added"; var loader1 = new net.ContentLoader(url, BuildXMLResults); } }
We create a function called LoadNews() that we initiate from our button named btnAdd. Since we are obtaining the additional RSS feeds’ URLs from a select element, we need to reference our select element, ddlRSS, so we can access its values.
When we want to add an RSS feed from the select element, we need to have some way to tell if it already has been added. One way to do this is to add a CSS class to the option element. Therefore, we need to add a check to verify that we have not added this RSS feed already. If the feed is new, we grab the value of the selected option and change the className to added.
We execute the ContentLoader with the URL of the feed and also the function BuildXMLResults(). We can use the default error message of the ContentLoader if it encounters an error. Now that we have the ability to load a document from the selection list, we need to add RSS feeds to the selection list and also add the event handler to the button, shown in listing 13.15.
<select name="ddlRSS"> <option value="http://radio.javaranch.com/frank/rss.xml"> Frank</option> <option value="http://radio.javaranch.com/gthought/rss.xml"> Gregg</option> </select> <input type="button" name="btnAdd" value="Add Feed" onclick="LoadNews()" />
In the selection list, we add URLs to RSS feeds that are not contained in our preloader RSS feed array. In this case, two additional RSS syndication feeds were added from JavaRanch’s radio blog. We add the onclick event handler to our button btnAdd so the function LoadNews() can be executed.
The last step to loading the individual feeds is to create a CSS class to add to our stylesheet. This gives an added benefit to the users by giving a visual aid that the feed has been loaded.
.added{background-color: silver;}
In the CSS class, we can add any CSS rule so we are able to distinguish the added feeds from the others. In this case, we change the background color of the option to silver so that the option stands out in the list. After we add the class, we can test our application.
As figure 13.10 shows, we have added the RSS feed of Frank since it is highlighted in silver. The feed labeled Gregg is not added since it still has the default white background color. The number of messages in our RSS reader also increased from 31 to 54 after we added the feed. The only features remaining to add are our back, forward, and pause buttons.
One of the most useful features that we can add is the ability to skip through messages. If we find a message that is not interesting to us, we can click a button to see the next one instead of having to wait for the timeout to execute. The pause feature allows us to have more time to read a message that is interesting or long. Since we have used global variables for our timers, pause, and the currentMessage counter, we are able to affect the current state of the RSS reader very easily. Listing 13.16 shows the code that lets the user flip through the feed.
By creating a function, MoveFeed() and allowing it to accept a single parameter, we can handle all three situations; pause, skip forward, and skip backward. We can use an integer to differentiate between the different actions. To pause the reader, we pass in a 0. To skip forward, we use 1, and to skip backward we use –1.
The first functionality we check for is the pause. We verify that the passed-in parameter is a 0. The pause button has two behaviors. The first is to enter the pause mode, which stops the transitions from executing. The second is resume, which allows for the slideshow to restart the transitions.
If the feed is not paused , then we need to set our bPaused variable to true and check to see if our timer timerSwitch is running. If the timer is running, we need to cancel it by using the clearTimeout method. We change the button’s text to display the string “RESUME”. If the button is clicked to resume the feed, we do the opposite of pausing the feed . We set the bPaused variable to false; we call our function ChangeView() with a slight pause in time, and we change the text of our button value back to “PAUSE”.
The pause behavior is now complete.
We have to create our skipping and backtracking functionality . Since we are changing messages, we should remove the timer to avoid problems with skipping multiple messages. After we remove the timeout, we need to see if the action was –1. If we are moving backward, we need to subtract 2 from the currentMessage variable. This is because the variable, currentMessage, is actually holding the value of the next message since it already has been incremented. By subtracting 1 from the variable, we stay on the same message. Since we are already have the next message variable stored in currentMessage, we do not have to do anything for the forward button.
We have to be sure that our number is not less than 0. If it is, we need to set our variable to the last message in our array. After we have changed the currentMessage, we can call our ChangeView() function to load our message. All we have to do is add the event handlers to the buttons (listing 13.17) so we can execute the function, MoveFeed().
value=" <BACK " onclick="MoveFeed(-1)"> value=" PAUSE " onclick="MoveFeed(0)"> value="NEXT>" onclick="MoveFeed(1)">
To initialize the function, we add onclick handlers to our buttons. The onclick handlers call our function MoveFeed(), which passes the integers of –1 to skip backwards, 0 to pause the reader, and 1 to skip forward a message. By saving the document and opening our browser to this page, we can test the last of the functionality.
Now that we have the ability to skip messages, we can advance to the messages in the middle of the RSS feed list. Figure 13.11 shows that the reader is paused since the button btnAdd’s text says RESUME. With the additional features that we have created, the RSS viewer allows us to read the feeds from our desktops without visiting the individual websites that host the feeds.
With the Ajax-based RSS syndication feed reader that we have developed, we are able to view RSS feeds from an HTML file stored on the desktop with no server-side code required. We can use this application to grab the RSS feeds we read without having to go to the websites. We may want to offer this page as a download for the users on our websites. We can set it up to read our site’s RSS feeds. Because we can run this script on our website too, we can use it for other things as well. One use can be a banner ad rotator, a company news banner, or anything else we can think of. But there are some limitations to what this script can do, and we may have trouble running this application with Mozilla on our desktop.
Unlike Microsoft Internet Explorer, Firefox and Mozilla cannot execute the application from our desktop due to security restrictions. The security restrictions keep Ajax from communicating from our desktop to other websites since they want to protect us from having code send information without our knowledge.
To verify that this is the problem with the Ajax script, we need to look for an error message. In Mozilla, we need to open up the JavaScript Console. The JavaScript console is located under Tools > Web Development > JavaScript Console (figure 13.12).
When we click on the JavaScript Console menu option, another window opens (figure 13.13).
In figure 13.13, we see a permission denied error caused by the XMLHttpRequest object. There are two ways to correct this. The first is to go into the configuration file of Mozilla and set the permission setting to allow the XMLHttpRequest object to perform its desired task. To do this, we type about:config into the address bar of the browser and adjust the setting, but that is not a safe procedure to perform.
The reason it is not safe is that we are enabling it for anything that runs on our computer. That means any script that wants to talk to the outside world would be able to do so. How can we avoid this and allow only our Ajax application to talk to the outside? The solution is to set the security with JavaScript. We showed how to do this in chapter 7, provided the browser is configured to listen to programmatic requests to the Privilege Manager, but let’s recap briefly here. Listing 13.18 shows the generic code for enabling the additional privileges required to read external resources.
if(window.netscape && window.netscape.security.PrivilegeManager.enablePrivilege) netscape.security.PrivilegeManager.enablePrivilege( 'UniversalBrowserRead'),
In listing 13.18, we check if we can access the Privilege Manager. If we can, we enable the UniversalBrowserRead privilege. We need to add this code in two separate places inside our ContentLoader object that handles the Ajax functionality.
The first place we need to add it is directly after the loadXMLDoc declaration, as shown in listing 13.19.
net.ContentLoader.prototype.loadXMLDoc = function( url,method,params,contentType){ if(window.netscape && window.netscape.security.PrivilegeManager.enablePrivilege) netscape.security.PrivilegeManager.enablePrivilege( 'UniversalBrowserRead'),
We also need to add it to our onReadyState function (listing 13.20).
net.ContentLoader.onReadyState=function(){ if(window.netscape && window.netscape.security .PrivilegeManager.enablePrivilege) netscape.security.PrivilegeManager .enablePrivilege(' UniversalBrowserRead'),
Both of these functions interact with the data from the outside world. That is why we are required to add this functionality in both locations. When the script is executed, we will get a message prompt informing us of the request to change the security settings (figure 13.14).
If we simply click the Allow button at the prompt, the security prompt will still open every single time the function is accessed. To avoid this, click the “Remember this decision” checkbox. That way, the browser makes a note of your decision and allows the XMLHttpRequest to execute every time it is accessed without issuing the prompt.
With the security settings of the browser changed, we are able to make this application work off the desktop with Mozilla, Firefox, and Netscape. We can access XML feeds from any site without having to open multiple tabs or windows by using this reader. We also have the ability to alter this application to obtain other information from the Web, such as weather and comics.
This application is not limited to being an XML syndication reader from sites. We can easily adapt it as a banner ad rotator, company news updater, an event calendar, and so much more.
For instance, we can store our banner ads within an XML document. That way, anyone can update the XML file with new ads without having to touch any of the HTML files or the server-side code. We can preload the banner ads and have them displayed in the reader. Instead of just having one ad on the screen, we can have them rotate through as the user is reading the site.
We can set up the XML document to hold the company news so we can display our current articles to the employees or customers. We just need to fill in the basic items of the XML feed. We can also make it display the updates to the site or any other information we want. As you can see, we are not limited to just the plain XML feeds.
Now that we have a fully developed script for reading RSS feeds, let’s take the time once again to improve upon our efforts. As mentioned earlier, there are lots of possibilities for extending our script in terms of perusing different types of content. In this section, we concentrate on reorganizing the script along Model-View-Controller (MVC) boundaries. As we explained in chapters 3 and 4, the MVC pattern is a very common design pattern for separating the responsibilities of software. We’ll start our discussion with defining the Model types, then we’ll create a View for the Model, and finally we’ll round out the discussion with the Controller that ties everything together.
The RSS reader we’ve developed in this example will definitely benefit from having some first-class Model types to deal with. This will make the software conceptually cleaner and easier to read and maintain. With Ajax-based applications putting a heavier emphasis on the client DHTML than more traditional web applications, it becomes increasingly important to write clean, maintainable software. The Model classes we develop should also be generally applicable to other applications that deal with RSS feeds. As a syntactic simplification, we’ll use the Prototype library to define types just as we did in chapter 10.
Let’s start by defining a Model class for an RSS feed. An RSS feed is for our purposes an XML document that adheres to a predefined structure and has a URL that specifies how it can be accessed. The primary attributes of the structure are the title, link, and description, with many other optional attributes, as discussed earlier. The feed also has several items, which can be thought of as the articles of its content. Let’s start by capturing what we know so far, as represented in listing 13.21.
RSSFeed = Class.create(); RSSFeed.prototype = { initialize: function( title, link, description ) { this.title = title; this.link = link; this.description = description; this.items = []; }, addItem: function(anItem) { this.items.push(anItem); } };
This code defines the RSSFeed type via the Prototype library Class.create(). You will recall that using this idiom, the initialize method is invoked by the generated constructor. So with this definition of our RSS feed Model class, a feed could be constructed via the following line of code:
var rssFeed = new RSSFeed( 'JavaRanch News', 'http://radio.javaranch.com/news/', 'Stories from around the ranch' );
This is pretty much all that’s required for the definition of an RSSFeed Model object. Notice that the RSSFeed has an addItem API that enables the addition of items to the internal item’s array. Each item should be a Model object as well—one that encapsulates the attributes of each item in the feed. Given what we know about the RSS items, let’s define our item Model class as shown in listing 13.22.
RSSItem = Class.create(); RSSItem.prototype = { initialize: function( rssFeed, title, link, description ) { this.rssFeed = rssFeed; this.title = title; this.link = link; this.description = description; } };
Nothing much to get excited about here. The item encapsulates the title, link, and description attributes but also holds a reference to the RSSFeed object that it belongs to. Given these two Model classes, now we can envision that an item and one of its feeds could be constructed as shown here:
var rssFeed = new RSSFeed( 'JavaRanch News', 'http://radio.javaranch.com/news/', 'Stories from around the ranch' ); var feed1 = new RSSItem( rssFeed, 'Win a copy of JBoss', 'http://radio.javaranch.com/news/05/07/20/9.html', 'Text of Article' ); rssFeed.addItem(feed1);
So far, so good. The Model is a very straightforward encapsulation of the attributes of an RSS feed and its items. The two Model classes that encapsulate these two concepts are RSSFeed and RSSItem, respectively. Now let’s consider the construction of the Model itself. We know that these objects will get instantiated as a result of the XML data being loaded into the client by an Ajax request. So let’s define an API that our Ajax handler can call for converting the XML response into an instance of our RSSFeed Model class. Let’s start by defining the contract of our Model creator as follows:
var rssFeed = RSSFeed.parseXML( rssXML );
This contract implies that we’ll pass the XML response returned from our Ajax handler to the parse method of our RSSFeed type, and it will return to us an instance of an RSSFeed. Given that assumption, let’s implement the parseXML() method as shown in listing 13.23.
RSSFeed.parseXML = function(xmlDoc) { var rssFeed = new RSSFeed( RSSFeed.getFirstValue(xmlDoc, 'title'), RSSFeed.getFirstValue(xmlDoc, 'link' ), RSSFeed.getFirstValue(xmlDoc, 'description')); var feedItems = xmlDoc.getElementsByTagName('item'), for ( var i = 0 ; i < feedItems.length ; i++ ) { rssFeed.addItem(new RSSItem(rssFeed, RSSFeed.getFirstValue(feedItems[i], 'title'), RSSFeed.getFirstValue(feedItems[i], 'link' ), RSSFeed.getFirstValue(feedItems[i], 'description')) } return rssFeed; }
This method does the textbook response XML parsing that we’ve done many times already. It takes the values of the title, link, and description elements and uses them to create the RSSFeed. It then iterates over all of the item elements and does the same, creating an RSSItem instance for each. Within each iteration, the addItem() method is used to add the item to its parent RSS feed. Note that a helper method is used here to get the node value from the first child of an element with a given tag name. The helper method, getFirstValue, is shown in listing 13.24.
RSSFeed.getFirstValue = function(element, tagName) { var children = element.getElementsByTagName(tagName); if ( children == null || children.length == 0 ) return ""; if ( children[0].firstChild && children[0].firstChild.nodeValue ) return children[0].firstChild.nodeValue; return ""; }
This is everything we need from a Model perspective. Obviously, we could add attributes for all the optional parts of an RSS feed and populate them if they are present in the feed. We didn’t do that in this case because the RSS reader doesn’t use or need any of the optional attributes. But it’s definitely an opportunity to provide extended metadata for future features. We could also define accessor methods for the attributes to provide a more formal contract for accessing them. For example, we could write a getTitle()/setTitle() method pair for accessing the title attribute. Since JavaScript doesn’t support visibility semantics like other object-oriented languages (for example, the private/protected keywords in Java), we didn’t bother. Now let’s take a gander at our View.
With our Model classes securely in place, we can now consider a View class. We could develop a View class for the RSSFeed, and another for the RSSItem, but because our RSSReader doesn’t really view a feed independently of an item, we’ll define a single View class called RSSItemView, which encapsulates the View for an RSSItem in the context of its parent RSSFeed. Since the View in this case is obviously HTML, our View class is really just responsible for the generation of HTML. Let’s start by looking at the constructor in listing 13.25.
RSSItemView = Class.create(); RSSItemView.prototype = { initialize: function(rssItem, feedIndex, itemIndex, numFeeds) { this.rssItem = rssItem; this.feedIndex = feedIndex + 1; this.itemIndex = itemIndex + 1; this.numFeeds = numFeeds; }, }
Let’s take a moment to consider the parameters. The first parameter is an instance of an RSSItem. This tells the View what Model instance it’s providing a view for. Note that it’s not generally copasetic for the Model classes to have any knowledge of the View, but the View by necessity typically has intimate knowledge of the Model. The other parameters provide some supplemental context for the View. The feedIndex tells the View which feed number it’s in. The itemIndex tells the View where this item resides within its parent RSSFeed’s array of items. The numFeeds tells the View how many feeds there are. All of these index-based parameters are for the View to indicate its place in the world, so to speak. The View might want to display a context area that indicates, for example, “this is feed number 1 of 7 and article number 3 of 5.” These attributes could be embedded within the Model, but they’re not really attributes that the Model should typically care about, so this context that the View needs is passed into the View constructor by the client.
As mentioned previously, the responsibility of the View is to generate HTML. So our View class will need a single method that does precisely that. Let’s see what that might look like in listing 13.26.
toHTML: function() { var out = "" out += '<span class="rssFeedTitlePrompt">RSS Feed ' out += '(' + this.feedIndex + ' of ' + this.numFeeds + ') : '; out += '</span>'; out += '<span class="rssFeedTitle">'; out += '<a href="' + this.rssItem.rssFeed.link + '">' + this.rssItem.rssFeed.title + '</a>'; out += '</span>'; out += '<br/>'; out += '<span class="rssFeedItemTitlePrompt">Article '; out += '(' + this.itemIndex + ' of ' + this.rssItem.rssFeed.items.length + ') : '; out += '</span>'; out += '<span class="rssFeedItemTitle">'; out += '<a href="' + this.rssItem.link + '">' + this.rssItem.title + '</a>'; out += '</span>'; out += '<div class="rssItemContent">'; out += this.rssItem.description; out += '</div>'; return out; },
The toHTML method produces the contextual elements of the display followed by the text of the article. The first portion of the code displays the RSS Feed (x of y) : RSS Feed Title. The link attribute of the rssFeed parent is used to generate the HREF of the anchor produced, and the title is used to generate the text of the anchor. A CSS class name is generated for each span, one for the prompt, and another for the anchor, allowing each to be styled independently. This is illustrated in figure 13.15.
The next portion of code generates the Article (x of y) : RSS Item Title. The link attribute of the RSS item is used to generate the HREF of the anchor produced, and the title of the item is used to generate the text of the anchor. This code also provides CSS class names for both the prompt and the title, as illustrated in figure 13.16.
The last few lines of the toHTML method generate a div element to hold the content of the RSSItem’s article (the description attribute). The code for this is as follows:
out += '<div class="rssItemContent">'; out += this.rssItem.description; out += '</div>';
The CSS class name rssItemContent is generated for the article content. It should have a little bit of margin and padding in order for the content to visually reside within the display without touching any borders. It should also have a fixed height and overflow set to auto so that the content scrolls when needed—independently of the contextual information shown previously. A representative CSS definition for this class is shown here:
.rssItemContent { border-top : 1px solid black; width : 590px; height : 350px; overflow : auto; padding : 5px; margin-top : 2px; }
Given the code and style shown in this code, the content area produced should look something like the sample shown in figure 13.17.
Putting it all together, the view generated by our RSSItemView class is shown in figure 13.18.
Before we leave the topic of our View, let’s add one more little method to the View to make its usage more convenient:
toString: function() { return this.toHTML(); }
The reason we give the View a toString method is that it allows us to use the View instance and the HTML string that it generates interchangeably. For example, we can assign the View to the innerHTML attribute of an element, and the string representation, which is the HTML that it generates, will be used. For instance, the following code would assign the generated HTML of a view to the innerHTML of a div with the ID contentDiv:
var rssItemView = new RSSItemView( anRSSFeed, 0, 0, 5 ); $('contentDiv'). innerHTML = rssItemView;
(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.
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:
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.
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.
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 is to set the default values for its state. Next, the options object, as with most of the components we’ve written, is used to specify configuration options to the 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 . 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.
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.
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.
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.
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.
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.
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. |
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.
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.
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) ); }, 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.
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 ); 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.
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(); 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, loadRSSFeed(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.
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).
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.
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.
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.
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 13.5.
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.
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; },
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.
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 fine-grained 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.
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.
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.
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 has responsibility for updating the UI, we should expect our work to be mostly done. If that’s your expectation, then you’re right on target. To illustrate this, our updateView() method that has been referenced numerous times throughout our refactoring session is shown in listing 13.42.
updateView: function() { var rssItemView = new RSSItemView( this.currentFeed.items[this.itemIndex], this.feedIndex, this.itemIndex, this.options.rssFeeds.length ); this.getLayer(this.visibleLayer).innerHTML = rssItemView; this.fadeIn( this.visibleLayer, this.bringVisibleLayerToTop.bind(this) ); },
As you can see, the updateView() method delegates all of the hard work to our View class by instantiating an instance of it, setting it as the value of the visible layer’s innerHTML property, and finally fading the layer into visibility. Three lines of code. Not too shabby. Notice that once the layer is faded into view, we call a completion callback named bringVisibleLayerToTop. What this does is update the layer’s zIndex style property to ensure that it’s above the other layer being faded out. The bringVisibleLayerToTop() function is implemented as follows:
bringVisibleLayerToTop: function() { this.getLayer(this.visibleLayer).style.zIndex = 10; this.getLayer((this.visibleLayer+1)%2).style.zIndex = 5; }
That’s all we have to do from a UI-manipulation perspective. The separation of concerns across our Model, View, and Controller classes has facilitated a clean, maintainable architecture.
Our refactoring session concentrated on repackaging our script in such a way as to provide an MVC implementation of our RSS reader. We created an RSSFeed Model class to encapsulate the concept of an RSS feed, as well as an RSSItem class. We created a View class to encapsulate the concept of providing a View for an RSSItem in context of its parent RSSFeed, the RSSItemView. Finally, we tied the Model and View classes together with an RSSReader Controller class that provided all of the event-management glue and sophisticated interaction of a slideshow with transition effects.
In this chapter, Ajax allowed us to obtain information straight from our desktop without requiring a commercial client application, saving us money and allowing us to customize the solution to our needs. We were able to load multiple XML files and only obtain the information that is relevant to our needs. We developed an HTML framework and applied CSS to allow easy customization of the reader. By using DHTML, we were able to develop a rich user interface that allows users to skip messages, pause messages, and add additional feeds as needed. All of this was possible by taking advantage of Ajax functionality to obtain the syndication feeds from websites. By changing a few statements, we can easily adapt the reader to read any XML feed. We can even develop our own custom XML formats to display news, ads, and anything else that may be of importance for our websites. Finally, we repackaged the script along the lines of a Model-View-Controller architecture in order to facilitate the readability and maintainability of our code.
18.222.3.153