| By Anil Sharma | Article Rating: |
|
| April 17, 2007 08:00 AM EDT | Reads: |
2,605 |
This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.
Row Selection in the SimpleTable
Note in the "setData" method of the SimpleTable, the following code.
- The code to add "cellClicked" as an "onclick" listener to each cell.
- The ID attribute of each attribute is set so that we can parse it to retrieve the row and column of the cell.
Listing 5.14 - SimpleTable - Row Selection
// cell clicked event handler
SimpleTable.prototype.cellClicked = function(event) {
var ev = event ? event : window.event;
var e = ev.srcElement ? ev.srcElement : ev.target;
// retrieve cell id
var cellId = e.id;
// retrieve row and column
var idx = cellId.indexOf("_");
var idx2 = cellId.lastIndexOf("_");
var row = parseInt(cellId.substring(idx+1, idx2));
var col = parseInt(cellId.substring(idx2+1));
// select the clicked row
this.selectRow(row);
// send message to its cell selected listener
for (var i=0; i<this.cellSelectedListeners.length; i++) {
this.cellSelectedListeners[i].cellSelected(row, col);
}
}
// highlights the given row
SimpleTable.prototype.selectRow = function(row) {
var tr;
// reset the background of currently selected row
if (this.currentRow != -1) {
tr = this.tbody.rows[this.currentRow];
for (var i=0; i<tr.cells.length; i++) {
tr.cells[i].style.backgroundColor = this.savedColor[i];
}
}
this.currentRow = row;
this.savedColor.length = 0;
// highlight the newly selected row
// save the current background color before highlighting
if (this.currentRow != -1) {
tr = this.tbody.rows[this.currentRow];
for (var i=0; i<tr.cells.length; i++) {
this.savedColor.push(tr.cells[i].style.backgroundColor);
tr.cells[i].style.backgroundColor = __selectionColor;
}
}
}
Row Selection Listener
It's a very commonly used function that when a cell in a table is clicked, a listener is notified of the cell's row and column number. We also use this function to tie a table to a form. The "addCellSelectedListener" method of the SimpleTable is used to add the listener. The code assumes that the given listener implements a "cellSelected" method.
Listing 5.15 - SimpleTable - Adding Selection Listener
// adds a cell selected listener
// assumes that the passed listener defines cell selected method
SimpleTable.prototype.addCellSelectedListener = function(l) {
if (l.cellSelected == null) {
alert("The given listener should implement cellSelected(r, c) method");
return;
}
this.cellSelectedListeners.push(l);
}
When the cell is selected, the "cellClicked" method of the SimpleTable invokes the "cellSelected" method of its listeners.
An example cell selected listener is shown below.
var someTable;
... ... ...
someTable.addCellSelectedListener(new CellSelectedListener());
... ... ...
// The cell selected method of the following listener will be called when
// user clicks on the cell of ‘someTable'.
function CellSelectedListerner() {
this.cellSelected = function(r, c) {
// do something
}
}
Example Components
In the sections above we discussed an implementation of the table. A form is another commonly used UI component. It consists of labels and fields. Before we get into the details of a form, let's take a look at commonly used fields and how they're modeled using the component class. We'll discuss these fields through the following classes.
- Component: The base class of all components
- TextComp: For a text field
- SelectComp: For the selection of a value using a dropdown list
- DateComp: For the date field
- TextArea: For the text area field
Component
Component is the base class of all other components. In the following section, we'll see how other classes inherit its behavior. A component fulfills the following specifications:
- A component has a name.
- It's associated with an HTML element used to display and edit its value.
- A component is bound to an object.
- It displays the value of the attribute of the object that has the same name as the name of the component.
- When the user changes the value, the component updates the value of the object's attribute.
- Before updating the value, the component might validate the value. If the validation fails, then the value of attribute isn't updated.
- A value-changed listener can be added to the component. After the value is successfully updated, the value-changed listener is triggered by passing the reference to its object, attribute name, and the new value of the attribute.
Listing 5.16 - Component Class
// base class of all other component types
function Component(name, displayName) {
// the associated element
this.element = null;
// name of the component
this.name = name;
// label of the component
this.displayName = displayName;
// the data, this compoennt is bound to this.data = null;
// list of value changed listener
// ‘valueChanged' method of each listener will
// be called after successful update.
this.valueChangedListeners = new Array();
}
// This method is used to associate an HTML element with
// the component
Component.prototype.setHtmlElement = function(e) {
this.element = e;
var self = this;
// add onchange listener to the HTML element
e.onchange = function() {self.changed();};
}
// This method is used to bound an object with
// the component.
Component.prototype.setData = function(data) {
// set the given data to its attribute
this.data = data;
if (this.element == null) {
return;
}
// retrieve attribute value
// assumes that the component name is
// same as the attribute name
var v = data[this.name];
if (v == null) {
v = ‘';
} else {
v = v.toString();
}
// set the value of element
this.element.value = v;
}
// This method is triggered by onchange of the HTML element.
Component.prototype.changed = function() {
var v = this.element.value;
// validate new value
if (!this.validate(v)) {
return;
}
// set the value of object's attribute
if (this.data != null) {
this.data[name] = v;
}
// inform value changed listeners
for (var i=0; i<this.valueChangedListeners.length; i++) {
this.valueChangedListeners[i].valueChanged(this.data, this.name, v);
}
}
// validates the value
// might be overridden by its subclass.
Component.prototype.validate = function(v) {
return true;
}
// adds a value changed listener
// assumes that the listener implements ‘valueChanged' listener
Component.prototype.addValueChangedListener = function(l) {
if (l.valueChanged == null) {
alert("The given listener should implement valueChanged(data, name, newValue)
method");
return;
}
this.valueChangedListeners.push(l);
}
Component.prototype.render = function() {
var elem;
elem = document.createElement(‘input');
elem.type = ‘text';
this.element = elem;
return elem;
}
Inheritance in JavaScript
Since attributes and methods are treated alike in JavaScript, assigning the properties of one object to another is the equivalent of the latter inheriting from the former. In the following code the "destination" object inherits from the "source" object:
Object.extend = function(destination, source) {
for (property in source) {
destination[property] = source[property];
}
return destination;
}
We make use of the inheritance in the following component implementation.
TextComp
TextComp inherits from component. It's used for a text-type value. Our implementation of TextComp doesn't have any specific behavior.
function TextComp(name, displayName) {
this.type = "TextComp";
Object.extend(this, new Component(name, displayName));
}
SelectComp
SelectComp inherits from the component. It's used for displaying and editing the pairs of code names, where "code" is the internal value and "name" is the display value. It's associated with a SELECT-type HTML element. The list of SELECT-element options can either be specified in the HTML template or populated programmatically.
function SelectComp(name, displayName) {
Object.extend(this, new Component(name, displayName));
this.type = "SelectComp";
// clears all option elements of its element
this.clearOptions = function() {
... ... ...
}
}
// programmatically adds an option
SelectComp.prototype.addOption = function(name, value) {
... ... ...
}
DateComp
Our example DateComp is really a rather simplified implementation. It's used to enter date in a mm/dd/yy format. It extends the component class and overrides the "validate" method to verify if the user-entered value is in the desired format.
function DateComp(name, displayName) {
this.type = "DateComp";
Object.extend(this, new Component(name, displayName));
}
// validates if the value is in mm/dd/yy format
DateComp.prototype.validate = function(v) {
... ... ...
return true;
}
TextAreaComp
TextAreaComp inherits from the component. It's associated with the TextArea HTML element and is used for a text-type value when the length of the string is long and shown in multiple lines. Our implementation of TextAreaComp doesn't have any specific behavior.
function TextAreaComp(name) {
Object.extend(this, new Component(name));
}
Project Detail Form
We've discussed a set of useful components. Here we'll see how to use them in a form. Say we have a requirement to display the selected project's summary as a form below the project list. The user would use this to change the properties of a project.
Refer to the MainStep-6.html for a discussion in this section.
We follow the same steps as in the case of a table. We ask our UI designer to design a project summary form. She gives us the following template as ProjectSummary.html.
Like the table example, we write ProjectSummary.js to load this template and display it below the project list. Listing 5.17 shows how this is done; the code is similar to the code for ProjectList.js.
Listing 5.17 - Displaying ProjectSummary.html Template
// Reference to Form component
var projectSummary = null;
// Reference to the block element containing Form
// This is attached in the main area
var projectSummaryContainer = null;
// Actual form element
var projectSummaryElement = null;
// attaches project summary form to MainArea2
// loads the template from the server
function displayProjectSummary() {
if (projectSummaryContainer == null) {
// project summary template is not yet loaded
// load the template
loadProjectSummaryElement();
} else {
// project summary template has been loaded
// simply attach it
// See Util.js for the details of attachElement function.
attachElement(‘MainArea2', projectSummaryContainer);
}
}
// loads HTML template for the project summary form
function loadProjectSummaryElement() {
var req = new DataRequest(basePageURL + ‘ProjectSummary.html',
requestCompletedProjectSummaryElement,
requestFailedProjectSummaryElement,
requestTimedoutProjectSummaryElement);
ajaxEngine.processRequest(req);
}
// call back after the project summary is loaded
// response.responseText gives the HTML retrieved
function requestCompletedProjectSummaryElement(response) {
projectSummaryContainer = setElementFromHtml(response.responseText,
‘MainArea2',
‘projectSummaryContainer');
// our projectSummaryContainer element contains the projectSummaryElement
projectSummaryElement = document.getElementById("projectSummaryElement");
// create a Form component
projectSummary = new Form();
// associate projectSummaryElement with it
projectSummary.setHtmlElement(projectSummaryElement);
// associates field components with the corresponding elements
projectSummary.setFieldComp(new TextComp("name"));
projectSummary.setFieldComp(new TextComp("resource"));
projectSummary.setFieldComp(new TextAreaComp("description"));
projectSummary.setFieldComp(new DateComp("startDate"));
projectSummary.setFieldComp(new DateComp("endDate"));
projectSummary.setFieldComp(new SelectComp("status"));
projectSummaryTemplateLoaded();
}
When we invoke "displayProjectSummary" from the "initialize" method of the MainPage-6.html, the resulting MainPage-6.html is shown below.
function initialize() {
loadTopNaviagation();
displayProjectList();
displayProjectSummary();
}
Form
We've seen how to load the ProjectSummaryTemplate.html template and display it in MainArea2 of the main page and how the element corresponding to the form is associated with the Form component. This section discusses the design of the Form class.
Listing 5.18 shows the implementation of the Form. The comments in the code explain its methods. The "setHtmlElement" method is used to associate the Form with the element. The "setFieldComp" method is used to associate a field element contained inside it with a given component. The "setData" method assumes that it's passed a JavaScript object where attributes of the object correspond to the fields of the component. The "setData" method in turns sets the value of the field components. A Form listens for the value-change event from all its fields. The Form, in turn, triggers its value-change listeners, when the value of a field changes.
Listing 5.18 - Form Class
function Form(name) {
// name of the component
this.name = name;
// the associated form element
this.element = null;
// collection of sub-elements contained inside
// the form element
this.collection = null;
// list of components (fields)
this.components = new Array();
// list of value change listeners
this.valueChangedListeners = new Array();
// current data set displayed by this form
this.data = null;
// original objects table
this.originalObjs = new Object();
// recorded changes by object
this.changesByObj = new Object();
}
// sets the form element
// builds a collection of sub-elements
Form.prototype.setHtmlElement = function(e) {
this.element = e;
this.collection = new ElementCollection(e);
}
// sets a field component
// retreieves the field element using the name of the
// component (component name is same as the field id)
Form.prototype.setFieldComp = function(comp) {
if (this.element != null) {
var e = this.collection.getElementById(comp.name);
if (e != null) {
comp.setHtmlElement(e);
}
}
// form listens for any change to its field
comp.addValueChangedListener(this);
// add the component to the components' list
this.components.push(comp);
}
// sets the dataset of the form
// iterates over the component list and
// sets data for each field
Form.prototype.setData = function(data) {
this.data = data;
this.copyOriginalData(data);
for (var i=0; i<this.components.length; i++) {
this.components[i].setData(data);
}
}
// invoked when the value of one of the field of the
// form changes
// when any field of the form changes, the form
// invokes valueChanged method of its listener
Form.prototype.valueChanged = function(data, name, value) {
this.recordChanges(data, name, value);
for (var i=0; i<this.valueChangedListeners.length; i++) {
this.valueChangedListeners[i].valueChanged(data, name, value);
}
}
// add a value change listener
// when any field of the form changes, the form
// invokes valueChanged method of its listener
Form.prototype.addValueChangedListener = function(l) {
if (l.valueChanged == null) {
alert("The given listener should implement" +
" valueChanged(data, name, newValue) method");
return;
}
this.valueChangedListeners.push(l);
}
Displaying Selected Rows in the Form
In the form above, we want to display the selected row of the table. Note that the names of the components are the same as the column IDs and the attributes of the object. This name/ID matching is key to data binding.
To display the selected row, we add the cell selection listener to the table after it's loaded. Refer to the MainPage-7.html for a discussion of this section.
function projectListTemplateLoaded() {
// load the project list data
loadProjectList();
// add a cell selection listener to the table
projectList.addCellSelectedListener(new CellSelectedListerner());
}
Inside the listener, we retrieve the object corresponding to the selected row and set it as the data of the form component. As seen above, the "setData" method of the form in turn sets the value of its components.
function CellSelectedListerner() {
// called when the user clicks on the cell of the table
// ‘r' is the row and ‘c' is the column of the cell
this.cellSelected = function(r, c) {
// retrieve object corresponding to the row
var rowData = projectList.getRowData(r);
if (rowData == null) {
rowData = new Object();
}
// set the object as the value of the form
projectSummary.setData(rowData);
}
}
Propagating Changes
In the above example a selected row is displayed inside the form. But in the reverse direction, when a field is edited, the changes aren't reflected in the table. In any UI, it's important that the updates are reflected consistently. This is handled through properly crafted event listeners.
Refer to the MainPage-8.html for a discussion in this section. We add a value-change listener to the form after it's loaded. The listener informs the table that the value has changed.
// add value change listener after the from's template is loaded.
function projectSummaryTemplateLoaded() {
projectSummary.addValueChangedListener(new ValueChangedListener());
}
// when the value of an attribute changes, inform the table
function ValueChangedListener() {
// this method is called when the user edits a field of the form
this.valueChanged = function(data, name, value) {
// inform the project list that the ‘value' of ‘name' attribute of
// the ‘data' object has changed
projectList.processValueChange(data, name, value);
}
}
The "processValueChange" method of the table is shown below. It updates the value of the cell of the row that corresponds to the updated object.
// this method is called when the value of an object's
// attribute changes.
// If the given, object is found in this table's dataset
// then the value of the cell corresponding to the
// changed attribute is updated.
this.processValueChange = function(data, name, value) {
if (this.dlist == null) {
return;
}
for (var i=0; i<this.dlist.length; i++) {
// check if the given object is present in its dataset
if (this.dlist[i] == data) {
// given object is present in my dataset
for (j=0; j<this.columns.length; j++) {
var col = this.columns[j];
// check if the col.id matches with the given name
if (col.id == name) {
// column matches the given attribute name
var tr = this.tbody.rows[i];
var td = tr.cells[j];
removeAll(td);
// update value of the cell.
td.appendChild(document.createTextNode(value));
break;
}
}
break;
}
}
}
Retrieving Change List When the requirement is to send updates to the server immediately, the value-changed handler can be used to send the changes to the server through an AJAX call. Another scenario is to record changes in the form until the UI retrieves them at the user's request and sends them to the server. When the user clicks on the "Save" button, the changes are sent to the server.
Since a form can be used to edit many objects before they're saved at the server, we record changes by object ID (we're assuming that the original data sent by the server has an "id" attribute for each object). The form makes a copy of the original object when it's set to the form the first time. Subsequently, the value-change listener compares the changed value with the original value and records the change. The code also takes care of the situation when the value is reset back to the original value. The change recorded for the reset field is removed. The following changes are made to the Form class to record the changes made by the user.
- A new "copyOriginalData" method is added to make a copy of the original object so that the new value can be compared against the original value.
- A new "recordChanges" method is added to record the changes. The changes are recorded in the table indexed by the "id" of the object (this is a per-object changes table). A form records the changes for multiple objects until they're cleared.
- A new "clearChanges" method is added to clear all the changes after they've been saved at the server.
- A new "getChangeList" method is added to get the list of changes as an XML string.
- The "setData" method is modified to call "copyOriginalData" to make a copy of the data to be edited by the form. The copy is made only if this is the first time the object is set in the form.
- The "valueChanged" method is modified to call "recordChanges" when the value of one of its field changes.
Listing 5.19 - Form's Methods for Recording Changes
// sets the dataset of the form
// makes a copy of the data
// iterates over the component list and
// sets data for each field
Form.prototype.setData = function(data) {
this.data = data;
this.copyOriginalData(data);
... ... ...
}
// invoked when the value of one of the field of the
// form changes
// it records the changes
// when any field of the form changes, the form
// invokes valueChanged method of its listener
Form.prototype.valueChanged = function(data, name, value) {
this.recordChanges(data, name, value);
... ... ...
}
// makes a copy of the given data
// works only if the given data has __id attribute
Form.prototype.copyOriginalData = function(data) {
// data has id and not added to the originalObjs yet
if (data.__id && this.originalObjs[data.__id] == null) {
var newData = new Object();
// copy attributes
for (property in data) {
newData[property] = data[property];
}
// insert into the orginal objects table
this.originalObjs[data.__id] = newData;
}
}
// records changes in the given object
Form.prototype.recordChanges = function(data, name, value) {
// retrieve original object
var origObj = this.originalObjs[data.__id];
if (origObj != null) {
// is value different
if (value != origObj[name]) {
// look for the object that records the change
var obj = this.changesByObj[data.__id];
if (obj == null){
// the first time the object is changed
// create an object for recording changes
obj = new Object();
// set its id and class to the object for which it is
// recording the changes
obj.__id = data.__id;
obj.__className = data.__className;
// insert into changedByObj table
this.changesByObj[obj.__id] = obj;
}
obj[name] = value;
} else {
// cleanup any change recorded for this attribute
var obj = this.changesByObj[data.__id];
// if changes were recorded for this attribute, remove them
if (obj != null && obj[name]){
// delete the changes
delete obj[name];
}
}
}
}
// clears the changes recorded
Form.prototype.clearChanges = function() {
// recreate the tables
this.originalObjs = new Object();
this.changesByObj = new Object();
if (this.data != null) {
// copy the current object
this.copyOriginalData(this.data);
}
}
// returns the changes list as XML
Form.prototype.getChangeList = function() {
var changes = "";
// for each changed object
for (id in this.changesByObj) {
var obj = this.changesByObj[id];
var s = "";
// for each changed attribute (except __id and _className)
for (property in obj) {
if (property != "__id" && property != "_className") {
s = s + "<" + property + ">" + obj[property] +
"</" + property + ">";
}
}
// if there were changes
if (s != "") {
var t = obj.__className;
if (t == null) {
t = "Change";
}
changes = "<" + t + " id=\"" + obj.__id + "\">" + s + "</" + t + ">";
}
// if changes are present, wrap them in changelist element
if (changes != "") {
return "<changelist>" + changes + "</changelist>";
}
return changes;
}
Controller Servlet in Detail
In the section "Message Format for the Server Invocation", we saw the format of the messages to be sent to our example server. The service specification is in an XML format and is sent as a parameter of the HTTP request. The rest of the message, for example, a change list, is passed as the body of the POST request. The controller servlet converts both documents into an XML document. The controller servlet uses the service specification to identify the service. It then simply delegates the message to the service for further processing.
For simplicity's sake, we assume that our services implement the following interface:
Listing 5.20 - Service Interface
public interface Service {
// processes the get request
// sdoc is the service-specification document
public String serviceGet(Document sdoc)
throws RemoteException, ServiceException;
// processes the post request
// sdoc is the service-specification document
// doc is the document sent in the body of the post request
public String servicePost(Document sdoc, Document doc)
throws RemoteException, ServiceException;
}
The controller servlet code, assuming the above service specification, is shown below.
Listing 5.21 - Controller Servlet
public class ControllerServlet extends HttpServlet {
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
// processes service-specification sent through GET method by the client
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Cache-Control", "no-store");
res.setHeader("Expires", "0");
res.setContentType("text/xml");
// Service spec is passed as a request parameter called "service_spec"
// Convert "service_spec" parameter to XML document
Document sdoc = getServiceSpecDoc(req);
// retrieve service name from the XML doc
String serviceName = getServiceName(sdoc, req);
// look up the service by its name
Service svc = getService(serviceName);
if (svc != null) {
try {
// delegate the request to the service
String s = svc.serviceGet(sdoc);
res.getWriter().write(s);
} catch (ServiceException exc) {
... ... ...
}
} else {
// error
// throw exception
}
}
// processes the service-specification sent in the POST request
// and the document sent in the body of the post request
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Cache-Control", "no-store");
res.setHeader("Expires", "0");
res.setContentType("text/xml");
// Read XML document from the request body
Document doc = null;
try {
doc = getDocument(req);
} catch (Exception exc) {
// error, throw exception
}
// Service spec is passed as a request parameter called "service_spec"
// Convert "service_spec" parameter to XML document
Document sdoc = getServiceSpecDoc(req);
// retrieve service name from the XML doc
String serviceName = getServiceName(sdoc, req);
// lookup the service by its name
Service svc = getXmlService(serviceName);
if (svc != null) {
try {
// delegate the processing to the service with
// the service-specification and the XML document sent in the body
String s = svc.servicePost(sdoc, doc);
if (s != null) {
res.getWriter().write(s);
}
} catch (ServiceException exc) {
// error
}
} else {
// error
}
}
}
This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.
Published April 17, 2007 Reads 2,605
Copyright © 2007 SYS-CON Media, Inc. — All Rights Reserved.
Syndicated stories and blog feeds, all rights reserved by the author.
More Stories By Anil Sharma
Anil Sharma is a founder of Vertex Logic and architect of its AjaxFace product. He is working with Vertex Logic's customers and helping them deploy WEB 2.0 applications built using AjaxFace. These applications are in the areas of online printing, community WEB sites and social networking. He is driving future products of Vertex Logic. Prior to Vertex Logic, he was CTO and founder of Softrock Systems and Component Plus. There he built a model driven application platform using J2EE. His primary interests include user interface infrastructures and model driven applications. He is one of the contributors of the book "Real World AJAX - Secrets of the Master".
- Kindle 2 vs Nook
- Cloud Computing on Gartner's Top 10 List and SYS-CON Events' 2010 Calendar
- Confessions of a Ulitzer Addict
- IBM Hardware Chief, Intel VC Exec Arrested in Insider Trading Scam
- Tactical Cloud Computing Panel at 1st Annual GovIT Expo
- Ulitzer.com Named Exclusive "New Media" Sponsor of Cloud Computing Conference & Expo
- Moving Your RIA Apps into the Cloud: Seven Challenges
- Adobe’s Aiming ColdFusion at Multiple Clouds
- Windows 7 – Microsoft’s First Step to the Cloud
- Ulitzer Provides a Powerful Social Journalism Platform
- Jill Tummler Singer, Deputy CIO of CIA, Keynotes at GovIT Expo
- Open Source Mobile Cloud Sync and Push Email
- Kindle 2 vs Nook
- The Difference Between Web Hosting and Cloud Computing
- Cloud Computing on Gartner's Top 10 List and SYS-CON Events' 2010 Calendar
- Ajax in RichFaces 3.3, JSF 2 and RichFaces 4
- Confessions of a Ulitzer Addict
- IBM Hardware Chief, Intel VC Exec Arrested in Insider Trading Scam
- My Thoughts on Ulitzer
- Tactical Cloud Computing Panel at 1st Annual GovIT Expo
- Ulitzer.com Named Exclusive "New Media" Sponsor of Cloud Computing Conference & Expo
- US Post Office Hops a Ride on NetSuite’s Cloud
- Moving Your RIA Apps into the Cloud: Seven Challenges
- Adobe’s Aiming ColdFusion at Multiple Clouds
- Building a Drag-and-Drop Shopping Cart with AJAX
- What Is AJAX?
- Google Maps! AJAX-Style Web Development Using ASP.NET
- Flashback to January 2006: Exclusive SYS-CON.TV Interviews on "OpenAjax Alliance" Announcement
- AJAXWorld Conference & Expo to Take Place October 2-4, 2006, at the Santa Clara Convention Center, California
- AJAX Sponsor Webcasts Are Now Available at AJAXWorld Website
- How and Why AJAX, Not Java, Became the Favored Technology for Rich Internet Applications
- "Real-World AJAX" One-Day Seminar Arrives in Silicon Valley
- AJAXWorld University Announces AJAX Developer Bootcamp
- AJAX Support In JadeLiquid WebRenderer v3.1
- Where Are RIA Technologies Headed in 2008?
- Struts Validations Framework Using AJAX




































