Ajax Hacks: Tips & Tools for Creating Responsive Web Sites

Type in a city and choose a state name; this hack quickly generates the U.S. postal code.

This hack asks the user for the name of a city and state, then generates the associated postal code in a text field (using a web service accessible at http://www.webservicex.net/uszip.asmx?op=GetInfoByState). Nothing else about the web page changes. Cool, and useful, too. How many people remember postal codes other than their own?

The hack gets the city and state values, then uses the request object to fetch the ZIP Code from a server-side component, which interacts with the web service. The server-side component pulls the ZIP Code out of an XML file it gets from the service and sends the code to our Ajax application.

We started out using the U.S. Postal Service's Web Tools API, which likely contains the most up-to-date ZIP Codes, as well as the four-digit codes that extend some of the five-digit codes. However, the USPS was very restrictive in terms of allowing us to write about the use of its web tools in our hacksfairly bureaucratic and not very cooperativeso we were not able to use a full implementation of its ZIP Code service for this hack.

Figure 4-11 shows what the page looks like in the browser.

Figure 4-11. Automatically get a ZIP Code

When the user chooses a state from the pop-up list, the application code sends the city and state to the server component, but only if the city field contains content (the user could leave it empty by mistake). Here is what the web page's code looks like:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/2000/REC-xhtml1-20000126/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <script type="text/javascript" src="/books/4/254/1/html/2/js/http_request.js"></script> <script type="text/javascript" src="/books/4/254/1/html/2/js/hacks3_6b.js"></script> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <title>Postal Code Search</title> </head> <body> <h3>Please enter the city and state</h3> <form action="javascript:void%200" method="get"> <p> City: <input type="text" size="20" maxlength="20" /> State: <select > <option value="al">Alabama</option> <option value="ak">Alaska</option> <option value="az">Arizona</option> <option value="ar">Arkansas</option> <option value="ca">California</option> <option value="co">Colorado</option> <!etc. --> </select> </p><p> Zip code: <input type="text" size="5" maxlength="5" /> </p> <div ></div> </form> </body> </html>

The web page imports two JavaScript files, http_request.js and hacks3_6b.js. http_request.js (see "Use Your Own Library for XMLHttpRequest" [Hack #3]) is responsible for setting up and using XMLHttpRequest. This file contains the httpRequest( ) function that hacks3_6b.js uses. The code in hacks3_6b.js handles the user's clicks on the pop-up list, sends a request with the city and state values, and then displays the returned ZIP Code in the zip5 text field. Here is the code in hacks3_6b.js:

window.onload=function( ){ var sts = document.getElementById("sts"); sts.onclick=function( ){ var cit = document.getElementById("city"); //Only make a request if the city text field //has a value if(cit.value) {getZipcode(cit.value,sts.value.toUpperCase( ));} }; }; function getZipcode(_ct,_st){ if(_ct.length > 0 && _st.length > 0){ httpRequest("GET","http://www.parkerriver.com/s/zip?city="+ encodeURIComponent(_ct)+"&state="+ encodeURIComponent(_st), true,handleResponse); } else { document.getElementById("zip5").value=""; } } function handleResponse( ){ var xmlReturnVal; try{ if(request.readyState == 4){ if(request.status == 200){ xmlReturnVal=request.responseXML; if(xmlReturnVal != null) { var zip5=xmlReturnVal.getElementsByTagName("zip")[0]; if(zip5 && zip5.childNodes.length > 0) { document.getElementById("zip5"). value=zip5.childNodes[0].data; } } } else { //request.status is 503 //if the application isn't available; //500 if the application has a bug alert( "A problem occurred with communicating between"+ " the XMLHttpRequest object and the server program."); } }//end outer if } catch (err) { alert("It does not appear that the server "+ "is available for this application. Please"+ " try again very soon. \\nError: "+err.message); } }

You've probably encountered this window.onload event handler in other hacks. It's an "event handler that sets up another event handler." When the browser completes loading the web page, window.onload specifies what happens when the user makes a selection from the pop-up list displaying the U.S. states:

window.onload=function( ){ var sts = document.getElementById("sts"); sts.onclick=function( ){ var cit = document.getElementById("city"); //Only make a request if the city text field //has a value if(cit.value) {getZipcode(cit.value,sts.value.toUpperCase( ));} }; };

The code gets the value of the city text field and the U.S. states pop-up, then calls getZipCode( ). This function puts together the URL that will connects this application to the server component. The code then calls the httpRequest( ) function to fetch the ZIP Code:

httpRequest("GET","http://localhost:8080/parkerriver/s/zip?city="+ encodeURIComponent(_ct)+"&state="+ encodeURIComponent(_st), true,handleResponse);

Again, httpRequest( ) is defined in http_request.js.

Server, Take Over

The server component has to connect with the web service, which sends back a large XML file containing all the ZIP Codes for a specific state. You have to use your own server intermediary because of the XMLHttpRequest restriction on connecting with a host that is different from the host from which the user downloaded the web page.

This particular web service, which is generously made available to our code, does not have an operation that returns just a ZIP Code in response to a city and state name. Therefore, you have to take this extra step to glean the ZIP Code from the XML file.

The web service returns an XML file that looks like this:

<NewDataSet> <Table> <CITY>Abington</CITY> <STATE>MA</STATE> <ZIP>02351</ZIP> <AREA_CODE>781</AREA_CODE> <TIME_ZONE>E</TIME_ZONE> </Table> <Table> <CITY>Accord</CITY> <STATE>MA</STATE> <ZIP>02018</ZIP> <AREA_CODE>781</AREA_CODE> <TIME_ZONE>E</TIME_ZONE> </Table> ... </NewDataSet>

The server component uses the Simple API for XML (SAX) to parse this return value. When the Java component finds the city name the user provided, it pulls out the associated ZIP Code and sends it to our Ajax application in the form <zip>02351</zip>. The handleResponse( ) function then makes sure this value is placed in the ZIP Coderelated text field. It all happens quite fast, considering the complexity involved!

Here is a snippet from handleResponse( ):

if(request.status == 200){ xmlReturnVal=request.responseXML; if(xmlReturnVal != null) { var zip5=xmlReturnVal.getElementsByTagName("zip")[0]; if(zip5 && zip5.childNodes.length > 0) { document.getElementById("zip5"). value=zip5.childNodes[0].data; } }

A property of the request object called responseXML stores the returned ZIP Code, which is encapsulated in a <zip> tag. The code xmlReturnVal.getElementsByTagName("zip")[0] returns the tag holding the ZIP Code. The last line of the code sample then stores the ZIP Code in the text field with id zip5.

The Servlet

As mentioned earlier, the application cannot connect directly with the web service using XMLHttpRequest because our web page has a different host than the web service's host. As a final step, let's look at the Java servlet that acts as the intermediary between the web service and the Ajax code. It sifts through all the ZIP Codes for a certain state and returns the first ZIP Code it finds that is associated with the specified city (this is a potential flaw in the application, as some cities can have multiple ZIP Codes):

package com.parkerriver; import java.net.*; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import javax.xml.parsers.*; import org.apache.log4j.*; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.*; public class ZipServlet2 extends HttpServlet { private Logger log = null; private String zipCode = null; public String getZipCode( ) { return zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } private static String wsUrl= "http://www.webservicex.net/uszip.asmx/GetInfoByState?USState="; public void init( ) throws ServletException { log = Logger.getLogger(ZipServlet2.class); } protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { String stte = httpServletRequest.getParameter("state"); String _city = httpServletRequest.getParameter("city"); String resp = null; if(stte != null && _city != null){ URL usps = new URL(wsUrl+stte); HttpURLConnection usp = (HttpURLConnection) usps. openConnection( ); usp.setRequestMethod("GET"); usp.setDoInput(true); usp.connect( ); BufferedReader in = new BufferedReader( new InputStreamReader( usp.getInputStream( ))); StringBuffer buf = new StringBuffer(""); String inputLine; while ((inputLine = in.readLine( )) != null) { buf.append(inputLine); } in.close( ); resp = buf.toString( ); try { getZipSax(resp,_city); resp="<zip>"+this.getZipCode( )+"</zip>"; } catch (ParserConfigurationException e) { e.printStackTrace( ); } } else { resp="<error />"; } httpServletResponse.setContentType("text/xml; charset=UTF-8"); //Convey to the user agent or browser that it should //not cache the responses httpServletResponse.setHeader("Cache-Control", "no-cache"); httpServletResponse.getWriter( ).write(resp); } protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { doGet(httpServletRequest, httpServletResponse); } /* Parse the XML file of zip codes using our own DefaultHandler. Give the ContentHandler the name of the city the user provided. */ private void getZipSax(String zipXML,String _city) throws ParserConfigurationException, IOException { try { SAXParser parser = SAXParserFactory.

newInstance( ).newSAXParser( ); parser.parse(new InputSource(new StringReader(zipXML)), new MyHandler(_city)); } catch (SAXException sxe) { log.info("Caught SAXException: "+sxe.getMessage( )); } } /* A SAX ContentHandler that parses an XML file; it sets the parent class's zipCode property when it finds the correct zip code in the XML, then throws a SAXException to halt the parsing. */ class MyHandler extends DefaultHandler{ private String city; private boolean foundCityFlag,foundCityVal,foundZipFlag; public MyHandler( ) { super( ); } public MyHandler(String _city) { this( ); this.city=_city; } public void startElement(String string, String string1, String string2, Attributes attributes) throws SAXException { if(string2.equalsIgnoreCase("city")){ foundCityFlag=true; } if(foundCityVal){ if(string2.equalsIgnoreCase("zip")){ foundZipFlag=true; } } } public void characters(char[] chars, int i, int i1) throws SAXException { if(foundCityFlag){ if(new String(chars,i,i1).equalsIgnoreCase(city)){ foundCityVal=true; } else { foundCityFlag=false; } } if(foundZipFlag){ setZipCode(new String(chars,i,i1)); throw new SAXException("We found the zip code."); } } } }

The SAX technique uses a callback objectin this case, a class called MyHandlerto look for city tags that contain the user's specified city. The MyHandler object implements an interface that is part of the SAX API called ContentHandler. A ContentHandler lets the programmer decide what should happen as the parser sifts through the XML stream at different stages: when the handler finds the beginning of an XML element, the content of an element, the end of an element, and so on.

If the ContentHandler finds a city tag whose content matches the user's chosen city, it looks for an associated zip tag and grabs the value of that tag. It then throws a SAXExceptionthe Java way of signaling that the XML parsing can stopbecause the code has found the ZIP Code value.

See http://www.saxproject.org for more information on SAX.

This is a nifty way to display ZIP Codes to the user because they generally appear in the text field very quickly, without the page refreshing or changing in any other way. The user just has to correctly spell the city, choose a state in the pop-up, and presto, there's the ZIP Code.

Категории