Build an Ajax application with the Dojo Toolkit

The Dojo toolkit is a JavaScript library that makes the process of building large JavaScript-based Rich Internet Applications (RIAs) much simpler. With a wide range of features—from DOM querying and manipulation, Asynchronous JavaScript and XML (Ajax) request handling, excellent object-orientation support, and a full user interface widget library (Dijit)—Dojo is an excellent library to use to build a dynamic and interactive web application. In this tutorial, learn about many of the concepts of Dojo and the Dijit widget library through the development of a fully featured sample application, a contact manager system. This application lets a user browse, create, edit, and remove contacts (and contact groups) from a MySQL database. PHP is used on the server side to communicate with the database, with Dojo and the Dijit component library providing a rich, Ajax-powered user interface. The final result is a powerful web application that you can use as a foundation for your own RIAs.

Joe Lennon, Product Manager, Core International

Photo of Joe LennonJoe Lennon is a 25-year-old mobile and web application developer from Cork, Ireland. Joe works for Core International, where he leads the development of Core's mobile HR self service solutions. Joe is also a keen technical writer, having written many articles and tutorials for IBM developerWorks on topics such as DB2 pureXML, Flex, JavaScript, Adobe AIR, .NET, PHP, Python and much more. Joe's first book, Beginning CouchDB was published in late 2009 by Apress. In his spare time, Joe enjoys travelling, reading and video games.



01 March 2011

Also available in Chinese Japanese

Before you start

Develop skills on this topic

This content is part of a progressive knowledge path for advancing your skills. See Get started with Dojo development

This tutorial is for web application developers who are interested in utilizing the Dojo Toolkit to create visually impressive RIAs with relative ease. The tutorial guides you through the process of developing a full web application using Dojo, and although you will learn a lot about Dojo through it, it is recommended that you familiarize yourself with the basics of Dojo before proceeding. You should also be familiar with HTML and CSS, and you should be comfortable with JavaScript development. You don't need to be an expert, but the more knowledge you have of these topics, the easier the tutorial will be to follow. For a detailed introduction to Dojo, please see Resources for links to some introductory articles.

About this tutorial

The Dojo toolkit is a powerful JavaScript framework that provides all of the building blocks required to build impressive desktop-style RIAs. At its base, the framework includes various useful features that make the process of writing JavaScript applications easier, including DOM querying and manipulation, effects and animations, Ajax event handling, and more. Where Dojo stands apart from many other libraries, however, is in its one-of-a-kind widget system, Dijit, which includes a powerful parser that lets you use Dijit components as if they were regular HTML elements. Finally, Dojo also includes an extensive suite of extensions in DojoX that provide additional features, including support for a wide range of data stores, data grids, additional Dijit components, and much more.

In this tutorial, you will implement each of these three parts of the toolkit in a sample application that resembles a desktop contact manager application. This application will allow you to manage your contacts by adding new ones as well as editing or deleting existing ones. You can arrange your contacts into groups, moving them between groups as required. The groups themselves can also be added, modified, and deleted. The idea is that this tutorial will show you how to create a fully functional, dynamic, database-driven application with relative simplicity and much less code than you might expect. The application you create in this tutorial can be easily extended or modified to create projects of your own.

Prerequisites

To follow this tutorial, you will need the following:

  • A web server that supports PHP, such as Apache
  • PHP, version 5 or later
  • MySQL, version 4 or later (earlier versions may work)
  • Dojo Toolkit 1.5 or later (can be loaded using the Google Content Delivery Network. Alternatively, you can download the toolkit locally and run it from your development machine.)

The PHP scripts and MySQL tables used in this application are extremely straightforward, so they should be relatively easy to convert to other server-side languages such as Java™ code, C#, Python, and so on, and databases such as DB2®, Oracle, and SQL Server. The majority of the work in this application is done by Dojo, with PHP and MySQL simply used for persistence.

See Resources for links to these tools.


Setting up

This tutorial guides you through the development of a feature-rich Dojo web application. This section explains the various concepts of the Dojo Toolkit that are covered in this tutorial as well as giving a tour of the sample application this tutorial will teach you to develop.

Dojo concepts used in this tutorial

As you work through this tutorial, you will be putting into practice various features of the Dojo toolkit. The following sections outline the features from the different parts of the toolkit (Base, Dijit, and Dojox).

Base

At its core, the Dojo Toolkit provides a base set of features common to many JavaScript libraries. These features typically include DOM querying and manipulation functions, features that enable developers to make cross-browser-friendly Ajax requests, event handling, data APIs, and more. In this tutorial, you will use many of the features of Dojo Base, including:

  • DOM querying
  • DOM manipulation
  • XmlHttpRequests (XHR/Ajax)
  • Event handling (dojo.connect)
  • Read/Write data stores

Dijit

One of the most distinctive aspects of the Dojo Toolkit is the Dijit component library, which lets you create powerful and visually stunning user interfaces with minimal effort and fuss. What's more, Dijit lets you use JavaScript components in a declarative fashion, thanks to Dojo's powerful widget parsing capabilities. This tutorial implements a wide variety of Dijit components, including:

  • Layout components (BorderContainers and ContentPanes)
  • Menus (Application and Context)
  • Tree
  • Dialogs
  • Forms, ValidationTextBoxes, TextBoxes, FilteringSelects, and Buttons

DojoX

Although much of the functionality required for most web applications is included in the other parts of the Dojo Toolkit, sometimes you need additional features that aren't available out of the box. With other libraries, you'd often need to look for third-party plug-ins to gain access to such features. Dojo's community-driven DojoX extension library means that you can access many stable and experimental additional components without needing to look elsewhere. In this tutorial, I use the following DojoX component: DojoX.

The sample application—a basic contact management application

The focus of this tutorial is on the creation of a full-featured sample application that includes various CRUD (Create, Update, Delete) operations to resemble a potential real-world application. The end result is a web-based contact management system that has desktop-like performance. Figure 1 shows how the final product will look in your web browser.

Figure 1. Final application main application
Three columns with lists of first names, last names, and email addresses. A box overlays it with edit, move or delete contact.

At the top of the application you have an application menu, with File and Edit options. These include sub-menus for the various operations you can perform, including adding new groups and contacts, and editing and deleting existing items.

On the left side of the application is a tree that displays the different groups available. Creating a new group results in the group being added to this tree. A context menu is available on the tree, so when you right-click an item in the tree you get options to rename or delete that group. Selecting (left-clicking) a group loads the contacts from that group on the right side. If you select the root node (Groups, which cannot be deleted), every contact, regardless of their group, is displayed on the right.

The right side of the application is split into two views, with the top section displaying a grid view of the contacts in the currently selected group, and the bottom half showing the details of the currently selected contact. Again, the grid features a context menu, which lets you edit, move, or delete a contact by right-clicking the contact and choosing the appropriate option.

The application makes extensive use of pop-up windows to provide additional functions, particularly in terms of creating, editing, and deleting items. An example of this is shown in Figure 2.

Figure 2. Dialog pop-up example: Editing contacts
A window with various fields that let you edit the contact's information.

The pop-up windows in the application fade out the main application in the background and display the pop-up window in focus over the application window. These windows typically take the shape of a form with components, including text boxes (with built-in "required" validation) and auto-complete drop-down lists. When deleting items, a confirmation window is shown that asks the user to confirm the deletion of the item. Pressing OK causes the item to be deleted; otherwise, the process is canceled.

I'm sure you'll agree that this application looks quite like a typical desktop application. Other features are also included, such as the ability to resize the split between the left and right side of the application, and the grid and preview panes on the right side. When you resize your browser window, the application resizes to take up the entire window. You should be able to take this sample application and build on its structure to create impressive looking results.


Server-side setup: Database and PHP scripts

In this section, you will set up a new MySQL database to store the data for the contact manager application. You will also set up a PHP script that will manage the connection to this database, as well as a series of PHP APIs that will take the data from MySQL and translate it into a JavaScript Object Notation (JSON) format that can be easily read by Dojo's XHR (Ajax) function callbacks.

Getting started: MySQL database and server-side configuration

Before getting started with Dojo, you need to set up the server-side part of the application. The first thing you need to do is create the MySQL user, database, tables, and data that will form the data layer for the application. If you have root access to MySQL (or at least the ability to create users and databases), you can simply run the install.sql script included with the source code of this tutorial. Assuming the MySQL bin directory is on your system path and you are in the source code folder, you can run this script as follows:

