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

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;

}