Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax Patterns And Best Practices (2006)

.pdf
Скачиваний:
39
Добавлен:
17.08.2013
Размер:
15.94 Mб
Скачать

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

169

function MonitorLinks( evt) {

evt = (evt) ? evt : ((event) ? event : null); if( evt) {

var elem = (evt.target) ? evt.target : ((evt.srcElement) ? evt.srcElement : null);

if( elem) {

if( elem.href == "http://www.apress.com/") { window.alert( "Not allowed on Apress"); return false;

}

else if( elem.href == "http://www.google.com/") { window.alert( "Not allowed on Google"); return false;

}

else if( elem.href == "http://www.slashdot.org/") { return true;

}

}

}

return false;

}

In the implementation of MonitorLinks, there is the usual code to retrieve the source HTML element (elem) and event (evt). If the variable elem is not null, the property elem.href is tested. The property is tested against three tests to see which link has been clicked. For the cases of having clicked on apress or google, a window.alert pop-up box appears, indicating that the link cannot be clicked. After the user clicks the OK button, the MonitorLinks function returns a value of false to indicate that the event bubbling should be canceled. Canceling the onclick event causes the navigation to be halted, with the HTML content staying as is.

You need to make a mental note that the function MonitorLinks assumes that the elem variable references a link element. The assumption is due to the property reference elem.href, because the href property is applicable only to a link element. It is not a bad thing to assume, but you must remember it because MonitorLinks is a function that captures the click event for all child HTML elements. If there were a button that generated a click event, MonitorLinks would fail and potentially cause undesired side effects. A solution is to use the elem.nodeName property and test whether the source element is a link. From the example, the if statement would be rewritten to the following:

if( elem && elem.nodeName == "A")

Another solution is to reference a common property such as id when testing for a specific link identifier. Using the id property is a useful solution because the property is type agnostic and is a unique identifier. The unique identifier is a good way to compare and distinguish HTML elements because there is no possibility of making a by-accident failure. A by-accident failure is illustrated by the following source code:

<a href="http://www.apress.com" target="external">Apress is not allowed</a> // ....