$ mysql -u root -pMyPassword < install.sql

.

Obviously, you should replace "MyPassword" with your actual MySQL root password. This creates a new database user, database, tables, and some sample data. The contents of install.sql are shown in Listing 1.

Listing 1. install.sql script for setting up MySQL database
create user 'dojo'@'localhost' identified by 'somepass';
create database dojocontacts;
grant all privileges on dojocontacts.* to 'dojo'@'localhost' with grant option;
use dojocontacts;

create table groups(
    id             int(11) auto_increment primary key,
    name             varchar(100) not null
) engine=INNODB;

create table contacts(
    id             int(11) auto_increment primary key,
    group_id        int(11) not null,
    first_name        varchar(100) not null,
    last_name        varchar(100) not null,
    email_address    varchar(255) not null,
    home_phone        varchar(100),
    work_phone        varchar(100),
    mobile_phone    varchar(100),
    twitter        varchar(255),
    facebook        varchar(255),
    linkedin        varchar(255),
    foreign key(group_id) references groups(id) on delete cascade
) engine=INNODB;

insert into groups(name) values('Family');
insert into groups(name) values('Friends');
insert into groups(name) values('Colleagues');
insert into groups(name) values('Others');

insert into contacts(group_id, first_name, last_name, email_address, home_phone, 
work_phone, twitter, facebook, linkedin)
values(3, 'Joe', 'Lennon', 'joe@joelennon.ie', '(555) 123-4567', '(555) 456-1237', 
'joelennon', 'joelennon', 'joelennon');

insert into contacts(group_id, first_name, last_name, email_address, home_phone, 
work_phone, twitter, facebook, linkedin)
values(1, 'Mary', 'Murphy', 'mary@example.com', '(555) 234-5678', '(555) 567-2348', 
'mmurphy', 'marym', 'mary.murphy');

insert into contacts(group_id, first_name, last_name, email_address, home_phone, 
work_phone, twitter, facebook, linkedin)
values(2, 'Tom', 'Smith', 'tsmith@example.com', '(555) 345-6789', '(555) 678-3459', 
'tom.smith', 'tsmith', 'smithtom');

insert into contacts(group_id, first_name, last_name, email_address, home_phone, 
work_phone, twitter, facebook, linkedin)
values(4, 'John', 'Cameron', 'jc@example.com', '(555) 456-7890', '(555) 789-4560', 
'jcameron', 'john.cameron', 'johnc');

insert into contacts(group_id, first_name, last_name, email_address, home_phone, 
work_phone, twitter, facebook, linkedin)
values(4, 'Ann', 'Dunne', 'anndunne@example.com', '(555) 567-8901', '(555) 890-5671',
'ann.dunne', 'adunne', 'dunneann');

insert into contacts(group_id, first_name, last_name, email_address, home_phone, 
work_phone, twitter, facebook, linkedin)
values(1, 'James', 'Murphy', 'james@example.com', '(555) 678-9012', '(555) 901-6782', 
'jmurphy', 'jamesmurphy', 'james.murphy');

Connecting to the database with PHP

With your MySQL database created, you can now create the PHP scripts that will insert, update, and delete data in these database tables.

In this sample application, the PHP scripts will merely be called using Ajax or XHR requests, and will always return a response to the request object in JSON format, with the exception of contact.php, which actually returns an HTML snippet that is inserted into the DOM. All of the PHP scripts are located in a subdirectory named data. Following are the scripts that are required for this project:

  • contact.php (Retrieves the contact detail for a given contact)
  • contacts.php (Retrieves a JSON representation of the contacts in the database)
  • database.php (Takes care of connecting to MySQL)
  • delete_contact.php (Deletes a contact from the database)
  • delete_group.php (Deletes a group from the database)
  • edit_contact.php (Adds/Edits a contact in the database)
  • edit_group.php (Renames a group in the database)
  • groups.php (Retrieves a JSON representation of the groups in the database)
  • move_contact.php (Moves a contact to a different group)
  • new_group.php (Adds a new group to the database)

Due to space restrictions, I can't list the contents of all of these files in this tutorial, so for full source code listings, see the accompanying source code for this tutorial. To give you an idea of how these PHP scripts work, however, I will discuss a few of them.

The database.php file is used by all of the other PHP scripts in this tutorial to open a connection to the MySQL database. This file is very straightforward; its contents are shown in Listing 2.

Listing 2. data/database.php file contents
<?php
$db_host = "localhost";
$db_user = "dojo";
$db_pass = "somepass";
$db_name = "dojocontacts";

//Connect to MySQL
$conn = mysql_connect($db_host, $db_user, $db_pass);
//Select database
mysql_select_db($db_name, $conn);
?>

Creating JSON-formatted API functions in PHP

There are two main types of operations performed in this application: retrieving data and manipulating data. Let's take a look at an example of each of these. First up, groups.php takes care of retrieving a list of groups from the database and represents it using the Dijit Identity API structure so it can be read by the Dijit Tree widget. Listing 3 shows the contents of this file.

Listing 3. data/groups.php file contents
<?php
include_once("database.php");
//SQL statement to get all groups
$sql = "SELECT id, name, 'node' as type FROM groups ORDER BY id";
$result = mysql_query($sql, $conn) or die("Could not load groups");

//Always show "Groups" as root element
$data = array(
    'identifier' => 'id',
    'label' => 'name',
    'items' => array(
        array(
            'id' => 0,
            'name' => 'Groups',
            'type' => 'root'
        )
    )
);

//Retrieve groups and add to array
if(mysql_num_rows($result) > 0) {
    while($row = mysql_fetch_assoc($result)) {
        $data['items'][0]['groups'][] = array('_reference' => $row['id']);
        $data['items'][] = $row;
    }
}

//Output $data array as JSON
header('Content-Type: application/json; charset=utf8');
echo json_encode($data);
?>

The Dijit Identity API requires that the JSON data structure take a particular format, with the properties identifier, label, and items at the top level. In Listing 3, I have added a root Groups item by default. This does not represent a particular group, but will always be displayed at the top of the tree (even if there are no groups). I have then looped through the result of the SQL SELECT statement and added each of these items as a child element.

First, each item is added to the items array, and the root item has a groups array added to it, with a collection of objects with a _reference property that refers to the child item's ID. Finally, the script takes the $data array that it builds up and outputs it as JSON. Later in this tutorial, you will create a data store to connect to this PHP script, which will take the JSON output and use it accordingly in a Dijit Tree.

The second example I discuss here is move_contact.php, which updates a contact record, setting the group_id value as required. Listing 4 shows the code for this file.

Listing 4. data/move_contact.php file contents

Click to see code listing

Listing 4. data/move_contact.php file contents

<?php
include_once("database.php");
//Get form values
$contact_id = $_POST['move_contact_id'];
$group_id = $_POST['move_contact_new'];
//Perform update
$sql = "UPDATE contacts SET group_id = ".mysql_real_escape_string($group_id, $conn)." WHERE id = ".mysql_real_escape_string($contact_id, $conn);
$result = mysql_query($sql) or die("Could not move contact in database");
//Check if performed via Ajax
if(mysql_affected_rows() > 0 && $_POST['move_contact_ajax'] == "0") {
    header("Location: ../index.html");
} else {
    //If Ajax, return JSON response
    header('Content-Type: application/json; charset=utf8');
    $data = array();
    //If rows affected, change was successful
    if(mysql_affected_rows() > 0) {
        $data['success'] = true;
        $data['id'] = $contact_id;
    } else {
        $data['success'] = false;
        $data['error'] = 'Error: could not move contact in database';
    }
    //Output array in JSON format
    echo json_encode($data);
}
?>

In Listing 4, two POST parameters are required, the contact ID and the new group ID that the contact should be moved to. These are then plugged into an SQL UPDATE statement. When the request is made through Ajax, a parameter is used to indicate that it was sent using Ajax and not using a regular HTML POST command. If it was the latter, the page is refreshed (to allow for a standard HTML fallback that you can implement if you want). If it was indeed performed using Ajax, a response structure is constructed and returned in a JSON format.

Most of the PHP server-side APIs work in a similar manner to the above code listings. As I said previously, for full code listings of all the scripts, see this tutorial's accompanying source code.

