20 min read

Implementing Tables and Lists

The first actual sample is very common in applications: tables and lists. In this sample, the table is populated using the DWR utility functions, and a remoted Java class. The sample code also shows how DWR is used to do inline table editing. When a table cell is double-clicked, an edit box opens, and it is used to save new cell data.

The sample will have country data in a CSV file: country Name, Long Name, two-letter Code, Capital, and user-defined Notes. The user interface for the table sample appears as shown in the following screenshot:

DWR Java AJAX User Interface: Basic Elements (Part 2)

Server Code for Tables and Lists

The first thing to do is to get the country data. Country data is in a CSV file (named countries.csv and located in the samples Java package). The following is an excerpt of the content of the CSV file (data is from http://www.state.gov ).

Short-form name,Long-form name,FIPS Code,Capital
Afghanistan,Islamic Republic of Afghanistan,AF,Kabul
Albania,Republic of Albania,AL,Tirana
Algeria,People's Democratic Republic of Algeria,AG,Algiers
Andorra,Principality of Andorra,AN,Andorra la Vella
Angola,Republic of Angola,AO,Luanda
Antigua andBarbuda,(no long-form name),AC,Saint John's
Argentina,Argentine Republic,AR,Buenos Aires
Armenia,Republic of Armenia,AM,Yerevan

The CSV file is read each time a client requests country data. Although this is not very efficient, it is good enough here. Other alternatives include an in-memory cache or a real database such as Apache Derby or IBM DB2. As an example, we have created a CountryDB class that is used to read and write the country CSV. We also have another class, DBUtils, which has some helper methods. The DBUtils code is as follows:

package samples;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.List;
import java.util.Vector;

public class DBUtils {

private String fileName=null;
public void initFileDB(String fileName)
{
this.fileName=fileName;
// copy csv file to bin-directory, for easy
// file access
File countriesFile = new File(fileName);
if (!countriesFile.exists()) {
try {
List<String> countries = getCSVStrings(null);
PrintWriter pw;
pw = new PrintWriter(new FileWriter(countriesFile));
for (String country : countries) {
pw.println(country);
}
pw.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

protected List<String> getCSVStrings(String letter) {
List<String> csvData = new Vector<String>();
try {
File csvFile = new File(fileName);
BufferedReader br = null;
if(csvFile.exists())
{
br=new BufferedReader(new FileReader(csvFile));
}
else
{
InputStream is = this.getClass().getClassLoader()
.getResourceAsStream("samples/"+fileName);
br=new BufferedReader(new InputStreamReader(is));
br.readLine();
}

for (String line = br.readLine(); line != null; line = br.readLine()) {
if (letter == null
|| (letter != null && line.startsWith(letter))) {
csvData.add(line);
}
}
br.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
return csvData;
}
}

The DBUtils class is a straightforward utility class that returns CSV content as a List of Strings. It also copies the original CSV file to the runtime directory of any application server we might be running. This may not be the best practice, but it makes it easier to manipulate the CSV file, and we always have the original CSV file untouched if and when we need to go back to the original version.

The code for CountryDB is given here:

package samples;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;

public class CountryDB {

private DBUtils dbUtils = new DBUtils();
private String fileName = "countries.csv";

public CountryDB() {
dbUtils.initFileDB(fileName);
}

public String[] getCountryData(String ccode) {
List<String> countries = dbUtils.getCSVStrings(null);
for (String country : countries) {
if (country.indexOf("," + ccode + ",") > -1) {
return country.split(",");
}
}
return new String[0];
}

public List<List<String>> getCountries(String startLetter) {

List<List<String>> allCountryData = new Vector<List<String>>();
List<String> countryData = dbUtils.getCSVStrings(startLetter);
for (String country : countryData) {
String[] data = country.split(",");
allCountryData.add(Arrays.asList(data));
}
return allCountryData;
}

public String[] saveCountryNotes(String ccode, String notes) {
List<String> countries = dbUtils.getCSVStrings(null);
try {
PrintWriter pw = new PrintWriter(new FileWriter(fileName));
for (String country : countries) {
if (country.indexOf("," + ccode + ",") > -1) {
if (country.split(",").length == 4) {
// no existing notes
country = country + "," + notes;
} else {
if (notes.length() == 0) {
country = country.substring(0, country
.lastIndexOf(","));
} else {
country = country.substring(0, country
.lastIndexOf(","))
+ "," + notes;
}

}
}
pw.println(country);
}
pw.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
String[] rv = new String[2];
rv[0] = ccode;
rv[1] = notes;
return rv;
}
}

The CountryDB class is a remoted class. The getCountryData() method returns country data as an array of strings based on the country code. The getCountries() method returns all the countries that start with the specified parameter, and saveCountryNotes() saves user added notes to the country specified by the country code.

In order to use CountryDB, the following script element must be added to the index.jsp file together with other JavaScript elements.

<script type='text/javascript' src='/DWREasyAjax/dwr/interface/CountryDB.js'></script>

There is one other Java class that we need to create and remote. That is the AppContent class that was already present in the JavaScript functions of the home page. The AppContent class is responsible for reading the content of the HTML file and parses the possible JavaScript function out of it, so it can become usable by the existing JavaScript functions in index.jsp file.

package samples;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Vector;

public class AppContent {

public AppContent()
{

}

public List<String> getContent(String contentId)
{
InputStream is = this.getClass().getClassLoader().getResourceAsStream(
"samples/"+contentId+".html");
String content=streamToString(is);
List<String> contentList=new Vector<String>();
//Javascript within script tag will be extracted and sent separately to client
for(String script=getScript(content);!script.equals("");script=getScript(content))
{
contentList.add(script);
content=removeScript(content);
}
//content list will have all the javascript
//functions, last element is executed last
//and all other before html content
if(contentList.size()>1)
{
contentList.add(contentList.size()-1, content);
}
else
{
contentList.add(content);
}
return contentList;
}

public List<String> getLetters()
{
List<String> letters=new Vector<String>();
char[] l=new char[1];
for(int i=65;i<91;i++)
{
l[0]=(char)i;
letters.add(new String(l));
}
return letters;
}

public String removeScript(String html)
{
//removes first script element
int sIndex=html.toLowerCase().indexOf("<script ");
if(sIndex==-1)
{
return html;
}
int eIndex=html.toLowerCase().indexOf("</script>")+9;
return html.substring(0, sIndex)+html.substring(eIndex);
}

public String getScript(String html)
{
//returns first script element
int sIndex=html.toLowerCase().indexOf("<script ");
if(sIndex==-1)
{
return "";
}
int eIndex=html.toLowerCase().indexOf("</script>")+9;
return html.substring(sIndex, eIndex);
}

public String streamToString(InputStream is)
{
String content="";
try
{
ByteArrayOutputStream baos=new ByteArrayOutputStream();
for(int b=is.read();b!=-1;b=is.read())
{
baos.write(b);
}
content=baos.toString();
}
catch(IOException ioe)
{
content=ioe.toString();
}
return content;
}
}

The getContent() method reads the HTML code from a file based on the contentId. ContentId was specified in the dwrapplication.properties file, and the HTML is just contentId plus the extension .html in the package directory. There is also a getLetters() method that simply lists letters from A to Z and returns a list of letters to the browser.

If we test the application now, we will get an error as shown in the following screenshot:

DWR Java AJAX User Interface: Basic Elements (Part 2)

We know why the AppContent is not defined error occurs, so let’s fix it by adding AppContent to the allow element in the dwr.xml file. We also add CountryDB to the allow element. The first thing we do is to add required elements to the dwr.xml file. We add the following creators within the allow element in the dwr.xml file.

<create creator="new" javascript="AppContent">
<param name="class" value="samples.AppContent" />
<include method="getContent" />
<include method="getLetters" />
</create>
<create creator="new" javascript="CountryDB">
<param name="class" value="samples.CountryDB" />
<include method="getCountries" />
<include method="saveCountryNotes" />
<include method="getCountryData" />
</create>

We explicitly define the methods we are remoting using the include elements. This is a good practice, as we don’t accidentally allow access to any methods that are not meant to be remoted.

Client Code for Tables and Lists

We also need to add a JavaScript interface to the index.jsp page. Add the following with the rest of the scripts in the index.jsp file.

<script type='text/javascript' src='/DWREasyAjax/dwr/interface/AppContent.js'></script>

Before testing, we need the sample HTML for the content area. The following HTML is in the TablesAndLists.html file under the samples directory:

<h3>Countries</h3>
<p>Show countries starting with
<select id="letters" onchange="selectLetter(this);return false;"> </select><br/>
Doubleclick "Notes"-cell to add notes to country.
</p>
<table border="1">
<thead>
<tr>
<th>Name</th>
<th>Long name</th>
<th>Code</th>
<th>Capital</th>
<th>Notes</th>
</tr>
</thead>
<tbody id="countryData">
</tbody>
</table>

<script type='text/javascript'>
//TO BE EVALED
AppContent.getLetters(addLetters);
</script>

The script element at the end is extracted by our Java class, and it is then evaluated by the browser when the client-side JavaScript receives the HTML. There is the select element, and its onchange event calls the selectLetter() JavaScript function. We will implement the selectLetter() function shortly.

JavaScript functions are added in the index.jsp file, and within the head element. Functions could be in separate JavaScript files, but the embedded script is just fine here.

function selectLetter(selectElement)
{
var selectedIndex = selectElement.selectedIndex;
var selectedLetter= selectElement.options[selectedIndex ].value;
CountryDB.getCountries(selectedLetter,setCountryRows);
}

function addLetters(letters)
{
dwr.util.addOptions('letters',['letter...']);
dwr.util.addOptions('letters',letters);
}

function setCountryRows(countryData)
{
var cellFuncs = [
function(data) { return data[0]; },
function(data) { return data[1]; },
function(data) { return data[2]; },
function(data) { return data[3]; },
function(data) { return data[4]; }
];
dwr.util.removeAllRows('countryData');
dwr.util.addRows( 'countryData',countryData,cellFuncs, {

cellCreator:function(options) {
var td = document.createElement("td");
if(options.cellNum==4)
{
var notes=options.rowData[4];
if(notes==undefined)
{
notes='&nbsp;';// + options.rowData[2]+'notes';
}
var ccode=options.rowData[2];
var divId=ccode+'_Notes';
var tdId=divId+'Cell';
td.setAttribute('id',tdId);
var html=getNotesHtml(ccode,notes);
td.innerHTML=html;
options.data=html;
}
return td;
},
escapeHtml:false
});
}

function getNotesHtml(ccode,notes)
{
var divId=ccode+'_Notes';
return "<div onDblClick="editCountryNotes('"+divId+"','"+ccode+"');"
id=""+divId+"">"+notes+"</div>";
}

function editCountryNotes(id,ccode)
{
var notesElement=dwr.util.byId(id);
var tdId=id+'Cell';
var notes=notesElement.innerHTML;
if(notes=='&nbsp;')
{
notes='';
}
var editBox='<input id="'+ccode+'NotesEditBox" type="text" value="'+notes+'"/><br/>';
editBox+="<input type='button' id='"+ccode+"SaveNotesButton' value='Save'
onclick='saveCountryNotes(""+ccode+"");'/>";
editBox+="<input type='button' id='"+ccode+"CancelNotesButton' value='Cancel'
onclick='cancelEditNotes(""+ccode+"");'/>";
tdElement=dwr.util.byId(tdId);
tdElement.innerHTML=editBox;
dwr.util.byId(ccode+'NotesEditBox').focus();
}

function cancelEditNotes(ccode)
{
var countryData=CountryDB.getCountryData(ccode, {
callback:function(data)
{
var notes=data[4];
if(notes==undefined)
{
notes='&nbsp;';
}
var html=getNotesHtml(ccode,notes);
var tdId=ccode+'_NotesCell';
var td=dwr.util.byId(tdId);
td.innerHTML=html;
}
});

}

function saveCountryNotes(ccode)
{
var editBox=dwr.util.byId(ccode+'NotesEditBox');
var newNotes=editBox.value;
CountryDB.saveCountryNotes(ccode,newNotes, {
callback:function(newNotes)
{
var ccode=newNotes[0];
var notes=newNotes[1];
var notesHtml=getNotesHtml(ccode,notes);
var td=dwr.util.byId(ccode+"_NotesCell");
td.innerHTML=notesHtml;
}
});
}

There are lots of functions for table samples, and we go through each one of them.

The first is the selectLetter() function. This function gets the selected letter from the select element and calls the CountryDB.getCountries() remoted Java method. The callback function is setCountryRows. This function receives the return value from the Java getCountries() method, that is List<List<String>>, a List of Lists of Strings.

The second function is addLetters(letters), and it is a callback function for theAppContent.getLetters() method, which simply returns letters from A to Z. The addLetters() function uses the DWR utility functions to populate the letter list.

Then there is a callback function for the CountryDB.getCountries() method. The parameter for the function is an array of countries that begin with a specified letter. Each array element has a format: Name, Long name, (country code) Code, Capital, Notes. The purpose of this function is to populate the table with country data; and let’s see how it is done. The variable, cellFuncs, holds functions for retrieving data for each cell in a column. The parameter named data is an array of country data that was returned from the Java class.

The table is populated using the DWR utility function, addRows(). The cellFuncs variable is used to get the correct data for the table cell. The cellCreator function is used to create custom HTML for the table cell. Default implementation generates just a td element, but our custom implementation generates the td-element with the div placeholder for user notes.

The getNotesHtml() function is used to generate the div element with the event listener for double-click.

The editCountryNotes() function is called when the table cell is double-clicked. The function creates input fields for editing notes with the Save and Cancel buttons.

The cancelEditNotes() and saveCountryNotes() functions cancel the editing of new notes, or saves them by calling the CountryDB.saveCountryNotes() Java method.

The following screenshot shows what the sample looks like with the populated table:

DWR Java AJAX User Interface: Basic Elements (Part 2)

Now that we have added necessary functions to the web page we can test the application.


Testing Tables and Lists

The application should be ready for testing if we have had the test environment running during development. Eclipse automatically deploys our new code to the server whenever something changes. So we can go right away to the test page http://127.0.0.1:8080/DWREasyAjax. On clicking Tables and lists we can see the page we have developed. By selecting some letter, for example “I” we get a list of all the countries that start with letter “I” (as shown in the previous screenshot).

Now we can add notes to countries. We can double-click any table cell under Notes. For example, if we want to enter notes to Iceland, we double-click the Notes cell in Iceland’s table row, and we get the edit box for the notes as shown in the following screenshot:

DWR Java AJAX User Interface: Basic Elements (Part 2)

The edit box is a simple text input field. We didn’t use any forms. Saving and canceling editing is done using JavaScript and DWR. If we press Cancel, we get the original notes from the CountryDB Java class using DWR and saving also uses DWR to save data. CountryDB.saveCountryNotes() takes the country code and the notes that the user entered in the edit box and saves them to the CSV file. When notes are available, the application will show them in the country table together with other country information as shown in the following screenshot:

DWR Java AJAX User Interface: Basic Elements (Part 2)

Afterword

The sample in this section uses DWR features to get data for the table and list from the server. We developed the application so that most of the application logic is written in JavaScript and Java beans that are remoted. In principle, the application logic can be thought of as being fully browser based, with some extensions in the server.

Implementing Field Completion

Nowadays, field completion is typical of many web pages. A typical use case is getting a stock quote, and field completion shows matching symbols as users type letters. Many Internet sites use this feature.

Our sample here is a simple license text finder. We enter the license name in the input text field, and we use DWR to show the license names that start with the typed text. A list of possible completions is shown below the input field. The following is a screenshot of the field completion in action:

DWR Java AJAX User Interface: Basic Elements (Part 2)

Selected license content is shown in an iframe element from http://www.opensource.org.

Server Code for Field Completion

We will re-use some of the classes we developed in the last section. AppContent is used to load the sample page, and the DBUtils class is used in the LicenseDB class. The LicenseDB class is shown here:

package samples;

import java.util.List;
import java.util.Vector;

public class LicenseDB{

private DBUtils dbUtils=new DBUtils();

public LicenseDB()
{
dbUtils.initFileDB("licenses.csv");
}

public List<String> getLicensesStartingWith(String startLetters)
{
List<String> list=new Vector<String>();
List<String> licenses=dbUtils.getCSVStrings(startLetters);
for(String license : licenses)
{
list.add(license.split(",")[0]);
}
return list;
}

public String getLicenseContentUrl(String licenseName)
{
List<String> licenses=dbUtils.getCSVStrings(licenseName);
if(licenses.size()>0)
{
return licenses.get(0).split(",")[1];
}
return "";
}
}

The getLicenseStartingWith() method goes through the license names and returns valid license names and their URLs. Similar to the data in the previous section, license data is in a CSV file named licenses.csv in the package directory. The following is an excerpt of the file content:

Academic Free License, http://opensource.org/licenses/afl-3.0.php
Adaptive Public License, http://opensource.org/licenses/apl1.0.php
Apache Software License, http://opensource.org/licenses/apachepl-1.1.php
Apache License, http://opensource.org/licenses/apache2.0.php
Apple Public Source License, http://opensource.org/licenses/apsl-2.0.php
Artistic license, http://opensource.org/licenses/artistic-license-1.0.php
...

There are quite a few open-source licenses. Some are more popular than others (like the Apache Software License) and some cannot be re-used (like the IBM Public License).

We want to remote the LicenseDB class, so we add the following to the dwr.xml file.

<create creator="new" javascript="LicenseDB">
<param name="class" value="samples.LicenseDB"/>
<include method="getLicensesStartingWith"/>
<include method="getLicenseContentUrl"/>
</create>

Client Code for Field Completion

The following script element will go in the index.jsp page.

<script type='text/javascript' src='/DWREasyAjax/dwr/interface/LicenseDB.js'></script>

The HTML for the field completion is as follows:

<h3>Field completion</h3>
<p>Enter Open Source license name to see it's contents.</p>
<input type="text" id="licenseNameEditBox" value="" onkeyup="showPopupMenu()" size="40"/>
<input type="button" id="showLicenseTextButton" value="Show license text"
onclick="showLicenseText()"/>
<div id="completionMenuPopup"></div>
<div id="licenseContent"></div>

The input element, where we enter the license name, listens to the onkeyup event which calls the showPopupMenu() JavaScript function. Clicking the Input button calls the showLicenseText() function (the JavaScript functions are explained shortly). Finally, the two div elements are place holders for the pop-up menu and the iframe element that shows license content.

For the pop-up box functionality, we use existing code and modify it for our purpose (many thanks to http://www.jtricks.com). The following is the popup.js file, which is located under the WebContent | js directory.

//<script type="text/javascript"><!--
/* Original script by: www.jtricks.com
* Version: 20070301
* Latest version:
* www.jtricks.com/javascript/window/box.html
*
* Modified by Sami Salkosuo.
*/
// Moves the box object to be directly beneath an object.
function move_box(an, box)
{
var cleft = 0;
var ctop = 0;
var obj = an;

while (obj.offsetParent)
{
cleft += obj.offsetLeft;
ctop += obj.offsetTop;
obj = obj.offsetParent;
}

box.style.left = cleft + 'px';

ctop += an.offsetHeight + 8;

// Handle Internet Explorer body margins,
// which affect normal document, but not
// absolute-positioned stuff.
if (document.body.currentStyle &&
document.body.currentStyle['marginTop'])
{
ctop += parseInt(
document.body.currentStyle['marginTop']);
}

box.style.top = ctop + 'px';
}

var popupMenuInitialised=false;
// Shows a box if it wasn't shown yet or is hidden
// or hides it if it is currently shown
function show_box(html, width, height, borderStyle,id)
{
// Create box object through DOM
var boxdiv = document.getElementById(id);
boxdiv.style.display='block';
if(popupMenuInitialised==false)
{
//boxdiv = document.createElement('div');
boxdiv.setAttribute('id', id);
boxdiv.style.display = 'block';
boxdiv.style.position = 'absolute';
boxdiv.style.width = width + 'px';
boxdiv.style.height = height + 'px';
boxdiv.style.border = borderStyle;
boxdiv.style.textAlign = 'right';
boxdiv.style.padding = '4px';
boxdiv.style.background = '#FFFFFF';
boxdiv.style.zIndex='99';
popupMenuInitialised=true;
//document.body.appendChild(boxdiv);
}

var contentId=id+'Content';
var contents = document.getElementById(contentId);
if(contents==null)
{
contents = document.createElement('div');
contents.setAttribute('id', id+'Content');
contents.style.textAlign= 'left';
boxdiv.contents = contents;
boxdiv.appendChild(contents);

}
move_box(html, boxdiv);

contents.innerHTML= html;

return false;
}

function hide_box(id)
{
document.getElementById(id).style.display='none';
var boxdiv = document.getElementById(id+'Content');
if(boxdiv!=null)
{
boxdiv.parentNode.removeChild(boxdiv);

}
return false;
}


//--></script>

Functions in the popup.js file are used as menu options directly below the edit box.

The show_box() function takes the following arguments: HTML code for the pop-up, position of the pop-up window, and the “parent” element (to which the pop-up box is related). The function then creates a pop-up window using DOM. The move_box() function is used to move the pop-up window to its correct place under the edit box and the hide_box() function hides the pop-up window by removing the pop-up window from the DOM tree.

In order to use functions in popup.js, we need to add the following script-element to the index.jsp file:

<script type='text/javascript' src='js/popup.js'></script>

Our own JavaScript code for the field completion is in the index.jsp file. The following are the JavaScript functions, and an explanation follows the code:

function showPopupMenu()
{
var licenseNameEditBox=dwr.util.byId('licenseNameEditBox');
var startLetters=licenseNameEditBox.value;
LicenseDB.getLicensesStartingWith(startLetters, {
callback:function(licenses)
{
var html="";
if(licenses.length==0)
{
return;
}
if(licenses.length==1)
{
hidePopupMenu();
licenseNameEditBox.value=licenses[0];
}
else
{
for (index in licenses)
{
var licenseName=licenses[index];//.split(",")[0];
licenseName=licenseName.replace(/"/g,"&quot;");
html+="<div style="border:1px solid #777777;margin-bottom:5;"
onclick="completeEditBox('"+licenseName+"');">"+licenseName+"</div>";
}
show_box(html, 200, 270, '1px solid','completionMenuPopup');
}
}
});
}

function hidePopupMenu()
{
hide_box('completionMenuPopup');
}

function completeEditBox(licenseName)
{
var licenseNameEditBox=dwr.util.byId('licenseNameEditBox');
licenseNameEditBox.value=licenseName;
hidePopupMenu();
dwr.util.byId('showLicenseTextButton').focus();
}

function showLicenseText()
{
var licenseNameEditBox=dwr.util.byId('licenseNameEditBox');
licenseName=licenseNameEditBox.value;
LicenseDB.getLicenseContentUrl(licenseName,{
callback:function(licenseUrl)
{
var html='<iframe src="'+licenseUrl+'" width="100%" height="600"></iframe>';
var content=dwr.util.byId('licenseContent');
content.style.zIndex="1";
content.innerHTML=html;
}
});
}

The showPopupMenu() function is called each time a user enters a letter in the input box. The function gets the value of the input field and calls the LicenseDB. getLicensesStartingWith() method. The callback function is specified in the function parameters. The callback function gets all the licenses that match the parameter, and based on the length of the parameter (which is an array), it either shows a pop-up box with all the matching license names, or, if the array length is one, hides the pop-up box and inserts the full license name in the text field. In the pop up box, the license names are wrapped within the div element that has an onclick event listener that calls the completeEditBox() function.

The hidePopupMenu() function just closes the pop-up menu and the competeEditBox() function inserts the clicked license text in the input box and moves the focus to the button. The showLicenseText() function is called when we click the Show license text button. The function calls the LicenseDB. getLicenseContentUrl() method and the callback function creates an iframe element to show the license content directly from http://www.opensource.org, as shown in the following screenshot:

DWR Java AJAX User Interface: Basic Elements (Part 2)

Afterword

Field completion improves user experience in web pages and the sample code in this section showed one way of doing it using DWR.

It should be noted that the sample for field completion presented here is only for demonstration purposes.

LEAVE A REPLY

Please enter your comment!
Please enter your name here