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

392CHAPTER 10

Type-ahead suggest

10.4Adding functionality:

multiple elements with different queries

With the way that we designed the script, we can have multiple type-ahead select elements on the page. We just need to add declarations with new calls to SetProperties() for each element. The downside to this method is that in order to have different values fill in the drop-down, we would have to reference different server-side pages. In most cases we will be fine with this, but the only difference between these methods is most likely the SQL statement.

We can come up with an elaborate solution to this problem by adding an additional parameter to our custom object and sending it to the server. Another option is to work with what we have now so that we can make a minimum number of changes to our code. In this case, the simple solution involves changing one line in our code and adding an if statement on the server-side code.

The goal is to be able to somehow differentiate between the elements on the server to determine which element has caused the postback. A simple way to tell the difference is to use the name that is on the element. In this case, we’ll reference the name of our textbox. In listing 10.20, we alter the parameter string to allow for this new functionality.

Listing 10.20 Altering the TypeAhead() function to allow for different queries

function TypeAhead(xStrText){

var strParams = "q=" + xStrText + "&where=" + theTextBox.obj.matchAnywhere + "&name=" + theTextBox.name;

var loader1 = new net.ContentLoader(theTextBox.obj.serverCode, BuildChoices,null,"POST",strParams);

}

By making the slight change to the variable strParams in the function TypeAhead(), we are passing the name of the textbox in the form parameters being passed to the server. That means we can reference this value on the server and use either an if-else or a case statement to run a different query. Now we do not need multiple pages for multiple elements.

10.5 Refactoring

Now that we’ve developed a fairly robust set of features for providing typeahead suggest capabilities, it’s time to think about how to package all of this functionality in a more palatable way for the consuming web developer. What

Refactoring 393

we’ve developed to this point provides the functionality needed for the suggest behavior, but it has some drawbacks in terms of the work required for a developer to plug it into a web page—or 20 to 30 web pages, for that matter.

So let’s imagine for a moment that we are the grand architect of an Ajax-based web framework and we’ve been assigned the task of writing a suggest component for the rest of the company to use. As the requirements-gathering meeting disperses, we’re handed a sheet giving us our loose set of functional requirements. Unsure of what we’re getting into, we glance down at the list (table 10.2).

Table 10.2 Our functional requirements

Number

Requirement Description

Priority

 

 

 

1

The component must work with existing HTML markup without requiring any changes

1

 

to the markup. Simple changes to the head section to inject the component’s behav-

 

 

ior are acceptable.

 

 

 

 

2

The component must support being instantiated multiple times on the same page

1

 

with no additional effort.

 

 

 

 

3

Each component instance should be independently configurable, in terms of both

1

 

the behavioral aspects (e.g., case matching, match anywhere) and the CSS styling.

 

 

 

 

4

The component should not introduce any global variables. The company uses third-

1

 

party JavaScript libraries, and the global namespace is already cluttered. Any global

 

 

names, with the exception of the component itself, are strictly prohibited.

 

 

 

 

5

The component should provide reasonable defaults for all of the configuration

1

 

options.

 

 

 

 

6

The component must work in IE and Firefox.

1

 

 

 

7

The component should use an open source framework to reduce the amount of cod-

1

 

ing effort required and improve the quality and robustness of the solution.

 

 

 

 

8

Oh, and if you can, get it done by the end of the week.

1

 

 

 

As we survey the list, several thoughts run through our head. Okay, first of all, the powers that be don’t seem understand the concept of a priority. But we’re fairly used to that, so we look to the heart of the matter—the requirements. And despite all our hard work, we’ve satisfied less than half of them. Our script is already done, so that satisfies number 7 in the sense that we don’t need to reduce the effort because the script is already implemented. Obviously requirement 8 is satisfied for the same reason. Our script supports multiple browsers, so number 6 is

394CHAPTER 10

Type-ahead suggest

covered as well. As for the rest, we’ve got some work to do. We have only a week, so we’d better get started.

10.5.1Day 1: developing the TextSuggest component game plan