With the server-side part of the application configured, you are now ready to use Dojo to create a snazzy front end to the application. The next section explains just how simple it is to declaratively add Dijit components to your HTML document.


Building the user interface with Dijit

Dojo's rich component library, Dijit, lets you build full user interfaces with minimal coding and minimal fuss. Dijit components can be used in two ways: either programmatically using JavaScript or declaratively using HTML-style elements with a dojoType attribute. In this tutorial, I create all of the Dijit widgets used declaratively. Any JavaScript logic behind these widgets will be done in a separate JavaScript source file, however.

The main application code is contained in three files. The layout and widget declarations are done in the main index.html file. A subdirectory named css contains a single file, style.css, which contains all the stylesheets required for this particular application. Finally, all JavaScript logic is stored in the script.js file in the js subdirectory.

For the code for the style.css file, see the source code that accompanies this tutorial. This code is all basic CSS syntax and should be relatively self-explanatory.

In this section, I explain how to build the user interface for the application using HTML and Dojo widgets.

The basic structure

Start the application by creating the basic HTML structure. This includes any relative stylesheets, setting the Dijit theme to be used, and loading the JavaScript source files required. In this application, I am loading Dojo from Google's CDN for convenience. Of course, this means that you can only use the application if you have an Internet connection available. If you want to use the application offline, you can simply download the Dojo library and load it locally.

Listing 5 shows the starting point for the application's main index.html file.

Listing 5. Basic index.html structure
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Dojo Contacts</title>
        <link rel="stylesheet"
href="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css">
        <link rel="stylesheet"
href="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox/grid/resources/Grid.css">
        <link rel="stylesheet"
href="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox/grid/resources
/claroGrid.css">
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body class="claro">
        <!-- Content goes here -->
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs
/dojo/1.5/dojo/dojo.xd.js" djConfig="parseOnLoad: true"></script>
        <script type="text/javascript" src="js/script.js"></script>
    </body>
</html>

As you can see, the application loads four stylesheets. Three of these are from Dojo itself: the first one is the main theme CSS file for the Claro Dijit theme, and the other two are used for the Data Grid DojoX extension. An important point to note is that I have added a class claro to the main <body> element of this document. This tells the Dojo parser that it should use the Claro theme. Without this, the application will not look right, so don't leave it out. I have loaded the <script> blocks for the application at the bottom of the file, just before the closing </body> element. This is the generally recommended way of including JavaScript source files, as it will not prevent the rest of the page from loading before the scripts have finished loading. I have loaded the main Dojo script from Google's CDN and the application's script file, which you will create later in this tutorial.

Defining the application layout

Next, create the main application layout. The goal for now is to achieve the appearance shown in Figure 3. Basically, by the end of this section, you want to be able to load the file in your web browser and see something like this screenshot.

Figure 3. Basic application layout
A window with one vertical column to the left and 2 horizontals columns to the right.

To use the Dijit components used in the application, you need to tell Dojo to load these widgets using the dojo.require JavaScript function. Create a file named script.js and save it in a subdirectory named js in your application folder. Add the contents of Listing 6 to this file.

Listing 6. Loading the Dijit layout components
dojo.require("dijit.dijit");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dijit.MenuBar");
dojo.require("dijit.PopupMenuBarItem");
dojo.require("dijit.Menu");
dojo.require("dijit.MenuItem");

This tells Dojo that it should load the BorderContainer and ContentPane layout widgets, as well as various Menu widgets that will be used in the application's menu bar. With these items loaded, you can now add them to your application's index.html file. Replace the comment <!-- Content goes here --> in the file with the contents of Listing 7.

Listing 7. Adding the layout components to the application
<div dojoType="dijit.layout.BorderContainer" design="header" gutters="false" 
liveSplitters="true" id="borderContainer">
    <div dojoType="dijit.layout.ContentPane" region="top" id="topBar">
        <h1>Dojo Contacts</h1>
        <div dojoType="dijit.MenuBar" id="navMenu">
            <div dojoType="dijit.PopupMenuBarItem">
                <span>File</span>
                <div dojoType="dijit.Menu" id="fileMenu">
                    <div dojoType="dijit.MenuItem" 
jsId="mnuNewContact">New Contact</div>
                    <div dojoType="dijit.MenuItem" 
jsId="mnuNewGroup">New Group</div>
                </div>
            </div>
            <div dojoType="dijit.PopupMenuBarItem">
                <span>Edit</span>
                <div dojoType="dijit.Menu" id="editMenu">
                    <div dojoType="dijit.MenuItem" jsId="mnuEditContact" 
disabled="true">Edit Contact</div>
                    <div dojoType="dijit.MenuItem" jsId="mnuMoveContact" 
disabled="true">Move Contact</div>
                    <div dojoType="dijit.MenuItem" jsId="mnuRenameGroup"
disabled="true">Rename Group</div>

                    <div dojoType="dijit.MenuSeparator"></div>
                    <div dojoType="dijit.MenuItem" jsId="mnuDeleteContact" 
disabled="true">Delete Contact</div>
                    <div dojoType="dijit.MenuItem" jsId="mnuDeleteGroup" 
disabled="true">Delete Group</div>
                </div>
            </div>
        </div>
    </div>
    <div dojoType="dijit.layout.BorderContainer" region="center" gutters="true" 
liveSplitters="true" id="mainSection">
        <div dojoType="dijit.layout.ContentPane" splitter="true" region="left" 
id="treeSection">
            Tree to go here
        </div>
        <div dojoType="dijit.layout.BorderContainer" design="header" gutters="true" 
liveSplitters="true" id="mainContainer" region="center">
            <div dojoType="dijit.layout.ContentPane" region="top" splitter="true" 
id="gridSection">
                Grid to go here
            </div>
            <div dojoType="dijit.layout.ContentPane" id="contactView" 
jsId="contactView" region="center">
                <em>Select a contact to view above.</em>
            </div>
        </div>
    </div>
</div>

As shown in Listing 7, you add Dijit components to your page using standard HTML elements with a special dojoType attribute. These widget declarations also use some distinctive attributes that are specific to that widget type, for example the region attribute is used to define where a child element of a BorderContainer should display in the context of the container. There are quite a few components used in Listing 7, so let's take a look at the basic structure for the components:

  • dijit.layout.BorderContainer
    • dijit.layout.ContentPane (top)
  • h1
  • dijit.MenuBar
  • dijit.PopupMenuBarItem
    • dijit.Menu
  • dijit.MenuItem
  • dijit.MenuItem
  • dijit.PopupMenuBarItem
    • dijit.Menu
  • dijit.MenuItem
  • dijit.MenuItem
  • dijit.MenuItem
    • dijit.layout.BorderContainer (center)
  • dijit.layout.ContentPane (left)
  • dijit.layout.BorderContainer (center)
  • dijit.layout.ContentPane (top)
  • dijit.layout.ContentPane (center)

At the top level there is a BorderContainer, which lets you place child components in one of five regions: top, center, left, right, and bottom. Below this are two components, a ContentPane at the top and a BorderContainer at the center. Because there are only two components used, the center component will take up all space not used by the top component. In the top section, there is a standard HTML h1 heading and a Dijit Menu structure. In the center section, there is a nested BorderContainer component, with a ContentPane on the left, and another BorderContainer in the center (which takes up any space not used by the left component). The left ContentPane is where you will put your Tree control. The BorderContainer in the center has two child components — both ContentPanes— in the top and center regions.

At this stage, you should be able to save your file and load it in your web browser to get a result like the one you earlier saw in Figure 3.

Adding the Tree component

Next, add a Tree component to the left side of the application. Dijit Trees are complex components, and require a couple of support components to ensure that they work appropriately. First, they require a data store that adheres to Dojo's Identity API, connected to some JSON data source. In the sample application, this data is generated by the PHP script data/groups.php, so you can connect the tree's data store to this API. Next, the Tree needs a model component to define the structure of the data store and what data it should read. You then add your Tree component and connect it to this model.

To get started, you first need to tell Dojo the components it should load so that they are available to the application. Add the two lines of code shown in Listing 8 to the script.js file.

Listing 8. Loading tree-related components
dojo.require("dojo.data.ItemFileWriteStore");
dojo.require("dijit.Tree");

You might be wondering why a writable store is being used. Dojo's Tree component is quite complex to work with and actually cannot be directly refreshed asynchronously (unless you put it in a ContentPane and refresh the entire ContentPane), so to add/update/delete items from the tree dynamically it needs to connect to a writable store.