if( elem.href == "http://www.apress.com/") {

170

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

In the example source code, the href property is http://www.apress.com, but the comparison is the value http://www.apress.com/. Between the two buffers, there is a missing slash character. When the web browser processes the link element written as HTML, a slash is added to the href property. The added slash is not obvious to the script author and leads to a by-accident error, where by debugging you find out that a slash has been added. Using the id property, there is no translation by the web browser causing a by-accident error.

Following is the rewritten HTML that uses id properties to identify each link:

<div onclick="return MonitorLinks( event)">

<a href="http://www.apress.com" id="apress" target="external">Apress is not allowed</a>

<a href="http://www.google.com" id="google" target="external">Google is not allowed</a>

<a href="http://www.slashdot.org" id="slashdot" target="external">Slashdot is allowed</a>

</div>

Following is the MonitorLinks function rewritten to use the id property:

function MonitorLinks( evt) {

evt = (evt) ? evt : ((event) ? event : null); if( evt) {

var elem = (evt.target) ? evt.target : ((evt.srcElement) ? evt.srcElement : null);

if( elem) {

if( elem.id == "apress") {

window.alert( "Not allowed on Apress"); return false;

}

else if( elem.id == "google") { window.alert( "Not allowed on Google"); return false;

}

else if( elem.id == "slashdot") { return true;

}

}

}

return false;

}

The HTML and the function implementation stay relatively the same, with the only real change being the addition and comparison of the id property.

Other Ways to Define Events

There are other ways to wire events. One popular other way is to retrieve the HTML element and then associate a function to that element. So, for example, if you were capturing the click event, you would assign the onclick property to a function. Then when a click occurs, an event

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

171

is generated and the element captures it. Consider the following example that illustrates how to capture an event by using a property to wire the event:

function DoAssociation() { (document.getElementById(

"manualassociation"))[ 'onclick'] = MonitorLinksId; document.getElementById(

"manualassociation").attachEvent( 'onclick', MonitorLinksId);

}

</script>

<body onload="DoAssociation()">

In the example, the wiring of the methods to an HTML element should happen in the HTML element body onload event. It is important that only when the onload event is being fired that the events can be wired. If the wiring occurs before the document has been loaded, some HTML elements might not exist and cannot be referenced. The onload event ensures that

the HTML content has been loaded and can be referenced.

After the method DoAssociation is called, there are two ways to wire an event to an HTML element. In either way, it is important to call the document.getElementById method to retrieve an HTML element instance.

The first way to assign an event is to assign the array index of the event that is to be wired. In the example, that means assigning the onclick array index. This assignment illustrates a fundamental feature of JavaScript: there are no differences made between properties, functions, and so on.

The second way to assign an event is to use the method attachEvent (as illustrated) or addEventListener. When calling the methods attachEvent or addEventListener, you will need two parameters. The first parameter is the event to be captured, and the second parameter is the function to be associated with the event. In both cases, it is not necessary to use function variables or identifiers, because an anonymous function would be acceptable. You would use attachEvent with Microsoft Internet Explorer, and addEventListener when using a Mozillabased or Safari browser.

The advantage of using the array index approach is that it works on all browsers without any special requirements. However, it works because it is a special case of how the JavaScript language is constructed. The official approved way would be to use either addEventListener or attachEvent. After the events have been wired, they will function identically to the MonitorLinks function of previous examples.

If you do not want to associate the event to the HTML element in the body onload event, it can be done after the element has been declared, as illustrated by the following source code:

<div id="manualassociation"></div>

...

<script>

(document.getElementById(

"manualassociation"))[ 'onclick'] = MonitorLinksId;

...

In the example source code, the div element with an ID manualassociation is declared completely. After a complete declaration, the div HTML element exists in the Document Object Model, making the element accessible for referencing.

172

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

Of course, it goes without saying that a good programming practice in the implementation of MonitorLinks would be to test whether the evt variable is null. This is because when the events are wired together by using programmatic terms, the first parameter may or may not be the event.

Defining and Implementing the Common Data Functionality

As outlined earlier in this chapter, the Common Data functionality requires defining a state and potentially some function that processes the state. When processing the state, the data may be locally processed or may involve some remote processing. If the state is processed remotely, a URL is involved and the process requires URL design. Therefore, this section presents materials relating to URL design.

The Purpose of the State and State Manipulations

Some may perceive the Common Data functionality as unnecessary overhead. The Common Data functionality is a necessity, albeit (as described in the “Applicability” section) only when the Decoupled Navigation pattern is a necessity. The purpose of the Common Data functionality is to provide a wedge between the Action and Presentation functionalities, enabling a decoupling of functions.

Consider Figure 6-11, which illustrates the steps that occur when an HTML button is clicked, generating an event that causes a JavaScript function to be called.

Figure 6-11. Steps resulting from clicking a button

Figure 6-11 represents the simple button click as two steps. The first step is the HTML event, which processes the mouse click. The second step is the content generation in the table row below the button. The content uses HTML injection by assigning the innerHTML property. From this simple example, there would be no need for the Common Data functionality because that would add an unnecessary layer.

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

173

Let’s continue building on this example. Imagine that the same user interface is used to make a remote call via the XMLHttpRequest object. Figure 6-12 illustrates the steps needed in the remote case.

Figure 6-12. Steps resulting from clicking a button when using an extra XMLHttpRequest call

Figure 6-12 shows an added step (step 2), in which a request is made by using the XMLHttpRequest object that then generates some data that is processed in step 3.

Looking at Figures 6-11 and 6-12, you might be wondering where the need for the Common Data functionality is. The need arises because often an application is converted from the state depicted in Figure 6-11 to that in Figure 6-12, or vice versa. Implementing the conversion can require some major restructuring of the code, or a completely new implementation that needs to be tested and maintained. The Common Data functionality decouples the steps so that an application that executed as in Figure 6-11 could be converted without major surprises into an application that executes as in Figure 6-12. The intention is to decouple, allowing the fewest number of changes and yielding the largest user benefit.

Consider the following code, which mimics the implementation of Figure 6-11:

function OnClick( event) {

document.getElementById( "myDiv").innerHTML = "data";

}

The code is a problem because what was defined as two steps in Figure 6-11 is one step in technical terms. The code is a function with an implementation. The problems of the function OnClick are that the text identifier myDiv is hard-coded, and so is the assigned value data. Imagine that the assignment code is used in multiple places, and imagine that the text has to be converted to uppercase before doing the assignment. Then the code would have to be updated in multiple places.

174

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

The solution is to decouple the steps of Figure 6-11, which was illustrated as a single piece of code, into two code pieces. The decoupled code would be as follows:

function InjectHTML( elementId, text) { document.getElementById( elementId).innerHTML = text;

}

function OnClick( event) { InjectHTML( "myDiv", "data");

}

There are now two functions (InjectHTML and OnClick). The function InjectHTML requires an element identifier and some text, and will perform an HTML injection. The function InjectHTML is a business-logic-specific implementation that operates on an HTML element reference defined by the client. The function OnClick reacts to the event and is responsible for gathering the data used to call the InjectHTML function. Each function has its responsibilities and each function is decoupled from the other. The only shared information is the data gathered by OnClick and processed by InjectHTML.

Figure 6-11 has been implemented by using a decoupled solution, but now Figure 6-12 needs to be implemented. This means that an additional step of using the XMLHttpRequest object needs to be added. For simplicity, assume that the XMLHttpRequest object functionality is encapsulated in the function CallXMLHttpRequest, which accepts a single parameter. As the function CallXMLHttpRequest is used to gather information, the function is called by OnClick, and the returned data is passed to the InjectHTML function. The modified source code is as follows:

function InjectHTML( elementId, text) { document.getElementById( elementId).innerHTML = text;

}

function OnClick( event) {

InjectHTML( "myDiv", CallXMLHttpRequest( "data"));

}

In the modified source code, the second parameter of the CallXMLHttpRequest function has been replaced with the function CallXMLHttpRequest. Looking at the solution technically, you can see that the three steps have been decoupled from each other, and each can vary without affecting the other. What is still kludgy is how the data is gathered and passed to the function InjectHTML. This is the reason for creating Common Data functionality.

The Common Data functionality replaces the kludgy calling of the functions with some common state. The problem at the moment is that the OnClick function relies on the functions InjectHTML and CallXMLHttpRequest. The reliance cannot be avoided, but what can be avoided is the calling convention. Imagine that instead of InjectHTML being used, the function InjectTextbox is used due to a business logic decision. And then imagine that InjectTextbox requires an extra parameter, as illustrated by the following source code:

function InjectTextbox( convert, elementId, text) { // ....

}

function OnClick( event) {

InjectTextbox( false, "myDiv", CallXMLHttpRequest( "data"));

}

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

175

Even though InjectTextbox and InjectHTML are similar, calling InjectTextbox requires a change in the logic of the OnClick function. The OnClick function has to make an additional decision of whether or not a conversion has to occur. You might say, “Well, duh, the OnClick function has to change if the called function changes.” But the reply is, “Why must the OnClick function change?” The purpose of the OnClick function is to gather the data necessary to call either the InjectHTML or InjectTexbox function. The purpose of the OnClick function is not to make decisions, because decisions can change if a user interface does not, and vice versa. The data gathering and decisions made about the data need to be decoupled.

In an ideal world where everything is decoupled, you would write the following source code:

<button onclick="Call( OnClick, null, InjectHTML)" />

<button onclick="Call( OnClick, CallXMLHttpRequest, InjectTextbox)" />

The modified source code has added the function Call, which has three parameters that are three function references. The first function reference, OnClick, is the Action functionality responsible for gathering the data into a state. The second function reference is either null or CallXMLHttpRequest and represents the Common Data functionality that is responsible for processing the state. And finally, the third function references, InjectHTML and InjectTextbox, are responsible for displaying the state.

The resulting calling sequence illustrates that the first button click event gathers the data, does not process the data, and displays the data. The second button click event gathers the data, calls a remotely located server, and displays the data. The OnClick functions used in either button click event are identical, meaning that the OnClick event is not dependent on whether the processing of the common data is local or remote. So now the functions are decoupled, as is the calling sequence. The exact details of this decoupling and the calling of the functions is the topic of the following sections.

Implementing a Decoupled Library

The core of the Common Data functionality is a decoupled library, which is responsible for managing and processing the state. The decoupled library is called DecoupledNavigation and is defined as follows:

function DecoupledNavigation() {

}

DecoupledNavigation.prototype.call = DecoupledNavigation_call; DecoupledNavigation.prototype.initializeRemote =

DecoupledNavigation_InitializeRemote;

The definition of DecoupledNavigation has no properties and two methods. There are no properties because the common state object instance is defined in the implementation of the DecoupledNavigation methods. The method DecoupledNavigation_call is used to make a Decoupled Navigation pattern call as illustrated in the example Call( OnClick...). The method DecoupledNavigation_initializeRemote is used when the Common Data functionality wants to make a call to a remote server.

The function DecoupledNavigation_call, exposed as DecoupledNavigation.call, wires together the Action, Common Data, and Presentation functionalities as illustrated by the following implementation:

176 C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

function DecoupledNavigation_call( evt, action, data, presentation) { evt = (evt) ? evt : ((event) ? event : null);

if ( evt) {

var elem = (evt.target) ? evt.target : ((evt.srcElement) ? evt.srcElement : null);

if ( elem) {

var obj = new Object(); obj.event = evt; obj.parent = this; obj.element = elem; obj.state = new Object();

obj.presentation = presentation; if ( (action) != null) {

if ( action( obj) != true) { return false;

}

}

obj.isRemote = false; if ( (data) != null) {

if ( data( obj) != true) { return false;

}

}

if( obj.isRemote) { return true;

}

if (presentation != null) {

if ( presentation( obj, obj.state) != true) { return false;

}

}

return true;

}

}

return true;

}

The implementation of DecoupledNavigation_call has four parameters. The first parameter, evt, is the event object. Whether the first parameter has a valid value goes back to the event problem outlined in the “Event Bubbling” section. The second parameter, action, is a function reference to an Action functionality (for example, OnClick). The third parameter, data, represents the function reference that performs a state manipulation. The fourth parameter, presentation, is a function reference to a Presentation functionality, which usually is some HTML control. All of the lines up to if( elem) were outlined in the “Event Bubbling” section and are used to extract the HTML event and HTML source element.

The lines thereafter are the important lines and represent the technical details of the Common Data functionality. These lines represent the state as an object instead of a series of

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

177

parameters, as illustrated by the example that had the OnClick function call either InjectHTML or InjectTextbox. So let’s look at those lines in more detail:

var obj = new Object(); obj.event = evt; obj.parent = this; obj.element = elem; obj.state = new Object();

obj.presentation = presentation;

The variable obj is the common object that is shared by the action, data, and presentation function references. The idea is to convert the parameters gathered by the example function OnClick and to convert them into an object instance. Based on that idea, the action function implementation manipulates obj and assigns the state. The state is then manipulated and processed by the data function reference. The state structures can be anything, and most likely will partially resemble the parameters used to call the example InjectHTML or InjectTextbox functions. It is essential that the action, data, and presentation function implementations know what the structure of the state is. The advantage of manipulating an object structure is that the calling code as illustrated by OnClick does not need to be modified. Only the functions that modify the object structure need to be modified, preserving a proven and testing navigation structure.

Getting back to the explanation of the obj properties, event and element reference the HTML event and source HTML element, respectively. The property state is the state that is manipulated by the various functionalities. The reason for using the state property is to provide an entry point for the common state that will not conflict with the other properties of obj. And the reference obj.presentation is required if a remote call is made; this need will be illustrated in a little bit.

Going back a bit further in the example source code, let’s look at the implementation of DecoupledNavigation_call. After obj has been instantiated, the calling of the action function reference is called, as illustrated again here:

if( (action) != null) {

if( action( obj) != true) { return false;

}

}

Before the action function reference can be called, a decision is made that ensures that the action variable is not null. If the action variable is null, there is no implementation for the Action functionality. This is useful, for example, if you’re creating a splash screen and you don’t need to generate a state but only some presentation information when the document has finished loading.

If the action variable is not null, the action function reference is called, where the parameter is the common object obj. The action function implementation can query and manipulate obj, and then return either a true or false. If the action function implementation is successful, true is returned. Returning false indicates a failure, which will cause DecoupledNavigation_ local to return false, causing the event bubbling to quit, if applicable.

178

C H A P T E R 6 D E C O U P L E D N A V I G A T I O N P A T T E R N

After the Action functionality has been executed, the property obj.state will be assigned and will be ready to be processed by the data function reference. The details of using the data function reference are illustrated again here:

obj.isRemote = false; if ( (data) != null) {

if ( data( obj) != true) { return false;

}

}

if( obj.isRemote) { return true;

}

The calling sequence of the data function reference is identical to the calling sequence of the action function reference. What is different is the assignment of the property obj.isRemote = false. The difference is due to the ability of the data function reference to process the state locally or remotely. If the data function reference processes the state remotely, an asynchronous call is made and further processing can continue only after the remote server has sent a response. The DecoupledNavigation_call function cannot continue and must return control to the web browser. The property assignment is used to indicate whether a remote server call is made. If a remote call is made, the presentation function reference cannot be called, and the function DecoupledNavigation_call returns a value of true.

This raises the question of whether a true or false value should be returned if the obj. isRemote property has a true value. Returning a value of true means that the event will continue to bubble, and depending on the context that might not be the best plan of action. The best plan of action depends on the context, and there is room for improvement in how the return value of the data function reference is handled.

If the data is processed locally, the Presentation functionality can be called. The calling sequence is illustrated as follows:

if( presentation != null) {

if( presentation( obj, obj.state) != true) { return false;

}

}

The calling of the Presentation functionality is identical to the Action and Data functionalities. The additional parameter obj.state is the state, and its presence makes it possible to recursively chain together multiple presentation functionalities, as illustrated in Figure 6-13.

Figure 6-13 illustrates how the function MyPresentation acts as a front processor for the functions InjectHTML and InjectTextbox. Because the state is a parameter, the front processor can filter out the appropriate state structure and then pass that state structure to the other Presentation functionalities. If state were not a parameter, the front processor would have to reassign the state property of the common variable.

The implementation of the function DecoupledNavigation_InitializeRemote has been delegated until a remote server call example is made. For now, the focus is on using the DecoupledNavigation class to perform a local call.