The first thing to decide is how to boost productivity to accommodate the short time schedule. One of the best ways to do this is by leveraging the work of others. If someone else can do some of the work, that’s less for us to do. So for this component, we’re going to leverage the open source efforts of Rico (http://openrico.org) and by extension Prototype.js (http://prototype.conio.net/). Rico provides some Ajax infrastructure, effects, and utility methods that will boost our development speed. Prototype provides some infrastructure for nice syntactic idioms that will make our code look cleaner and also take less time to develop. Let’s take a look at the implications of using Prototype and Rico.

Prototype

Prototype provides developers with a few extensions to the core JavaScript object as well as a few functions that make for a nice coding style. Here are the ones we’ll use in this example:

The Class object

The Class object introduced in the Prototype library has a single method called create(), which has the responsibility of creating instances that can have any number of methods. The create() method returns a function that calls another method in the same object named initialize(). It sounds complicated from the inside, but in practical use, it is straightforward. What this effectively does is create a syntactical way for specifying types in JavaScript. The idiom is as follows:

var TextSuggest = Class.create();

TextSuggest.prototype = {

initialize: function( p1, p2, p3 ) {

 

Called during construction

 

},

 

 

...

 

 

};

This segment of code creates what we can think of as a “class” (even though the language itself doesn’t support such a concept) and defines a constructor function named initialize(). The client of the component can create an instance via this line of code:

var textSuggest = new TextSuggest(p1, p2, p3);

Refactoring 395

The extend() method

The Prototype library extends the JavaScript base object and adds a method to it named extend(), thus making this method available to all objects. The extend() method takes as its parameters two objects, the base object and the one that will extend it. The properties of the extending object are iterated over and placed into the base object. This allows for a per-instance object extension mechanism. We’ll exploit this later when we implement the default values of the configurability parameters of the TextSuggest component.

The bind/bindAsEventListener() method

The Prototype library also adds two methods to the Function object called bind() and bindAsEventListener(). These methods provide a syntactically elegant way to create function closures. You will recall other examples where we created closures, such as

oThis = this;

this.onclick = function() { oThis.callSomeMethod() };

With the bind() method of Prototype, this can be expressed more simply as

this.onclick = this.callSomeMethod.bind(this);

The bindAsEventHandler() API passes the Event object to the method and normalizes the differences between IE and the W3C standard event model to boot!

The $ method—syntactic sugar

A little-known fact about JavaScript is that you can name methods with certain special characters, such as the dollar sign ($). The Prototype library did just that to encapsulate one of the most common tasks in DHTML programming, namely, getting an element out of the document based on its ID. So, in our code we will be able to write constructs such as

$('textField').value = aNewValue;

rather than

var textField = document.getElementById('textField') textField.value = aNewValue;

Rico

We got Prototype for free by virtue of using Rico. Let’s talk about what we’ll be using from Rico. Rico has a rich set of behaviors, drag-and-drop capability, and cinematic effects, but since we are writing a component that uses a single text field, we won’t need most of these. What we will be able to use, however, is a nice

396CHAPTER 10

Type-ahead suggest

Ajax handler and some of the utility methods provided by Rico. We will discuss the utility methods of Rico as the example progresses, but first let’s take a moment to discuss the Rico Ajax infrastructure.

The Rico Ajax capabilities are published via a singleton object available to the document named ajaxEngine. The ajaxEngine API provides support for registering logical names for requests as well as for registering objects that know how to process Ajax responses. For example, consider the following:

ajaxEngine.registerRequest( 'getInvoiceData', 'someLongGnarlyUrl.do' );

ajaxEngine.registerAjaxObject( 'xyz', someObject );

The first line registers a logical name for a potentially ugly Ajax URL. This logical name can then be used when sending requests rather than having to keep track of the aforementioned ugly URL. An example is shown here:

ajaxEngine.sendRequest('getInvoiceData', request parameters... );

The registerRequest() method isolates the usage of URLs to a single location, usually in the onload of the body element. If the URL needs to be changed, it can be changed at the point of registration without affecting the rest of the code.

Then registerAjaxObject() illustrates the registration of an Ajax handling object. The previous example implies that the object reference someObject should be referred to in responses by the logical name xyz and be set to handle Ajax responses via an ajaxUpdate() method.

Given that these functionalities of the ajaxEngine object are used, the only thing left to consider is the XML response expected by the Ajax engine. This is somewhat different from the dynamically generated JavaScript returned by the previous version of this example, but Rico expects to see XML. The response should have a top-level element around all of the <response> elements named <ajaxresponse>. Within that element, the server can return as many <response> elements as required by the application. This is a nice feature, as it allows the server to return responses handled by different objects that update potentially unrelated portions of a web page—for example, to update a status area, a data area, and a summary area. The XML response for the previous example is shown here:

<ajax-response>

<response type="object" id="xyz">

... the rest of the XML response as normal ...

</response>

Refactoring 397

<response...

> more response elements if needed..

</response>

</ajax-response>

This XML indicates to the ajaxEngine that an object registered under the identifier xyz should handle this response. The engine finds the object registered under the name xyz and passes the content of the appropriate <response> element to its ajaxUpdate() method.

Well, it was a short day overall. We spent some time researching open source frameworks to boost our productivity, and we came up with a game plan for incorporating them into our component. We’ve not yet written any code, but we have decided on a jump-start. We also have a good handle on a platform that will boost our performance, satisfying number 7 on our requirements list. Tomorrow we code.

10.5.2Day 2: TextSuggest creation—clean and configurable

Now that there’s a good technology platform to build on, let’s start writing the component. It’s often good to work backward from the desired result in order to think about the contract of our component up front. Let’s recap our first requirement:

Requirement 1—The component must work with existing HTML markup without requiring any changes to the markup. Simple changes to the head section to inject the component’s behavior are acceptable.

This requirement forces us to leave pretty much everything inside the <body> alone. In light of that, let’s assume we’re going to inject our script into the HTML via code that looks similar to the HTML in listing 10.21.

Listing 10.21 TextSuggest HTML markup

<html>

<head>

<script>

var suggestOptions = { /*details to come*/ };

function injectSuggestBehavior() { Create component in <head> suggest = new TextSuggest( 'field1',

'typeAheadData.aspx', suggestOptions );

} ); </script>