Next, find the "Tree to go here" section of your index.html file and replace it with the code in Listing 9. This defines the data store, the tree model, and the tree itself and puts it in the left side of the application. It also adds a context menu to the tree, although the items will be disabled right now. You will implement the context menu later.

Listing 9. Adding the tree to the application
<div dojoType="dojo.data.ItemFileWriteStore" jsId="groupsStore" 
url="data/groups.php"></div>
<div dojoType="dijit.tree.TreeStoreModel" jsId="groupsModel" 
childrenAttrs="groups" store="groupsStore" query="{id:0}"></div>
<div dojoType="dijit.Tree" id="groupsTree" jsId="groupsTree" model="groupsModel">
    <div dojoType="dijit.Menu" targetNodeIds="groupsTree">
        <div dojoType="dijit.MenuItem" jsId="ctxMnuRenameGroup" 
disabled="true">Rename Group</div>
        <div dojoType="dijit.MenuItem" jsId="ctxMnuDeleteGroup" 
disabled="true">Delete Group</div>
    </div>
</div>

In Listing 9, you'll notice the attribute jsId is used in each component. This attribute tells the Dojo parser to create JavaScript variables for each of these components and names it as the value given here. This makes it much easier to work with these components later, as it removes the need to use dijit.byId to query the DOM in an effort to find these widgets. It should also give a performance reward, as DOM querying can be expensive in terms of application speed and responsiveness, particularly on slower web browsers.

If you reload your browser, the application should now look like the window in Figure 4 (assuming you have the PHP scripts set up correctly and the data for the tree actually loads).

Figure 4. Dijit Tree in action
Dojo contacts window with a group listing in the left pane, and the right side split horizontally into 2 panes.

Adding the Grid component

Adding the grid to the application should work similarly to the tree component. Unlike the tree, however, the grid can be easily reloaded asynchronously by refreshing the data store, so no model is required, and a read-only store can safely be used. First, add the following line to the script.js file so the grid component is available for use in the application:

dojo.require("dojox.grid.DataGrid");

.

Note that the Data Grid widget is included in DojoX and not the Dijit library. DojoX components often require additional stylesheets to be loaded (in this case, these styles have already been included in the basic template I created earlier).

Now you can add the grid to the main application file. Data grids in Dojo can be applied to a standard <table> element, allowing you to easily define the headings you want to use. The grid can define a series of attributes to set how the grid should work; for example, whether client-side column sorting should be enabled. Find the "Grid to go here" reference in the index.html file and replace it with the code in Listing 10.

Listing 10. Adding the grid to the application
<span dojoType="dojo.data.ItemFileReadStore" jsId="contactsStore" 
url="data/contacts.php"></span>
<table dojoType="dojox.grid.DataGrid" id="contactsGrid" jsId="contactsGrid" 
columnReordering="true" sortFields="['last_name','first_name']" store="contactsStore" 
query="{first_name: '*'}"
clientSort="true" selectionMode="single" rowHeight="25" noDataMessage="<span 
class='dojoxGridNoData'>No contacts found in this group</span>">
    <thead>
        <tr>
            <th field="last_name" width="200px">Last Name</th>
            <th field="first_name" width="200px">First Name</th>
            <th field="email_address" width="100%">E-mail Address</th>
        </tr>
    </thead>

    <script type="dojo/method" event="onRowContextMenu" args="e">
    </script>
</table>
<div dojoType="dijit.Menu" id="gridMenu" targetNodeIds="contactsGrid">
    <div dojoType="dijit.MenuItem" jsId="ctxMnuEditContact" 
disabled="true">Edit Contact</div>
    <div dojoType="dijit.MenuItem" jsId="ctxMnuMoveContact" 
disabled="true">Move Contact</div>
    <div dojoType="dijit.MenuItem" jsId="ctxMnuDeleteContact"
disabled="true">Delete Contact</div>
</div>

Again, like the tree previously, the grid also has a context menu available. You will see how to enable these menus later in this tutorial. If you're wondering why there is an inline Dojo method <script> block, this is due to a bug in the DataGrid component where a context menu will not display without this empty block being included. It is worth pointing out that you could actually put JavaScript code directly in your layout code using these blocks if you like, but, personally, I prefer to separate my application logic from the UI elements where possible.

The grid loads the data using a read-only data store, which connects to the API data/contacts.php. It automatically sorts the data on the client side by last name and then first name. The grid only allows one row to be selected at a time, and defines an error message that is displayed if no data is found.

At this stage, your application should look like Figure 5 (again assuming you have added the PHP scripts from the source code bundle and configured the server side correctly):

Figure 5. DojoX Data Grid in action
Dojo contacts window with a group listing in the left pane, and the right side split horizontally into 2 panes. The top right pane contains columns with last name, first name, then email addresses.

Hidden application components: dijit.Dialog windows

At this point, the main application UI is complete. In this section, you will add some hidden dialog windows that will display in a modal way over the main application window when you select a menu option. These pop-up windows typically contain web forms that let you define the various properties of a group or contact, but you will also create a simple OK message window that looks somewhat nicer than the standard JavaScript alert dialog box.

There are five of these dialogs in total. These dialog windows contain various Dijit form fields, including ValidationTextBoxes, FilteringSelects, and Buttons. To include all of these additional widgets, you must load them using dojo.require, so add the code from Listing 11 to your script.js file.

Listing 11. Loading dialog and form components
dojo.require("dijit.Dialog");
dojo.require("dijit.form.Form");
dojo.require("dijit.form.ValidationTextBox");
dojo.require("dijit.form.TextBox");
dojo.require("dijit.form.FilteringSelect");
dojo.require("dijit.form.Button");

You can now start adding the dialog boxes. These components should be added outside of the main application BorderContainer to ensure that they appear over all other elements on the page. Start adding them just above the first <script> element near the bottom of the index.html file.

First, add the "New Group" and "Rename Group" dialog boxes. The code for these windows is shown in Listing 12.

Listing 12. New Group and Rename Group dialog box code
<div id="newGroupDialog" jsId="newGroupDialog" dojoType="dijit.Dialog" title="Create 
New Group" draggable="false">
    <div dojoType="dijit.form.Form" id="newGroupForm" 
jsId="newGroupForm" action="data/new_group.php" method="post">
        <input type="hidden" name="new_group_ajax" 
id="new_group_ajax" value="0">
        <label for="new_group_name">Group Name:</label>
        <input type="text" name="new_group_name" 
id="new_group_name" required="true" dojoType="dijit.form.ValidationTextBox" />
        <button dojoType="dijit.form.Button" type="submit">Submit</button>
        <button dojoType="dijit.form.Button" jsId="newGroupCancel" 
type="button">Cancel</button>
    </div>
</div>

<div id="editGroupDialog" jsId="editGroupDialog" dojoType="dijit.Dialog" 
title="Rename Group" draggable="false">
    <div dojoType="dijit.form.Form" id="editGroupForm" jsId="editGroupForm" 
action="data/edit_group.php" method="post">
        <input type="hidden" name="edit_group_ajax" id="edit_group_ajax" 
value="0">
        <input type="hidden" name="edit_group_id" id="edit_group_id">
        <table cellspacing="4" cellpadding="4">
            <tr>
               <td><label for="edit_group_old">Old Group 
Name:</label></td>
               <td><input type="text" name="edit_group_old" id="edit_group_old"
disabled="true" dojoType="dijit.form.TextBox" /></td>
            </tr>
            <tr>
                <td><label for="edit_group_name">New Group 
Name:</label></td>
                <td><input type="text" name="edit_group_name" 
id="edit_group_name" required="true" dojoType="dijit.form.ValidationTextBox" 
style="margin-bottom: 6px" /></td>
            </tr>
            <tr>
                <td colspan="2" align="center">
                    <button dojoType="dijit.form.Button" 
type="submit">Submit</button>
                    <button dojoType="dijit.form.Button" jsId="editGroupCancel"
type="button">Cancel</button>
                </td>
            </tr>
        </table>
    </div>
</div>

The New Group dialog contains a form, which will submit to the PHP script data/new_group.php. This form contains a ValidationTextBox form control, which is a Dijit-styled text box with automatic validation. In this case, the control has been flagged as mandatory by setting the attribute required to true. The dialog also contains Submit and Cancel buttons. You will implement the code that handles this form (and all other forms that are created in this section) later in the tutorial.

