- •preface
- •acknowledgments
- •about this book
- •Who should read this book?
- •Roadmap
- •Code conventions
- •Code downloads
- •Author Online
- •About the title
- •About the cover illustration
- •Rethinking the web application
- •A new design for the Web
- •1.1 Why Ajax rich clients?
- •1.1.1 Comparing the user experiences
- •1.1.2 Network latency
- •1.1.3 Asynchronous interactions
- •1.1.4 Sovereign and transient usage patterns
- •1.1.5 Unlearning the Web
- •1.2 The four defining principles of Ajax
- •1.2.1 The browser hosts an application, not content
- •1.2.2 The server delivers data, not content
- •1.2.3 User interaction with the application can be fluid and continuous
- •1.2.4 This is real coding and requires discipline
- •1.3 Ajax rich clients in the real world
- •1.3.1 Surveying the field
- •1.3.2 Google Maps
- •1.4 Alternatives to Ajax
- •1.4.2 Java Web Start and related technologies
- •1.5 Summary
- •1.6 Resources
- •First steps with Ajax
- •2.1 The key elements of Ajax
- •2.2 Orchestrating the user experience with JavaScript
- •2.3 Defining look and feel using CSS
- •2.3.1 CSS selectors
- •2.3.2 CSS style properties
- •2.3.3 A simple CSS example
- •2.4 Organizing the view using the DOM
- •2.4.1 Working with the DOM using JavaScript
- •2.4.2 Finding a DOM node
- •2.4.3 Creating a DOM node
- •2.4.4 Adding styles to your document
- •2.4.5 A shortcut: Using the innerHTML property
- •2.5 Loading data asynchronously using XML technologies
- •2.5.1 IFrames
- •2.5.2 XmlDocument and XMLHttpRequest objects
- •2.5.3 Sending a request to the server
- •2.5.4 Using callback functions to monitor the request
- •2.5.5 The full lifecycle
- •2.6 What sets Ajax apart
- •2.7 Summary
- •2.8 Resources
- •Introducing order to Ajax
- •3.1 Order out of chaos
- •3.1.1 Patterns: creating a common vocabulary
- •3.1.2 Refactoring and Ajax
- •3.1.3 Keeping a sense of proportion
- •3.1.4 Refactoring in action
- •3.2 Some small refactoring case studies
- •3.2.2 Managing event handlers: Observer pattern
- •3.2.3 Reusing user action handlers: Command pattern
- •3.2.4 Keeping only one reference to a resource: Singleton pattern
- •3.3 Model-View-Controller
- •3.4 Web server MVC
- •3.4.1 The Ajax web server tier without patterns
- •3.4.2 Refactoring the domain model
- •3.4.3 Separating content from presentation
- •3.5 Third-party libraries and frameworks
- •3.5.2 Widgets and widget suites
- •3.5.3 Application frameworks
- •3.6 Summary
- •3.7 Resources
- •Core techniques
- •The page as an application
- •4.1 A different kind of MVC
- •4.1.1 Repeating the pattern at different scales
- •4.1.2 Applying MVC in the browser
- •4.2 The View in an Ajax application
- •4.2.1 Keeping the logic out of the View
- •4.2.2 Keeping the View out of the logic
- •4.3 The Controller in an Ajax application
- •4.3.1 Classic JavaScript event handlers
- •4.3.2 The W3C event model
- •4.3.3 Implementing a flexible event model in JavaScript
- •4.4 Models in an Ajax application
- •4.4.1 Using JavaScript to model the business domain
- •4.4.2 Interacting with the server
- •4.5 Generating the View from the Model
- •4.5.1 Reflecting on a JavaScript object
- •4.5.2 Dealing with arrays and objects
- •4.5.3 Adding a Controller
- •4.6 Summary
- •4.7 Resources
- •The role of the server
- •5.1 Working with the server side
- •5.2 Coding the server side
- •5.2.1 Popular implementation languages
- •5.3 The big picture: common server-side designs
- •5.3.1 Naive web server coding without a framework
- •5.3.2 Working with Model2 workflow frameworks
- •5.4 The details: exchanging data
- •5.4.2 Introducing the planet browser example
- •5.5 Writing to the server
- •5.5.1 Using HTML forms
- •5.5.2 Using the XMLHttpRequest object
- •5.5.3 Managing user updates effectively
- •5.6 Summary
- •5.7 Resources
- •Professional Ajax
- •The user experience
- •6.1 Getting it right: building a quality application
- •6.1.1 Responsiveness
- •6.1.2 Robustness
- •6.1.3 Consistency
- •6.1.4 Simplicity
- •6.1.5 Making it work
- •6.2 Keeping the user informed
- •6.2.1 Handling responses to our own requests
- •6.2.2 Handling updates from other users
- •6.3 Designing a notification system for Ajax
- •6.3.1 Modeling notifications
- •6.3.2 Defining user interface requirements
- •6.4 Implementing a notification framework
- •6.4.1 Rendering status bar icons
- •6.4.2 Rendering detailed notifications
- •6.4.3 Putting the pieces together
- •6.5 Using the framework with network requests
- •6.6 Indicating freshness of data
- •6.6.1 Defining a simple highlighting style
- •6.6.2 Highlighting with the Scriptaculous Effects library
- •6.7 Summary
- •6.8 Resources
- •Security and Ajax
- •7.1 JavaScript and browser security
- •7.1.1 Introducing the “server of origin” policy
- •7.1.2 Considerations for Ajax
- •7.1.3 Problems with subdomains
- •7.2 Communicating with remote services
- •7.2.1 Proxying remote services
- •7.2.2 Working with web services
- •7.3 Protecting confidential data
- •7.3.1 The man in the middle
- •7.3.2 Using secure HTTP
- •7.3.3 Encrypting data over plain HTTP using JavaScript
- •7.4 Policing access to Ajax data streams
- •7.4.1 Designing a secure web tier
- •7.4.2 Restricting access to web data
- •7.5 Summary
- •7.6 Resources
- •Performance
- •8.1 What is performance?
- •8.2 JavaScript execution speed
- •8.2.1 Timing your application the hard way
- •8.2.2 Using the Venkman profiler
- •8.2.3 Optimizing execution speed for Ajax
- •8.3 JavaScript memory footprint
- •8.3.1 Avoiding memory leaks
- •8.3.2 Special considerations for Ajax
- •8.4 Designing for performance
- •8.4.1 Measuring memory footprint
- •8.4.2 A simple example
- •8.5 Summary
- •8.6 Resources
- •Ajax by example
- •Dynamic double combo
- •9.1 A double-combo script
- •9.2 The client-side architecture
- •9.2.1 Designing the form
- •9.2.2 Designing the client/server interactions
- •9.3 Implementing the server: VB .NET
- •9.3.1 Defining the XML response format
- •9.4 Presenting the results
- •9.4.1 Navigating the XML document
- •9.4.2 Applying Cascading Style Sheets
- •9.5 Advanced issues
- •9.5.2 Moving from a double combo to a triple combo
- •9.6 Refactoring
- •9.6.1 New and improved net.ContentLoader
- •9.7 Summary
- •Type-ahead suggest
- •10.1 Examining type-ahead applications
- •10.1.2 Google Suggest
- •10.2.1 The server and the database
- •10.3 The client-side framework
- •10.3.1 The HTML
- •10.3.2 The JavaScript
- •10.3.3 Accessing the server
- •10.5 Refactoring
- •10.5.1 Day 1: developing the TextSuggest component game plan
- •10.5.3 Day 3: Ajax enabled
- •10.5.4 Day 4: handling events
- •10.5.6 Refactor debriefing
- •10.6 Summary
- •11.1 The evolving portal
- •11.1.1 The classic portal
- •11.1.2 The rich user interface portal
- •11.2 The Ajax portal architecture using Java
- •11.3 The Ajax login
- •11.3.1 The user table
- •11.4 Implementing DHTML windows
- •11.4.1 The portal windows database
- •11.4.3 Adding the JS external library
- •11.5 Adding Ajax autosave functionality
- •11.5.1 Adapting the library
- •11.5.2 Autosaving the information to the database
- •11.6 Refactoring
- •11.6.1 Defining the constructor
- •11.6.2 Adapting the AjaxWindows.js library
- •11.6.3 Specifying the portal commands
- •11.6.4 Performing the Ajax processing
- •11.6.5 Refactoring debrief
- •11.7 Summary
- •Live search using XSLT
- •12.1 Understanding the search techniques
- •12.1.1 Looking at the classic search
- •12.1.3 Examining a live search with Ajax and XSLT
- •12.1.4 Sending the results back to the client
- •12.2 The client-side code
- •12.2.1 Setting up the client
- •12.2.2 Initiating the process
- •12.3 The server-side code: PHP
- •12.3.1 Building the XML document
- •12.3.2 Building the XSLT document
- •12.4 Combining the XSLT and XML documents
- •12.4.1 Working with Microsoft Internet Explorer
- •12.4.2 Working with Mozilla
- •12.5 Completing the search
- •12.5.1 Applying a Cascading Style Sheet
- •12.5.2 Improving the search
- •12.5.3 Deciding to use XSLT
- •12.5.4 Overcoming the Ajax bookmark pitfall
- •12.6 Refactoring
- •12.6.1 An XSLTHelper
- •12.6.2 A live search component
- •12.6.3 Refactoring debriefing
- •12.7 Summary
- •Building stand-alone applications with Ajax
- •13.1 Reading information from the outside world
- •13.1.1 Discovering XML feeds
- •13.1.2 Examining the RSS structure
- •13.2 Creating the rich user interface
- •13.2.1 The process
- •13.2.3 Compliant CSS formatting
- •13.3 Loading the RSS feeds
- •13.3.1 Global scope
- •13.3.2 Ajax preloading functionality
- •13.4 Adding a rich transition effect
- •13.4.2 Implementing the fading transition
- •13.4.3 Integrating JavaScript timers
- •13.5 Additional functionality
- •13.5.1 Inserting additional feeds
- •13.5.2 Integrating the skipping and pausing functionality
- •13.6 Avoiding the project’s restrictions
- •13.6.1 Overcoming Mozilla’s security restriction
- •13.6.2 Changing the application scope
- •13.7 Refactoring
- •13.7.1 RSS reader Model
- •13.7.2 RSS reader view
- •13.7.3 RSS reader Controller
- •13.7.4 Refactoring debrief
- •13.8 Summary
- •The Ajax craftsperson’s toolkit
- •A.1 Working smarter with the right toolset
- •A.1.1 Acquiring tools that fit
- •A.1.2 Building your own tools
- •A.1.3 Maintaining your toolkit
- •A.2 Editors and IDEs
- •A.2.1 What to look for in a code editor
- •A.2.2 Current offerings
- •A.3 Debuggers
- •A.3.1 Why we use a debugger
- •A.3.2 JavaScript debuggers
- •A.3.3 HTTP debuggers
- •A.3.4 Building your own cross-browser output console
- •A.4 DOM inspectors
- •A.4.1 Using the Mozilla DOM Inspector
- •A.4.2 DOM inspectors for Internet Explorer
- •A.4.3 The Safari DOM Inspector for Mac OS X
- •A.5 Installing Firefox extensions
- •A.6 Resources
- •JavaScript for object-oriented programmers
- •B.1 JavaScript is not Java
- •B.2 Objects in JavaScript
- •B.2.1 Building ad hoc objects
- •B.2.2 Constructor functions, classes, and prototypes
- •B.2.3 Extending built-in classes
- •B.2.4 Inheritance of prototypes
- •B.2.5 Reflecting on JavaScript objects
- •B.2.6 Interfaces and duck typing
- •B.3 Methods and functions
- •B.3.1 Functions as first-class citizens
- •B.3.2 Attaching functions to objects
- •B.3.3 Borrowing functions from other objects
- •B.3.4 Ajax event handling and function contexts
- •B.3.5 Closures in JavaScript
- •B.4 Conclusions
- •B.5 Resources
- •Ajax frameworks and libraries
- •Accesskey Underlining Library
- •ActiveWidgets
- •Ajax JavaServer Faces Framework
- •Ajax JSP Tag Library
- •Ajax.NET
- •AjaxAC
- •AjaxAspects
- •AjaxCaller
- •AjaxFaces
- •BackBase
- •Behaviour
- •Bindows
- •BlueShoes
- •CakePHP
- •CL-Ajax
- •ComfortASP.NET
- •Coolest DHTML Calendar
- •Dojo
- •DWR (Direct Web Remoting)
- •Echo 2
- •FCKEditor
- •Flash JavaScript Integration Kit
- •Google AjaxSLT
- •Guise
- •HTMLHttpRequest
- •Interactive Website Framework
- •Jackbe
- •JPSpan
- •jsolait
- •JSON
- •JSRS (JavaScript Remote Scripting)
- •LibXMLHttpRequest
- •Mochikit
- •netWindows
- •Oddpost
- •OpenRico
- •Pragmatic Objects
- •Prototype
- •Qooxdoo
- •RSLite
- •Ruby on Rails
- •Sack
- •SAJAX
- •Sarissa
- •Scriptaculous
- •SWATO…
- •Tibet
- •TinyMCE
- •TrimPath Templates
- •Walter Zorn’s DHTML Libraries
- •WebORB for .NET
- •WebORB for Java
- •XAJAX
- •x-Desktop
- •XHConn
- •index
- •Symbols
- •Numerics
Refactoring 401
Table 10.3 Values of configuration properties (continued)
Value |
Explanation |
|
|
ignoreCase |
A boolean value indicating whether or not the matching should be case |
|
sensitive. |
|
|
count |
The maximum number of suggestions to render. |
|
|
Note that there are several options that specify which CSS class names should be generated internally when building the HTML structure for the pop-up list of suggestions. Recall the configurability requirements from table 10.2:
Requirement 3—Each component instance should be independently configurable, in terms of both the behavioral aspects (for example, case matching, match anywhere) and the CSS styling.
Requirement 5—The component should provide reasonable defaults for all of the configuration options.
Our code will use this configuration mechanism to provide per-instance configurability in terms of behavior (for example, case matching) as well as styling (for example, the CSS class names).
So, here at the end of day 2, we’ve made a good start on our component. With our constructor out of the way, and with a clean way to make our component highly configurable, it’s time to move on to making it Ajax aware.
10.5.3Day 3: Ajax enabled
Let’s put some Ajax into action, shall we? A TextSuggest component without Ajax is like a burger without the beef. With no disrespect to vegetarians, it’s time for the beef. You already saw a hint of some Ajax setup when we were looking at the constructor. As you might recall, we placed a method call within the constructor called initAjax(). The initAjax() method does the setup required for the Rico Ajax support discussed earlier. Here’s the implementation:
initAjax: function(url) {
ajaxEngine.registerRequest( this.id + '_request', url ); ajaxEngine.registerAjaxObject( this.id + '_updater', this );
},
Recall that the registerRequest() method provides the Rico Ajax engine a logical name for the URLs to invoke for a given request via the sendRequest() method. Given that we have to support the requirement of having multiple suggest
402CHAPTER 10
Type-ahead suggest
components on the same page using different URLs (but using the same ajaxEngine singleton), we need to generate a unique logical name for each. So, we generate the name for the request based on the ID of the component, which we assume to be unique. The same goes for the handler registration. We register this as the object that will handle responses routed to the ID we’re generating.
An example would probably help at this point. Suppose we attach the suggest behavior to a field with id='field1', and then we effectively register ourselves as 'field1_updater'. The XML we expect to come back to this component should have a response element that looks like this:
<ajax-response>
<response type='object' id='field1_updater'>.
...same xml content as before. </response>
</ajax-response>
Internally, we will be sending requests via the following:
ajaxEngine.sendRequest( 'field1_request',
'param1=val1', 'param2=val2', ... );
With that in mind, there are two things we have to do from the client to make our component Ajax enabled: send the request and handle the response. Let’s look at each in turn.
Text suggest—sending the Ajax request
Obviously there’s a little bit of work involved in getting to the point where we can send a request. The text input will have to generate an onchange event that we will listen to and conditionally send a request for the suggestions. We’ve not put any of that code in place yet, but that’s okay. We can still think in terms of our method responsibilities and the contracts we’d like to enforce independently of that being done. So, let’s assume that some piece of code yet to be written will decide that it needs to send a request to get some suggestions. Let’s call it sendRequestForSuggestions() and implement it as follows:
sendRequestForSuggestions: function() {
if ( this.handlingRequest ) { this.pendingRequest = true; return;
}
this.handlingRequest = true; this.callRicoAjaxEngine();
},
Refactoring 403
All this code does is to conditionally call this.callRicoAjaxEngine() if a request is not still being processed. This simple mechanism turns an internal boolean property, this.handlingRequest, to true as soon as an Ajax request is made and back to false (shown later) once the request has been handled. This is a very simple mechanism to use to throttle the sending of events based on the speed of the server. The boolean property this.pendingRequest is set to true if the method is called while a request is currently being processed. This state will let the handler know that it may have to send another request once the one being processed is finished. Now let’s peek under the hood and look at the callRicoAjaxEngine() method shown in listing 10.23.
Listing 10.23 Using the Rico ajaxEngine
callRicoAjaxEngine: function() { |
|
|
||
var callParms = |
[]; |
|
|
|
callParms.push( |
this.id + '_request'); |
Build the |
||
callParms.push( |
'id=' |
+ this.id); |
||
parameter |
||||
callParms.push( |
'count=' |
+ this.options.count); |
||
array |
||||
callParms.push( |
'query=' |
+ this.lastRequestString); |
||
callParms.push( |
'match_anywhere=' |
+ this.options.matchAnywhere); |
||
callParms.push( |
'ignore_case=' |
+ this.options.ignoreCase); |
var additionalParms = this.options.requestParameters || []; for( var i=0 ; i < additionalParms.length ; i++ )
callParms.push(additionalParms[i]);
ajaxEngine.sendRequest.apply( ajaxEngine, |
Send the Ajax request |
callParms ); |
},
To understand what this method does, we first need to talk about a JavaScript mechanism we are making use of on the very last line of the method:
ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
This uses a method called apply(), which is available to all function objects (see appendix B for more details). Let’s illustrate the usage with a simpler example:
Greeter.prototype.greetPeople = function(str1, str2) { alert('hello ' + str1 + 'and ' + str2)
};
Suppose we have an instance of Greeter called friendlyPerson, and we want to call the greetPeople() method on that object. But we don’t have the parameters
404CHAPTER 10
Type-ahead suggest
in a form that is easy to pass. We actually have an array of people. This is where the apply method comes in handy. We can write the code as
var people = [ "Joe", "Sally" ]; friendlyPerson.greetPeople.apply( friendlyPerson, people );
The apply() method converts the array passed in as the second argument to firstclass method parameters and invokes the method on the object passed in as the first parameter. The previous code is equivalent to
friendlyPerson.greetPeople( people[0], people[1] );
Now back to the task at hand. We have to call ajaxEngine’s sendRequest() method, which takes as its first parameter the logical name of the request, and a variable number of string parameters of the form key=value representing the request parameters. Therein lies the rub. We have request parameters from different sources, and we don’t know how many we have. Let’s look at the code again:
var callParms = []; |
|
|
b |
callParms.push( this.id + '_request'); |
|
||
|
|
||
... |
|
|
|
callParms.push( 'ignore_case=' |
+ this.options.ignoreCase); |
|
|
var additionalParms = |
|
|
|
this.options.requestParameters || []; |
c |
||
for( var i=0 ; i < additionalParms.length ; i++ ) |
|
|
|
callParms.push(additionalParms[i]); |
|
|
The array of parameters to send to the sendRequest() method via apply is populated from a combination of the internal state of the object, things like the ID and the lastRequestString, as well as specific properties of the Options object (for example, count, matchAnywhere, ignoreCase) b.
However, we also have to provide a mechanism for the user of our component to pass in external request parameters as well c. For this, we look for the existence of a requestParameters property on the options object. If it is non-null, it’s assumed to be an array of strings of the form key=value. The array is iterated over and added to the callParms already populated with the component-specific parameters. Finally, the request is sent via
ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
Whew! Request sending all done. Now let’s hope the server is up and running and we get a response. And let’s talk about how we will handle it when it does.
Refactoring 405
Text suggest—handling the Ajax response
We went to a lot of trouble to provide a robust request-sending capability, so we’d better make sure we properly handle the response or all our hard work will be in vain. Recall that Rico’s ajaxEngine routes the request back to the handler object’s ajaxUpdate() method, passing the content of the <response> element. So, by implication, we must write an ajaxUpdate() method, and that method will be the entry point into our response handling. The ajaxUpdate() method is shown in listing 10.24 along with its parsing helper methods, createSuggestions() and getElementContent().
Listing 10.24 Ajax response handling
ajaxUpdate: function( ajaxResponse ) { |
|
|
|
|
|
|
|
||
this.createSuggestions( ajaxResponse ); |
|
|
|
|
Create suggestions |
||||
|
|
|
|
||||||
if ( this.suggestions.length == 0 ) { |
|
|
|
|
|
||||
this.hideSuggestions(); |
|
|
|
|
|
|
|
|
|
$( this.id + "_hidden").value = ""; |
|
|
|
|
|
||||
} |
|
|
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.updateSuggestionsDiv(); |
|
Create and |
|||||||
this.showSuggestions(); |
|
|
show UI |
||||||
this.updateSelection(0); |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.handlingRequest = false; |
|
|
Finish handling response |
||||||
|
|
||||||||
if ( this.pendingRequest ) { |
|
|
|
|
|
|
|
||
this.pendingRequest |
= false; |
|
|
|
|
|
|
|
|
this.lastRequestString = this.textInput.value; |
|||||||||
this.sendRequestForSuggestions(); |
|
|
|
Send another request |
|||||
|
|
|
}
},
createSuggestions: function(ajaxResponse) { this.suggestions = [];
var entries = ajaxResponse.getElementsByTagName('entry'); for ( var i = 0 ; i < entries.length ; i++ ) {
var strText = this.getElementContent( entries[i].getElementsByTagName('text')[0]);
var strValue = this.getElementContent( entries[i].getElementsByTagName('value')[0]);
this.suggestions.push({ text: strText, value: strValue });
}
},
getElementContent: function(element) { return element.firstChild.data;
}