</head>

398CHAPTER 10

Type-ahead suggest

<body onload="injectSuggestBehavior()"> <form name="Form1">

AutoComplete Text Box:

<input type="text" id="field1" name="txtUserInput"> </form>

</body>

</html>

The implication of this HTML is that we’re going to construct our object with the ID of the text field we will be attaching to, the URL for the Ajax data source, and a set of configuration objects yet to be specified. (Note that the text field needs an ID attribute for this to work properly.) Everything inside the <body> element is left untouched. With that established, let’s start with a look at the constructor. We’ll put a name for our TextSuggest component into the global namespace via the constructor function that, as you recall, is generated by the Prototype library’s Class.create() method, as shown in listing 10.22.

Listing 10.22 TextSuggest constructor

TextSuggest = Class.create();

TextSuggest.prototype = {

initialize: function(anId, url,

options) {

Reference the

this.id

 

= anId;

 

b

 

 

this.textInput

= $(this.id);

 

 

input element

var browser

= navigator.userAgent.toLowerCase();

this.isIE

=

 

 

 

 

 

browser.indexOf("msie") !=

-1;

 

c Detect the

this.isOpera =

 

 

 

 

browser type

browser.indexOf("opera")!=

-1;

 

 

 

this.suggestions = []; this.setOptions(options); d Set the defaults this.initAjax(url); this.injectSuggestBehavior();

},

...

};

Now let’s deconstruct the constructor. As already mentioned, we pass into our constructor the ID of the text input to which we’ll be attaching the suggest

Refactoring 399

behavior. A reference is held to both the ID and the DOM element for the input field b. Next we do a little browser sniffing and store the state for the few things in the rest of the component that need to know specifics about the browser runtime environment c. In this case, special case code is needed only for IE and Opera, so we sniff only for them.

We’ll discuss the complex part of setting up Ajax and injecting behavior later d. Let’s concentrate for the rest of the day on component configurability. As you recall, earlier we created a SetProperties() function to hold all of the configurable aspects of our suggest script:

function SetProperties

(xElem, xHidden, xserverCode, xignoreCase, xmatchAnywhere, xmatchTextBoxWidth, xshowNoMatchMessage, xnoMatchingDataMessage, xuseTimeout, xtheVisibleTime){

...

}

This meets the requirement of providing configurability but not of providing a convenient API or appropriate defaults. For this, we introduce an options object that is passed into the constructor. The options object has a property for each configuration parameter of the suggest component. Let’s now fill in the options with some configuration parameters:

var suggestOptions = {

matchAnywhere

:

true,

ignoreCase

:

true

};

function injectSuggestBehavior() { suggest = new TextSuggest( 'field1',

'typeAheadXML.aspx', suggestOptions );

} );

This simple idiom comes with a big-time payload:

It keeps the signature of the constructor clean. The client pages using our component can construct it with only three parameters.

Configuration parameters can be added over time without changing the contract of the constructor.

We can write a smart setOptions() that provides appropriate default values for any unspecified properties, allowing the caller to specify only the properties that she wants to override.

400CHAPTER 10

Type-ahead suggest

The last bullet is exactly what the d setOptions() method shown earlier in the constructor does. Let’s look at how it works:

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

suggestDivClassName:

'suggestDiv',

suggestionClassName:

'suggestion',

matchClassName

:

'match',

matchTextWidth

:

true,

selectionColor

:

'#b1c09c',

matchAnywhere

:

false,

ignoreCase

:

false,

count

:

10

}.extend(options || {});

},

Each property in the options object that has an appropriate default value is specified here. Then, the extend() method of the Prototype library is called to override any properties specified in the options object passed in at construction time. The result is a merged options object that has the defaults and overrides specified in a single object! In the example we used here, the matchAnywhere and ignoreCase boolean properties were both overridden to values of true. The values of the configuration properties are explained in table 10.3.

Table 10.3 Values of configuration properties

Value

Explanation

 

 

suggestDivClassName

Specifies the CSS class name of the div element that will be generated to

 

hold the suggestions.

 

 

suggestionClassName

Specifies the CSS class name of the span element that is generated for

 

each suggestion.

 

 

matchClassName

Specifies the CSS class name of the span holding the portion of the sug-

 

gestion that matches what the user has typed in.

 

 

matchTextWidth

A boolean value indicating whether or not the div generated for the sugges-

 

tions should size itself to match the width of the text field it is attached to.

 

 

selectionColor

Specifies a hex value (or any valid value used as a CSS color specification)

 

for the background color of the selected suggestion.

 

 

matchAnywhere

A boolean value that specifies whether the match should be looked for only

 

at the beginning of a string or anywhere.

 

 

 

continued on next page