The Rename Group dialog allows users to change the name of a group. It displays a read-only TextBox control with the current group name and provides a ValidationTextBox for the new group name to be supplied. Again, it also features a pair of buttons.

Next, add the Move Contact dialog window, as shown in Listing 13.

Listing 13. Move Contact dialog mark-up
<div id="moveContactDialog" jsId="moveContactDialog" dojoType="dijit.Dialog" 
title="Move Contact" draggable="false">
    <div dojoType="dijit.form.Form" id="moveContactForm" jsId="moveContactForm" 
action="data/move_contact.php" method="post">
        <input type="hidden" name="move_contact_ajax" 
id="move_contact_ajax" value="0">
        <input type="hidden" name="move_contact_id" id="move_contact_id">
        <table cellspacing="4" cellpadding="4">
            <tr>
                <td><label for="move_contact_name">Contact Name:
</label></td>
                <td><input type="text" name="move_contact_name" 
id="move_contact_name" disabled="true" dojoType="dijit.form.TextBox" /></td>
            </tr>
            <tr>
                <td><label for="move_contact_old">Current 
Group:</label></td>
                <td><input type="text" name="move_contact_old" 
id="move_contact_old" disabled="true" dojoType="dijit.form.TextBox" /></td>
            </tr>
            <tr>
                <td><label for="move_contact_new">New Group:</label>
</td>
                <td><input dojoType="dijit.form.FilteringSelect" 
name="move_contact_new" store="groupsStore" searchAttr="name" query="{type:'node'}" 
id="move_contact_new" required="true" style="margin-bottom: 6px" /></td>
            </tr>
            <tr>
                <td colspan="2" align="center">
                    <button dojoType="dijit.form.Button" 
type="submit">Submit</button>
                    <button dojoType="dijit.form.Button" jsId="moveContactCancel" 
type="button">Cancel</button>
                </td>
            </tr>
        </table>
    </div>
</div>

This dialog displays the selected contact's name and the current group they belong to. It then displays a FilteringSelect drop-down list, which allows the user to select a new group to move the contact to. This drop-down is actually backed by the same data store as the Tree control that displays the list of groups on the left side of the interface. However, the query attribute of the FilteringSelect component lets you tell Dojo not to include the root Groups element, so the user cannot select it as an option to move the contact to.

The next dialog is the Edit Contact dialog, which is actually used for both adding new contacts and modifying existing ones. The code for this dialog is in Listing 14.

Listing 14. Edit/Add Contact dialog mark-up
<div id="editContactDialog" jsId="editContactDialog" dojoType="dijit.Dialog" 
title="Edit Contact" draggable="false">
    <div dojoType="dijit.form.Form" id="editContactForm" jsId="editContactForm"
action="data/edit_contact.php" method="post">
        <input type="hidden" name="edit_contact_ajax" id="edit_contact_ajax" 
value="0">
        <input type="hidden" name="edit_contact_real_id" 
id="edit_contact_real_id">
        <table cellspacing="4" cellpadding="4">
            <tr><td><label for="edit_contact_id">Contact ID:
</label></td>
            <td><input type="text" name="edit_contact_id" id="edit_contact_id" 
disabled="true" dojoType="dijit.form.TextBox" /></td></tr>
            <tr><td><label for="move_contact_new">Group:
</label></td>
            <td><input dojoType="dijit.form.FilteringSelect" 
name="edit_contact_group" store="groupsStore" searchAttr="name" query="{type:'node'}" 
id="edit_contact_group" required="true" /></td></tr>
            <tr><td><label for="edit_contact_first_name">First Name:
</label></td>
            <td><input type="text" name="edit_contact_first_name" 
id="edit_contact_first_name" required="true" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_last_name">Last Name:
</label></td>
            <td><input type="text" name="edit_contact_last_name" 
id="edit_contact_last_name" required="true" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_email_address">
Email Address:</label></td>
            <td><input type="text" name="edit_contact_email_address" 
id="edit_contact_email_address" required="true" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_home_phone">Home 
Phone:</label></td>
            <td><input type="text" name="edit_contact_home_phone" 
id="edit_contact_home_phone" required="false" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_work_phone">Work 
Phone:</label></td>
            <td><input type="text" name="edit_contact_work_phone" 
id="edit_contact_work_phone" required="false" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_twitter">Twitter:
</label></td>
            <td><input type="text" name="edit_contact_twitter" 
id="edit_contact_twitter" required="false" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_facebook">Facebook:
</label></td>
            <td><input type="text" name="edit_contact_facebook" 
id="edit_contact_facebook" required="false" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td><label for="edit_contact_linkedin">LinkedIn:
</label></td>
            <td><input type="text" name="edit_contact_linkedin" 
id="edit_contact_linkedin" required="false" dojoType="dijit.form.ValidationTextBox" 
/></td></tr>
            <tr><td colspan="2" align="center">
                <button dojoType="dijit.form.Button" 
type="submit">Submit</button>
                <button dojoType="dijit.form.Button" jsId="editContactCancel" 
type="button">Cancel</button>
            </td></tr>
        </table>
    </div>
</div>

There's quite a bit of mark-up for this form, but that's only because of the number of fields it contains; it's not any more complex than the Move Contact dialog, really. It has a display-only field for the Contact ID value, which will be set to [NEW] in JavaScript in the case of creating new contacts (the ID will be auto-generated in MySQL). It also contains form fields for the Group (the same drop-down as the Move Contact dialog), first name, last name, email address, home phone, work phone, Twitter account, Facebook account, and LinkedIn account.

The final dialog that needs to be created is a standard "OK" dialog box that will be used as a success or error message display window whenever a save operation is performed using XHR/Ajax. The code for this dialog is shown in Listing 15.

Listing 15. OK message dialog box mark-up
<div id="okDialog" jsId="okDialog" dojoType="dijit.Dialog" title="Title" 
draggable="false">
    <p id="okDialogMessage" style="margin-top: 5px">Message</p>
    <div align="center">
        <button dojoType="dijit.form.Button" jsId="okDialogOK" 
type="button">OK</button>
    </div>
</div>

There's nothing at all complex about this dialog—it simply contains a message and a button to close it. With this dialog now created, that concludes the interface code for the application. You can reload the interface in your browser if you wish, but you won't see any difference to the screenshot in Figure 5, as these dialogs need to be connected using JavaScript. The next section explains how to do this.


Application interface logic

With the user interface mark-up created, you can now start to connect the various components to one another using JavaScript.

Adding logic on load

Any JavaScript code that you add should be executed when the DOM has finished loading, so before doing anything, add the code in Listing 16 to the end of the script.js file in the js subdirectory of your application.

Listing 16. Add logic on load
dojo.addOnLoad(function() {
    //Application code goes here
});

All the source code you add from here on should be added inside this function block. First, let's connect the tree and the data grid so that when you click on a node in the tree, the grid will display the contacts in the selected group.

Connecting the tree to the grid

Add the following functions in Listing 17 inside the dojo.addOnLoad function block.

Listing 17. Updating the data grid from the tree
function refreshGrid() {
    contactsGrid.selection.clear();
    mnuEditContact.set("disabled", true);
    mnuMoveContact.set("disabled", true);
    mnuDeleteContact.set("disabled", true);
    ctxMnuEditContact.set("disabled", true);
    ctxMnuMoveContact.set("disabled", true);
    ctxMnuDeleteContact.set("disabled", true);
    dijit.byId("contactView").set("content", '<em>Select a contact to 
view above.</em>');
}

function updateDataGrid(item) {
    var newURL = "data/contacts.php?group_id="+item.id;
    var newStore = new dojo.data.ItemFileReadStore({url: newURL});
    contactsGrid.setStore(newStore);
    refreshGrid();
}

function selectNode(e) {
    var item = dijit.getEnclosingWidget(e.target).item;
    if(item !== undefined) {
        groupsTree.set("selectedItem",item);
        if(item.id != 0) {
            mnuRenameGroup.set("disabled",false);
            mnuDeleteGroup.set("disabled",false);
            ctxMnuRenameGroup.set("disabled",false);
            ctxMnuDeleteGroup.set("disabled",false);
        } else {
            mnuRenameGroup.set("disabled",true);
            mnuDeleteGroup.set("disabled",true);
            ctxMnuRenameGroup.set("disabled",true);
            ctxMnuDeleteGroup.set("disabled",true);
        }
    }
}

The refreshGrid function simply clears any selections in the grid and the main contact view pane and disables any related menu options. The updateGrid function refreshes the grid's data store, telling it to look for contacts in the selected group. Finally, the selectNode function sets the selected node on the tree, and enables/disables menu options as appropriate (they should be enabled if a "real" group is selected and disabled if the root Groups node is selected).

Now it's time to connect these functions to events so that the tree and grid can talk to one another (see Listing 18).

Listing 18. Connecting the tree to the grid
dojo.connect(groupsTree, "onClick", null, updateDataGrid);
dojo.connect(groupsTree, "onMouseDown", null, selectNode);

These events ensure that tree nodes get selected, even if you right-click a node. It also updates the data grid when you click on a tree node. Reload the application in your browser, and you should now be able to select a tree and see the relevant group contacts in the grid. You should also see that when you select a group (and not the Groups root node), the context menu options and application menu options become available. Of course, the menus don't actually do anything yet; you will see how to implement those a little bit later in the tutorial. Figure 6 shows the application with a group selected and the context menu in action.

Figure 6. Connected tree and data grid
Dojo contacts window with a group listing in the left pane, and the right side split horizontally into 2 panes. The top right pane contains columns with last name, first name, then email addresses with a pop-up box to rename or delete the group.

Connecting the grid to the display pane

Next, you need to connect the grid and the main contact display pane (the bottom section on the right side of the interface). First, add the two functions given in Listing 19 to the script.js file, below the code you added from Listing 18.

Listing 19. Updating the contact view from the grid
function selectRow(e) {
    if(e.rowIndex != null) {
        this.selection.clear();
        this.selection.setSelected(e.rowIndex, true);

        mnuEditContact.set("disabled", false);
        mnuMoveContact.set("disabled", false);
        mnuDeleteContact.set("disabled", false);
        ctxMnuEditContact.set("disabled", false);
        ctxMnuMoveContact.set("disabled", false);
        ctxMnuDeleteContact.set("disabled", false);
    }
}

function displayContact(idx) {
    var item = this.getItem(idx);
    var contactId = item.id;
    contactView.set("href", "data/contact.php?contact_id="+contactId);

    mnuEditContact.set("disabled", false);
    mnuMoveContact.set("disabled", false);
    mnuDeleteContact.set("disabled", false);
    ctxMnuEditContact.set("disabled", false);
    ctxMnuMoveContact.set("disabled", false);
    ctxMnuDeleteContact.set("disabled", false);
}

The selectRow function is used to set the row when a user right-clicks on the grid, and the displayContact function sets the href value of the ContentPane where the contact details are to be displayed. This content is loaded asynchronously from the PHP script contact.php, which is passed the ID of the selected contact. This function also enables menus as appropriate. These functions don't do anything unless they are connected to events, so let's wire them up now (see Listing 20).

Listing 20. Connecting the grid to the display pane
dojo.connect(contactsGrid, "onRowContextMenu", null, selectRow);
dojo.connect(contactsGrid, "onSelected", null, displayContact);

You can now reload the application in your browser and you should be able to select a contact in the grid and see the contact's details in the view pane. The relevant options should also be enabled in both the application and context menus. This is shown in Figure 7.

Figure 7. Connected grid and display pane
Dojo contacts window with a group listing in the left pane, and the right side split horizontally into 2 panes. The top right pane contains columns with last name, first name, then email addresses. The bottom right pane shows details for Joe Lennon.

Connecting menu options to dialog windows

Up until this point, none of the menu options in the application actually do anything. Similarly, there are a series of dialog windows that never appear. In this section, you will connect the menus to open these dialog windows accordingly.

Creating functions for configuring dialog windows

First up, create the functions given in Listing 21, which will configure the dialog windows correctly for the selected operation.

Listing 21. Functions for configuring dialog windows for selected operation
function renameGroup() {
    var group = groupsTree.get("selectedItem");
    var groupId = group.id;
    var groupName = group.name;

    dojo.byId("edit_group_id").value = groupId;
    dijit.byId("edit_group_old").set("value", groupName);
    editGroupDialog.show();
}
function refreshGroupDropDown() {
    var theStore = dijit.byId("edit_contact_group").store;
    theStore.close();
    theStore.url = "data/groups.php";
    theStore.fetch();
}
function newContact() {
    var contact = contactsGrid.selection.getSelected()[0];
    refreshGroupDropDown();
    dojo.byId("edit_contact_real_id").value = "";
    dojo.byId("edit_contact_id").value = "[NEW]";
    dijit.byId("edit_contact_group").reset();
    dijit.byId("edit_contact_first_name").reset();
    dijit.byId("edit_contact_last_name").reset();
    dijit.byId("edit_contact_email_address").reset();
    dijit.byId("edit_contact_home_phone").reset();
    dijit.byId("edit_contact_work_phone").reset();
    dijit.byId("edit_contact_twitter").reset();
    dijit.byId("edit_contact_facebook").reset();
    dijit.byId("edit_contact_linkedin").reset();
    
    dijit.byId("editContactDialog").set("title", "New Contact");
    dijit.byId("editContactDialog").show();
}
function editContact() {
    refreshGroupDropDown();
    var contact = contactsGrid.selection.getSelected()[0];
    dojo.byId("edit_contact_real_id").value = contact.id;
    dojo.byId("edit_contact_id").value = contact.id;
    dijit.byId("edit_contact_group").set("value", contact.group_id);
    dojo.byId("edit_contact_first_name").value = contact.first_name;
    dojo.byId("edit_contact_last_name").value = contact.last_name;
    dojo.byId("edit_contact_email_address").value = contact.email_address;
    dojo.byId("edit_contact_home_phone").value = contact.home_phone;
    dojo.byId("edit_contact_work_phone").value = contact.work_phone;
    dojo.byId("edit_contact_twitter").value = contact.twitter;
    dojo.byId("edit_contact_facebook").value = contact.facebook;
    dojo.byId("edit_contact_linkedin").value = contact.linkedin;
    
    dijit.byId("editContactDialog").set("title", "Edit Contact");
    dijit.byId("editContactDialog").show();
}
function moveContact() {
    var contact = contactsGrid.selection.getSelected()[0];
    var contactName = contact.first_name+" "+contact.last_name;
    var groupName = contact.name;
    
    dojo.byId("move_contact_id").value = contact.id;
    dojo.byId("move_contact_name").value = contactName;
    dojo.byId("move_contact_old").value = groupName;
    
    dijit.byId("moveContactDialog").show();
}

Next, you need to connect these functions to the relevant menu options using dojo.connect (see Listing 22).

Listing 22. Connecting menu options to functions
dojo.connect(mnuNewGroup, "onClick", null, function(e) {
    newGroupDialog.show();
});
dojo.connect(mnuRenameGroup, "onClick", null, renameGroup);
dojo.connect(ctxMnuRenameGroup, "onClick", null, renameGroup);
dojo.connect(mnuNewContact, "onClick", null, newContact);
dojo.connect(mnuEditContact, "onClick", null, editContact);
dojo.connect(ctxMnuEditContact, "onClick", null, editContact);
dojo.connect(mnuMoveContact, "onClick", null, moveContact);
dojo.connect(ctxMnuMoveContact, "onClick", null, moveContact);

In Listing 22, the New Group menu option is connected to an anonymous function, as it only has a single line to open the New Group dialog, so it doesn't really warrant a function of its own. Of course, if you prefer, you could put all of the functions used here in anonymous blocks like this, but I've separated them out, as it's easier to explain this way.

You can now reload the application and load any menu (except for the Delete options) and see the relevant dialog window open and load as appropriate. You'll probably notice right away that none of the buttons in these dialogs (except the hide "X" icon in the top right) actually do anything yet. You'll implement those features shortly. Figure 8 shows an example of the Move Contact window in action.

Figure 8. Move Contact dialog window working
Move Contact window with the contact's name, current group, and a field to enter the group you want to move the contact to.

Deleting groups and contacts

In the previous section, you learned how to connect all menus to their relevant dialogs, except for the delete actions. That is because the delete actions do not have a dialog window. Instead, the application should prompt the user to confirm that they want to delete a group or contact, and if they click the appropriate button, it should proceed with the deletion.

Dojo's shortcomings are few and far between, but, unfortunately, one of them is an out-of-the-box confirmation dialog box. Luckily, it's quite simple to build a custom dialog that does just that. To create a confirmation dialog box, add the function shown in Listing 23 to the script.js file.

Listing 23. Function to create a confirmation dialog box
function confirmDialog(title, body, callbackFn) {
    var theDialog = new dijit.Dialog({
        id: 'confirmDialog',
        title: title,
        draggable: false,
        onHide: function() {
            theDialog.destroyRecursive();
        }
    });

    var callback = function(mouseEvent) {
        theDialog.hide();
        theDialog.destroyRecursive(false);

        var srcEl = mouseEvent.srcElement ? mouseEvent.srcElement : mouseEvent.target;

        if(srcEl.innerHTML == "OK") callbackFn(true);
        else callbackFn(false);
    };

    var message = dojo.create("p", {
        style: {
            marginTop: "5px"
        },
        innerHTML: body
    });
    var btnsDiv = dojo.create("div", {
        style: {
            textAlign: "center"
        }
    });
    var okBtn = new dijit.form.Button({label: "OK", id: "confirmDialogOKButton", 
onClick: callback });
    var cancelBtn = new dijit.form.Button({label: "Cancel", 
id: "confirmDialogCancelButton", onClick: callback });

    theDialog.containerNode.appendChild(message);
    theDialog.containerNode.appendChild(btnsDiv);
    btnsDiv.appendChild(okBtn.domNode);
    btnsDiv.appendChild(cancelBtn.domNode);

    theDialog.show();
}

This function accepts three arguments: the title to display in the dialog, the message to show, and the function that should be called back after the user clicks OK or Cancel. This function is called with a true value if OK is pressed and a false value if Cancel is pressed.

You can now use this function to create confirmation dialogs for the Delete Group and Delete Contact menu options. The results of deleting (and other CRUD operations you'll see in the next section) cause the okDialog box to be shown. So, let's get a handle to the message in that box now, while also getting the OK button to hide the dialog (see Listing 24).

Listing 24. Use the OK button to hide the dialog
var okDialogMsg = dojo.byId("okDialogMessage");
dojo.connect(okDialogOK, "onClick", null, function(e) {
dijit.byId("okDialog").hide();
});

Listing 25 contains the code for the function that handles deleting groups.

Listing 25. deleteGroup function
function deleteGroup() {
    confirmDialog("Confirm delete", "Are you sure you wish to delete this group? 
This will also delete any contacts in this group.<br />This action cannot 
be undone.", function(btn) {
        if(btn) {
            var group = groupsTree.get("selectedItem");
            var groupId = group.id;
            var groupName = group.name;

            dojo.xhrPost({
                url: "data/delete_group.php",
                handleAs: "json",
                content: {
                    "group_id": groupId
                },
                load: function(data) {
                    if(data.success) {
                        groupsStore.fetch({
                            query: {"id": groupId.toString()},
                            onComplete: function (items, request) {
                                if(items) {
                                    var len=items.length;
                                    for(var i=0;i<len;i++) {
                                        var item = items[i];
                                        groupsStore.deleteItem(item);
                                    }
                                }
                            },
                            queryOptions: { deep: true}
                        });
                        groupsStore.save();

                        groupsTree.set("selectedItem", groupsModel.root);
                        updateDataGrid(groupsModel.root);
                        okDialog.set("title","Group deleted successfully");
                        okDialogMsg.innerHTML = "The group <strong>"+groupName+"
</strong> was deleted successfully.";
                        okDialog.show();
                    }
                    else {
                        okDialog.set("title","Error deleting group");
                        okDialogMsg.innerHTML = data.error;
                        okDialog.show();
                    }
                },
                error: function(data) {
                    okDialog.set("title","Error deleting group");
                    okDialogMsg.innerHTML = data;
                    okDialog.show();
                }
            });
        }
    });
}

There's a bit of new ground to cover here, but the deleteContact function is very similar, so let's add that now too, and I'll discuss both in tandem.

Listing 26. deleteContact function
function deleteContact() {
    var confirmed = false;
    confirmDialog("Confirm delete", "Are you sure you wish to delete this 
contact?<br />This action cannot be undone.", function(btn) {
        if(btn) {
            var contact = contactsGrid.selection.getSelected()[0];
            var contactId = contact.id;
            var contactName = contact.first_name+" "+contact.last_name;

            dojo.xhrPost({
                url: "data/delete_contact.php",
                handleAs: "json",
                content: {
                    "contact_id": contactId
                },
                load: function(data) {
                    if(data.success) {
                        var treeSel = groupsTree.get("selectedItem");
                        var groupId;
                        if(treeSel) {
                            groupId = treeSel.id;
                        } else {
                            groupId = 0;
                        }
                        var url = contactsStore.url+"?group_id="+groupId;
                        var newStore = new dojo.data.ItemFileReadStore({url:url});
                        contactsGrid.setStore(newStore);
                        refreshGrid();

                        okDialog.set("title","Contact deleted successfully");
                        okDialogMsg.innerHTML = "The contact <strong>"
+contactName+"</strong> was deleted successfully.";
                        okDialog.show();
                    }
                    else {
                        okDialog.set("title","Error deleting contact");
                        okDialogMsg.innerHTML = data.error;
                        okDialog.show();
                    }
                },
                error: function(data) {
                    okDialog.set("title","Error deleting contact");
                    okDialogMsg.innerHTML = data;
                    okDialog.show();
                }
            });
        }
    });
}

Both of these functions use the new confirmation dialog function to ask the user to confirm the deletion. Deleting a group will cascade delete any contacts in that group, so the user is warned about that in the case of attempting to delete a group. If the user confirms, the functions go to either the group's tree or contacts grid as required and gets the detail of the item that needs to be deleted. An Ajax dojo.xhrPost function call is then used to asynchronously call the relevant server-side PHP API. The JSON response received from this is then parsed and used to display a relevant success or error message.

Finally, you need to connect these functions to the relevant events (that is, the menu options). The code in Listing 27 does just that.

Listing 27. Connecting menu options to delete functions
dojo.connect(mnuDeleteContact, "onClick", null, deleteContact);
dojo.connect(ctxMnuDeleteContact, "onClick", null, deleteContact);
dojo.connect(mnuDeleteGroup, "onClick", null, deleteGroup);
dojo.connect(ctxMnuDeleteGroup, "onClick", null, deleteGroup);

You can now save and reload your application. If you try to delete a contact, you should see the message shown in Figure 9.

Figure 9. Delete confirmation dialog box
A message box asking you to confirm the deletion.

Pressing OK will actually go ahead and delete the contact, and the following message will subsequently be displayed (see Figure 10).

Figure 10. Deletion successful message
A message box telling you the contact was successfully deleted.

You'll also notice that the relevant contact has been removed from the underlying grid. If you delete a group, you should see similar functionality (except that deleting a group also deletes all the contacts in that group).


Saving data from dialog boxes using Ajax

The only function that has yet to be added to the application is the implementation of the dialog boxes for creating new groups and contacts, renaming groups, and editing and moving existing contacts. Let's wrap up the application by implementing these functions now.

In the previous section, you learned how to implement the delete function. This used the dojo.xhrPost function to call a PHP script using Ajax. You will be adding similar function calls to perform the other operations, except you will tap into the form submit actions to override the default functions of the forms.

Adding and renaming groups

Let's start with the adding and renaming of groups. Add the two functions in Listing 28 to your script.js file.

Listing 28. Functions to add and rename groups
function doNewGroup(e) {
    e.preventDefault();
    e.stopPropagation();
    dojo.byId("new_group_ajax").value = "1";
    if(this.isValid()) {
        dojo.xhrPost({
            form: this.domNode,
            handleAs: "json",
            load: function(data) {
                if(data.success) {
                    okDialog.set("title","Group created successfully");
                    okDialogMsg.innerHTML = "The group <strong>"+data.name+
"</strong> was created successfully.";

                    groupsStore.newItem({"id":data.id.toString(),"name":data.name},
{"parent": groupsModel.root, "attribute":"groups"});
                    groupsStore.save();

                    newGroupDialog.hide();
                    okDialog.show();
                }
                else {
                    okDialog.set("title","Error creating group");
                    okDialogMsg.innerHTML = data.error;
                    okDialog.show();
                }
            },
            error: function(error) {
                okDialog.set("title","Error creating group");
                okDialogMsg.innerHTML = error;
                okDialog.show();
            }
        });
    }
}

//Process the editing of an existing group in the database
function doEditGroup(e) {
    e.preventDefault();
    e.stopPropagation();
    dojo.byId("edit_group_ajax").value = "1";
    if(this.isValid()) {
        dojo.xhrPost({
            form: this.domNode,
            handleAs: "json",
            load: function(data) {
                if(data.success) {
                    okDialog.set("title","Group renamed successfully");
                    okDialogMsg.innerHTML = "The group <strong>"+data.name+
"</strong> was renamed successfully.";

                    var group = groupsTree.get("selectedItem");
                    groupsStore.setValue(group, "name", data.name);
                    groupsStore.save();

                    editGroupDialog.hide();
                    okDialog.show();
                }
                else {
                    okDialog.set("title","Error renaming group");
                    okDialogMsg.innerHTML = data.error;
                    okDialog.show();
                }
            },
            error: function(error) {
                okDialog.set("title","Error renaming group");
                okDialogMsg.innerHTML = error;
                okDialog.show();
            }
        });
    }
}

These functions will be called when the user tries to submit the New Group or Rename Group forms. They override the default submit action and update a flag to say that the form is being submitted using Ajax and should receive a JSON response. An XHR POST is then performed to the URL in the form's action attribute. When the response is received, the tree is updated accordingly, and a success dialog message is displayed. Before you can test the functions, however, you need to connect them to the relevant forms. This is shown in Listing 29.

Listing 29. Connecting form submit to relevant functions
dojo.connect(newGroupDialog, "onShow", null, function(e) {
    dijit.byId("new_group_name").reset();
});
dojo.connect(newGroupForm, "onSubmit", null, doNewGroup);
dojo.connect(newGroupCancel, "onClick", null, function(e) {
    newGroupDialog.hide();
});        

dojo.connect(editGroupDialog, "onShow", null, function(e) {
    dijit.byId("edit_group_name").reset();
});
dojo.connect(editGroupForm, "onSubmit", null, doEditGroup);
dojo.connect(editGroupCancel, "onClick", null, function(e) {
    editGroupDialog.hide();
});

When the dialog boxes are displayed, the form values will be reset. Also, when you click Cancel in a dialog box, the dialog is hidden. You should now be able to add and rename groups in the application.

Adding, editing, and moving contacts

Adding and editing contacts are actually done using the same dialog, and you have already provided the functions to distinguish between new and existing contacts. As a result, only a single function is required to take care of this operation. You will also create a function now for handling moving contacts from one group to another. These functions are defined in Listing 30.

Listing 30. Functions to add, edit, and move contacts
function doMoveContact(e) {
    e.preventDefault();
    e.stopPropagation();
    dojo.byId("move_contact_ajax").value = "1";
    if(this.isValid()) {
        dojo.xhrPost({
            form: this.domNode,
            handleAs: "json",
            load: function(data) {
                if(data.success) {
                    okDialog.set("title","Contact moved successfully");
                    okDialogMsg.innerHTML = "The contact was moved successfully.";

                    var treeSel = groupsTree.get("selectedItem");
                    var groupId;
                    if(treeSel) {
                        groupId = treeSel.id;
                    } else {
                        groupId = 0;
                    }
                    var url = contactsStore.url+"?group_id="+groupId;
                    var newStore = new dojo.data.ItemFileReadStore({url:url});
                    contactsGrid.setStore(newStore);
                    refreshGrid();
                        
                    moveContactDialog.hide();
                    okDialog.show();
                }
                else {
                    okDialog.set("title","Error moving contact");
                    okDialogMsg.innerHTML = data.error;
                    okDialog.show();
                }
            },
            error: function(error) {
                okDialog.set("title","Error moving contact");
                okDialogMsg.innerHTML = error;
                okDialog.show();
            }
        });
    }
}

//Process the editing of an existing contact in the database
function doEditContact(e) {
    e.preventDefault();
    e.stopPropagation();
    dojo.byId("edit_contact_ajax").value = "1";
    if(this.isValid()) {
        dojo.xhrPost({
            form: this.domNode,
            handleAs: "json",
            load: function(data) {
                if(data.success) {
                    if(data.new_contact) {
                        okDialog.set("title","Contact added successfully");
                        okDialogMsg.innerHTML = "The contact was added successfully.";
                    } else {
                        okDialog.set("title","Contact edited successfully");
                        okDialogMsg.innerHTML = "The contact was edited successfully.";
                    }

                    var treeSel = groupsTree.get("selectedItem");
                    var groupId;
                    if(treeSel) {
                        groupId = treeSel.id;
                    } else {
                        groupId = 0;
                    }
                    var url = contactsStore.url+"?group_id="+groupId;
                    var newStore = new dojo.data.ItemFileReadStore({url:url});
                    contactsGrid.setStore(newStore);
                    refreshGrid();
                        
                    editContactDialog.hide();
                    okDialog.show();
                }
                else {
                    if(data.new_contact) {
                        okDialog.set("title","Error adding contact");
                    } else {
                        okDialog.set("title","Error editing contact");
                    }
                    okDialogMsg.innerHTML = data.error;
                    okDialog.show();
                }
            },
            error: function(error) {
                okDialog.set("title","Error editing contact");
                okDialogMsg.innerHTML = error;
                okDialog.show();
            }
        });
    }
}

These functions work in a similar fashion to their corresponding "group" functions, so I won't go into too much detail. Rather than update the tree, however, these functions reload the grid control asynchronously. It's also worth noting that the doEditContact function has some additional logic to determine if the user has just added a new contact or edited an existing one, so that the correct message can be displayed to the user.

The final bit of code that's required is the code to connect these functions to the relevant forms. The code for this is shown in Listing 31.

Listing 31. Connecting the add, edit, and move contact actions
dojo.connect(moveContactDialog, "onShow", null, function(e) {
    var theStore = dijit.byId("move_contact_new").store;
    theStore.close();
    theStore.url = "data/groups.php";
    theStore.fetch();
    dijit.byId("move_contact_new").reset();
});
dojo.connect(moveContactForm, "onSubmit", null, doMoveContact);
dojo.connect(moveContactCancel, "onClick", null, function(e) {
    moveContactDialog.hide();
});

dojo.connect(editContactForm, "onSubmit", null, doEditContact);
dojo.connect(editContactCancel, "onClick", null, function(e) {
    editContactDialog.hide();
});

When the Move Contact dialog is shown, it reloads the store behind the drop-down list of groups so that it includes any newly added, renamed groups, and excludes any deleted ones. With this code added, you can save your application and load it in your browser to see it working in all its glory.

Suggested improvements

Although the sample application created in this tutorial is fully functional, there are a number of features that could be added to enhance the user experience and make the application even more impressive. Unfortunately, I did not have the scope to add these in this tutorial, but they should be relatively straightforward to add should you want to do so yourself. Some of these enhancements might include:

  • Allow subgroups to be added to groups
  • Allow contacts to be added to more than one group
  • Drag and drop moving of contacts
  • Re-ordering groups with drag and drop
  • Flexible contact fields (let users add their own fields for individual contacts)
  • Inline contact editing
  • Inline grid editing
  • Multiple row selection and deletion
  • Add a "Trash" feature for deleted items, allowing for recovery and drag and drop deletion
  • Implement Dojo and PHP object orientation to tidy up code
  • Allow the user to move contacts to another group when deleting groups, rather than just deleting them
  • Use MySQLi in PHP scripts for additional security
  • Add more complex validation rules
  • Add log-in system to allow for more than one user

Summary

This tutorial was centered on the creation of a full-featured sample application that lets you manage contacts. It provided a step-by-step guide to creating a user interface using Dojo widgets, and communicating with a MySQL database using XHR/Ajax calls to a series of PHP API scripts. The tutorial should have given you a head start so that you can take the sample application and amend it to create your own complex Dojo applications.


Download

DescriptionNameSize
Tutorial source codedojo.ajax.tutorial.source.zip13KB

Resources

Learn

Get products and technologies

Discuss

  • Get involved in the My developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=629347
ArticleTitle=Build an Ajax application with the Dojo Toolkit
publish-date=03012011