目次


Dojo Toolkit を使って Ajax アプリケーションを構築する

Comments

始める前に

このチュートリアルの対象読者は、Dojo Toolkit を利用して比較的簡単に、しかも見事な見栄えの RIA を作成したいと思っている Web アプリケーション開発者です。チュートリアルでは、Dojo を使用して完全な Web アプリケーションを開発するプロセスを紹介します。この開発プロセスを通して、Dojo について多くを学ぶことになりますが、まずはその前に Dojo の基本をよく理解しておくことをお勧めします。また、HTML と CSS に精通していること、そして JavaScript 開発に慣れていることも必要です。エキスパートである必要はありませんが、これらのトピックに関する知識が深ければ深いほど、チュートリアルの内容を理解しやすくなります。Dojo の詳細を学ぶには、「参考文献」に記載されている入門記事へのリンクを参照してください。

このチュートリアルについて

Dojo Toolkit は、デスクトップ・スタイルの見事な RIA を構築するために必要なビルディング・ブロックがすべて揃っている強力な JavaScript フレームワークです。このフレームワークの Base コンポーネントには、DOM に対するクエリー、DOM の操作、エフェクトとアニメーション、Ajax によるイベント処理など、JavaScript アプリケーションの作成プロセスを容易にする数々の有用な機能が含まれています。けれども、その他多くのライブラリーのなかで Dojo を際立った存在にしているのは、その独特のウィジェット・システムである Dijit です。Dijit コンポーネントには強力なパーサーが組み込まれているため、Dijit コンポーネントをまるで通常の HTML 要素のように扱うことができます。さらに Dojo には、広範なデータ・ストアのサポート、データ・グリッド、そして Dijit コンポーネントには含まれていない追加機能などをはじめ、幅広い拡張機能一式を提供する DojoX コンポーネントもあります。

このチュートリアルでは、以上の 3 つの Dojo Toolkit コンポーネントのそれぞれを、連絡先情報を管理するデスクトップ・アプリケーションを模したサンプル・アプリケーションに実装します。このアプリケーションでは連絡先情報を管理するために、新規連絡先を追加したり、既存の連絡先を編集、削除したりすることができます。また、連絡先をグループに分け、必要に応じてグループ間で連絡先を移動することもできます。グループ自体も、追加、変更、および削除ができるようになっています。このチュートリアルの目的は、十分な機能を備えた動的なデータベース駆動型アプリケーションを比較的単純なプロセスで、しかも予想外に少ないコードで作成する方法を学んでもらうことです。チュートリアルで作成するこのアプリケーションは、皆さん独自のプロジェクトを作成するために簡単に拡張または変更することができます。

前提条件

このチュートリアルに従うには、以下のツールが必要です。

  • PHP をサポートする Web サーバー (Apache など)
  • PHP バージョン 5 以降
  • MySQL バージョン 4 以降 (これより前のバージョンでも機能する場合があります。)
  • Dojo Toolkit 1.5 以降 (Google Content Delivery Network を使用してロードすることができます。または、このツールキットをローカルにダウンロードして、開発用マシンから実行することもできます。)

このアプリケーションで使用する PHP スクリプトと MySQL のテーブルは極めて単純なものなので、他のサーバー・サイド言語 (Java コード、C#、Python など) やデータベース (DB2、Oracle、SQL Server など) にも比較的簡単に変換できるはずです。このアプリケーションでの作業の大部分は Dojo を使って行い、PHP および MySQL は永続化のためにのみ使用します。

以上のツールについては、「参考文献」のリンクを参照してください。

セットアップ

このチュートリアルでは、機能豊富な Dojo Web アプリケーションを開発する方法を紹介します。まずこのセクションでは、チュートリアルで取り上げる Dojo Toolkit のさまざまな概念について説明し、さらにこれから開発するサンプル・アプリケーションの概要を紹介します。

このチュートリアルで使用する Dojo の概念

このチュートリアルに取り組むなかで、皆さんは Dojo Toolkit のさまざまな機能を実践することになります。以下に、Dojo Toolkit の各コンポーネント (Base、Dijit、および Dojox) に含まれる機能について概説します。

Base

Dojo Toolkit の中核となっているのは、多くの JavaScript ライブラリーに共通する基本的な機能一式です。これらの機能には一般に、DOM に対してクエリーを実行するための機能や、DOM を操作するための機能、開発者がクロスブラウザー対応の Ajax リクエストを実行するための機能、イベント処理、データ API などが挙げられます。このチュートリアルでは以下のように、Dojo Base に含まれる多くの機能を使用します。

  • DOM に対するクエリー
  • DOM の操作
  • XmlHttpRequests (XHR/Ajax)
  • イベント処理 (dojo.connect)
  • データ・ストアの読み取り/書き込み

Dijit

Dojo Toolkit の最も際立った特色の 1 つが、Dijit コンポーネント・ライブラリーです。Dijit を利用することで、強力かつ見た目も見事なユーザー・インターフェースを最小限の作業と手間で作成することができます。さらに、Dojo の強力なウィジェット構文解析機能により、Dijit では JavaScript コンポーネントを宣言型で使用することもできます。このチュートリアルでは、以下の多種多様な Dijit コンポーネントを実装します。

  • レイアウト・コンポーネント (BorderContainers および ContentPanes)
  • メニュー (アプリケーションおよびコンテキスト)
  • ツリー
  • ダイアログ
  • フォーム、ValidationTextBoxTextBoxFilteringSelect、およびボタン

DojoX

Dojo Toolkit の Base コンポーネントと Dijit コンポーネントには、大抵の Web アプリケーションに必要な機能の大部分が含まれていますが、これらのコンポーネントには含まれていない機能が追加で必要になることもあります。他のライブラリーではそのよう場合、追加の機能を利用するためのサード・パーティーのプラグインを探さなければならないのが通常です。その一方、Dojo のコミュニティーが主導する DojoX 拡張ライブラリーには、Dojo の他のコンポーネントに含まれていない多くの安定した実験的コンポーネントが集められているので、他を探す必要はありません。このチュートリアルでは DojoX コンポーネントを使用します。

サンプル・アプリケーション — 基本的な連絡先管理アプリケーション

このチュートリアルで焦点とするのは、現実に考えられるアプリケーションを模したサンプル・アプリケーションとして、各種の CRUD (Create, Read, Update, Delete) 操作を組み込んだ機能豊富なアプリケーションを作成することです。最終的には、デスクトップのようなパフォーマンスを実現する Web ベースの連絡先管理システムを完成させます。図 1 に、Web ブラウザーに表示された完成後のアプリケーションを示します。

図 1. 完成後のメイン・アプリケーション
「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」のリストを記載した 3 つの列が示されています。列の上に重ねて表示されているボックスには、連絡先情報を編集、移動、または削除するための選択肢が示されています。
「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」のリストを記載した 3 つの列が示されています。列の上に重ねて表示されているボックスには、連絡先情報を編集、移動、または削除するための選択肢が示されています。

このアプリケーションの画面上部には、「File (ファイル)」および「Edit (編集)」からなるアプリケーション・メニューがあります。メニューのそれぞれには、ユーザーが実行可能な操作 (新規グループおよび連絡先の追加、既存の項目の編集、削除など) を選択できるサブメニューがあります。

アプリケーションの左側にあるツリーには、選択可能なグループが表示されます。新規グループを作成すると、そのグループがツリーに追加されます。ツリーではコンテキスト・メニューを使用できるようになっていて、ツリーで任意のグループを右クリックすると、そのグループの名前変更または削除のメニューが表示されます。グループを選択 (左クリック) すると、右側のペインにそのグループに含まれる連絡先がロードされます。ルート・ノード (「Groups (グループ)」。このノードは削除できません) を選択した場合には、どのグループに属しているかに関わらず、すべての連絡先が右側のペインに表示されます。

アプリケーションの右側は 2 つのビューに分割されています。上半分のセクションには、現在選択されているグループの連絡先を示すグリッド・ビューが表示され、現在選択されている連絡先の詳細が、下半分のセクションに表示されます。グリッドに表示される項目にもコンテキスト・メニューがあり、連絡先を右クリックして適切な項目を選択することにより、連絡先を編集、移動、または削除することができます。

このアプリケーションは、ポップアップ・ウィンドウを有効に活用して追加の機能 (特に項目の作成、編集、削除) を提供します。図 2 に、その一例を示します。

図 2. ダイアログ・ポップアップ表示の例: 連絡先の編集
連絡先情報を編集するための各種フィールドを表示するウィンドウ
連絡先情報を編集するための各種フィールドを表示するウィンドウ

このアプリケーションのポップアップ・ウィンドウは、メイン・アプリケーション・ウィンドウが背後で暗くなったところに、はっきりとポップアップ表示され、そのほとんどがテキスト・ボックス (「必須フィールド」の検証が組み込まれています) や自動補完ドロップダウン・リストなどのコンポーネントで構成されたフォームの形を取ります。項目を削除する場合には、ユーザーに対して本当に項目を削除してよいのか確認を求めるウィンドウが表示されます。ユーザーが「OK」ボタンをクリックすると項目が削除され、そうでなければこの削除プロセスがキャンセルされます。

同意していただけると思いますが、このアプリケーションは典型的なデスクトップ・アプリケーションと非常によく似ています。他の機能としては、アプリケーションの左右に分割された表示ペインのサイズや、右側の上下に分割されたグリッド・ペインとプレビュー・ペインのサイズを変更することができます。また、ブラウザー・ウィンドウのサイズを変更すると、変更後のウィンドウ全体に収まるようにアプリケーションのサイズが変更されます。このサンプル・アプリケーションの構造をベースに、皆さん独自の見事な見栄えのアプリケーションを作成できるはずです。

サーバー・サイドのセットアップ: データベースと PHP スクリプト

このセクションでは、連絡先管理アプリケーションのデータを保管する MySQL データベースを新規に構築します。また、このデータベースとの接続を管理する PHP スクリプトを作成し、MySQL からデータを取得して Dojo の XHR (Ajax) 関数コールバックで簡単に読み取れる JSON (JavaScript Object Notation) フォーマットへと変換する一連の PHP API も作成します。

最初の手順: MySQL データベースとサーバー・サイドの構成

Dojo を導入する前に、アプリケーションのサーバー・サイドの部分をセットアップしておかなければなりません。最初に必要な作業は、アプリケーションのデータ層を形作る MySQL のユーザー、データベース、テーブル、そしてデータを作成することです。MySQL に root でアクセスできる (あるいは、少なくともユーザーとデータベースを作成できる) 権限を持っている場合、このチュートリアルのソース・コードに含まれている install.sql スクリプトを実行すれば、この作業を完了することができます。MySQL の bin ディレクトリーがシステム・パス上にあり、カレント・ディレクトリーがソース・コード・フォルダーであるという前提でこのスクリプトを実行する方法は、以下のとおりです。

$ mysql -u root -pMyPassword < install.sql

当然、「MyPassword」の部分は実際の MySQL の root パスワードに置き換えてください。このコマンドを実行すると、新規データベース・ユーザー、データベース、テーブル、およびサンプル・データが作成されます。リスト 1 に、install.sql の内容を記載します。

リスト 1. MySQL データベースを構築するための install.sql スクリプト
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');

PHP でデータベースに接続する

MySQL データベースを作成したら、次は、新しく作成されたデータベース・テーブルに対して、データの挿入、更新、削除を行う PHP スクリプトを作成します。

このサンプル・アプリケーションでは、これらの PHP スクリプトは単に Ajax または XHR リクエストによって呼び出されるだけに過ぎません。呼び出されたスクリプトは、リクエスト・オブジェクトに対するレスポンスを JSON フォーマットで返しますが、contact.php は例外です。この PHP スクリプトは DOM に挿入される HTML スニペットを返します。すべての PHP スクリプトは、data という名前のサブディレクトリーに配置されています。このプロジェクトに必要なスクリプトは以下のとおりです。

  • contact.php (指定された連絡先の詳細情報を取得)
  • contacts.php (データベースに保管された連絡先の JSON 表現を取得)
  • database.php (MySQL との接続を管理)
  • delete_contact.php (データベースから連絡先を削除)
  • delete_group.php (データベースからグループを削除)
  • edit_contact.php (データベース内の連絡先を追加/編集)
  • edit_group.php (データベース内のグループの名前を変更)
  • groups.php (データベースに保管されたグループの JSON 表現を取得)
  • move_contact.php (連絡先を別のグループに移動)
  • new_group.php (データベースに新規グループを追加)

スペースに限りがあるため、このチュートリアルでは上記のすべてのファイルの内容を記載することはできません。完全なソース・コードの内容については、このチュートリアルに付属のソース・コードを参照してください。ただし、これらの PHP スクリプトがどのように機能するかを理解できるように、いくつかのスクリプトを抜粋してその内容を説明します。

まず、database.php ファイルは、このチュートリアルの他のすべての PHP スクリプトが MySQL データベースとの接続を開くために使用します。このファイルは、リスト 2 に記載するような非常に単純な内容になっています。

リスト 2. data/database.php ファイルの内容
<?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);
?>

JSON フォーマットでデータを出力する API の機能をPHP で作成する

サンプル・アプリケーションで行われる操作には、主に 2 つのタイプがあります。1 つはデータの取得、もう 1 つはデータの操作です。それぞれの例を検討すると、まず、groups.php はデータベースからグループのリストを取得するためのスクリプトです。このスクリプトは取得したリストを、Dijit Tree ウィジェットが読み取れるように Dijit の Identity API 構造を使って表現します。リスト 3 に、このファイルの内容を記載します。

リスト 3. data/groups.php ファイルの内容
<?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);
?>

Dijit Identity API を使用するには、JSON データ構造が、最上位レベルに identifierlabel、および items プロパティーを持つようなフォーマットになっていなければなりません。リスト 3 では、items プロパティーにデフォルトのルート項目である Groups を追加しました。この項目は特定のグループを表現するものではありませんが、(グループがないとしても) 常にツリーの最上部に表示されます。次に、SQL SELECT 文の実行結果をループ処理して、それぞれの項目を子要素として追加します。

まず、items 配列に各項目を格納しますが、ルート項目の場合、_reference プロパティーを持つオブジェクトのコレクションからなる groups 配列を設定し、その _reference プロパティーによって子項目の ID が参照されるようにします。そしてループ処理を終えると、このループ処理で作成した $data 配列を JSON として出力します。後ほど、この PHP スクリプトに関連付けるデータ・ストアを作成します。さらに、このスクリプトの JSON 出力を取得して適宜 Dijit Tree で使用することになります。

次に説明する例は、必要に応じて group_id 値を設定して連絡先レコードを更新する move_contact.php です。リスト 4 に、このファイルのコードを記載します。

リスト 4. data/move_contact.php ファイルの内容
<?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);
}
?>

リスト 4 には、2 つの POST パラメーターが必要です。その 1 つは連絡先 ID、もう 1 つは、その連絡先情報を移動する先の新規グループ ID です。これらのパラメーターは、SQL UPDATE 文に組み込まれます。Ajax によってリクエストが行われる際には、そのリクエストが Ajax を使用して送信されたものであり、通常の HTML POST コマンドを使用して送信されたリクエストではないことを示すためのパラメーターが使用されます。HTML POST コマンドで送信された場合には、ページ全体が最新情報で更新されます (そのため (必要に応じて実装できる) 標準的な HTML フォールバックが可能になります)。Ajax を使用して送信された場合には、返されるレスポンスの構造は JSON フォーマットで構成されたものになります。

PHP サーバー・サイドの API のほとんどは、上記のコード・リストと同じように機能します。前述したように、すべてのスクリプトの完全なコード・リストについては、このチュートリアルに付属のソース・コードを参照してください。

これで、アプリケーションのサーバー・サイドの部分は構成できたので、ここからは、Dojo を使用してアプリケーションの粋なフロントエンドを作成する作業に取り掛かれます。次のセクションでは、Dijit コンポーネントを HTML 文書に宣言型で追加するのがいかに簡単であるかを説明します。

Dijit を使用して作成するユーザー・インターフェース

Dojo のリッチ・コンポーネント・ライブラリーである Dijit を使用すると、完全なユーザー・インターフェースを作成するために必要なコーディングが最小限で済み、作業の手間も最小限になります。Dijit コンポーネントを使用するには 2 通りの方法があります。1 つは、JavaScript によるプログラムで使用する方法で、もう 1 つは HTML スタイルの要素に dojoType 属性を設定する宣言型の方法です。このチュートリアルで作成するすべての Dijit ウィジェットは宣言型を用いますが、これらのウィジェットを利用するための JavaScript ロジックは、別個の JavaScript ソース・ファイルに実装します。

メイン・アプリケーションのコードは、3 つのファイルに格納されます。レイアウトとウィジェット宣言を処理するのは、メインの index.html ファイルです。この特定のアプリケーションに必要なすべてのスタイルシートは、css サブディレクトリー内に単独で置かれている style.css ファイルに含まれています。すべての JavaScript ロジックは、js サブディレクトリー内の script.js ファイルに格納します。

style.css ファイルのコードについては、このチュートリアルに付属のソース・コードを参照してください。このコードでは基本的な CSS 構文しか使用していないので、その内容はコードを読めば明らかなはずです。

このセクションでは、HTML と Dojo ウィジェットを使用してアプリケーションのユーザー・インターフェースを作成する方法を説明します。

基本構造

まず、アプリケーションの基本的な HTML 構造を作成するところから始めます。この構造には、使用する Dijit テーマを設定するスタイルシートや、必要な JavaScript ソース・ファイルをロードするスタイルシートなど、関連するあらゆるスタイルシートを含めます。サンプル・アプリケーションでは便宜上、Google の CDN (Contents Delivery Network) から Dojo をロードしています。もちろんこれは、インターネットに接続できなければアプリケーションを使用できないことを意味しますが、Dojo ライブラリーをダウンロードして、ローカルからロードするようにすれば、オフラインでアプリケーションを使用することもできます。

リスト 5 に、出発点となるアプリケーションのメイン index.html ファイルを記載します。

リスト 5. 基本の index.html 構造
<!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>

ご覧のように、アプリケーションがロードするスタイルシートは 4 つあります。そのうちの 3 つは Dojo 自身のスタイルシートで、その 1 つは Claro Dijit テーマのメイン・テーマ CSS ファイルです。他の 2 つは Data Grid DojoX 拡張機能に使用されます。重要な点として、この文書のメイン <body> 要素には、クラスとして claro を追加していることに注意してください。これは、Dojo パーサーに Claro テーマを使用するように指示するためです。このクラスを追加しなければアプリケーションが正しく表示されないため、必ず追加するようにしてください。アプリケーションの <script> ブロックは、ファイルの終わりにある </body> 終了要素の直前でロードします。通常、JavaScript ソース・ファイルを読み込む方法としては、この方法が推奨されています。ファイルの終わりのほうでスクリプトを読み込むようにすれば、スクリプトを読み込みが完了するまでページの他の部分がロードされないという事態を防ぐことができるためです。Google の CDN からロードするメイン Dojo スクリプトの他、後ほど作成するアプリケーションのスクリプト・ファイルも読み込んでいます。

アプリケーションのレイアウトを定義する

次は、メイン・アプリケーションのレイアウトを作成します。この段階で目標とするのは、図 3 に示す外観にすることです。基本的にこのセクションの終わりまでには、Web ブラウザーにファイルをロードすると、このスクリーンショットのような画面が表示されるようにします。

図 3. アプリケーションの基本レイアウト
左側に 1 つのカラムがあり、右側に上下に分かれた 2 つのカラムがあるウィンドウ
左側に 1 つのカラムがあり、右側に上下に分かれた 2 つのカラムがあるウィンドウ

アプリケーションで Dijit コンポーネントを使用するには、dojo.require JavaScript 関数を使用することで、これらのウィジェットを読み込むように Dojo に対して指定する必要があります。script.js という名前のファイルを作成して、アプリケーション・フォルダーの js サブディレクトリーに保存してください。このファイルに、リスト 6 に記載する内容を追加します。

リスト 6. Dijit レイアウト・コンポーネントを読み込む
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");

上記のコードにより、Dojo は BorderContainer および ContentPane レイアウト・ウィジェット、そしてアプリケーションのメニュー・バーで使用する各種の Menu ウィジェットを読み込みます。ウィジェットが読み込まれるようになったので、これらのコンポーネントをアプリケーションの index.html ファイルに追加することができます。それには、ファイルのコメント <!-- Content goes here --> をリスト 7 の内容に置き換えてください。

リスト 7. レイアウト・コンポーネントをアプリケーションに追加する
<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>

リスト 7 に示されているように、Dijit コンポーネントをページに追加するには、特殊な dojoType 属性を設定した標準 HTML 要素を使用します。これらのウィジェット宣言では、それぞれのウィジェット・タイプに固有の属性も使用します。その一例は、コンテナーのコンテキストで BorderContainer の子要素を表示する場所を定義する region 属性です。リスト 7 ではかなり多くのコンポーネントが使用されているので、これらのコンポーネントの基本構造に目を通しておきます。

  • dijit.layout.BorderContainer
    • dijit.layout.ContentPane (上部)
  • h1
  • dijit.MenuBar
  • dijit.PopupMenuBarItem
    • dijit.Menu
  • dijit.MenuItem
  • dijit.MenuItem
  • dijit.PopupMenuBarItem
    • dijit.Menu
  • dijit.MenuItem
  • dijit.MenuItem
  • dijit.MenuItem
    • dijit.layout.BorderContainer (中央)
  • dijit.layout.ContentPane (左側)
  • dijit.layout.BorderContainer (中央)
  • dijit.layout.ContentPane (上部)
  • dijit.layout.ContentPane (中央)

最上位レベルにあるのは、BorderContainer です。このコンポーネントでは、5 つの領域 (上部、中央、左側、右側、下部) のいずれかに子コンポーネントを配置できるようになっています。その下のレベルにあるコンポーネントは、上部の ContentPane と中央の BorderContainer の 2 つです。使用されているコンポーネントは 2 つだけなので、上部のコンポーネントが使用しない残りのスペースのすべてを中央のコンポーネントが占めます。上部のセクションには、標準的な HTML の見出しである h1 が 1 つ、そして Dijit Menu 構造が 1 つあります。中央のセクションを占める BorderContainer コンポーネントには、左側に ContentPane、中央に別の BorderContainer (左側のコンポーネントが使用しないスペースを占めます) がネストされます。左側の ContentPane が、Tree コントロールを配置する場所です。中央の BorderContainer には、その子コンポーネントとして上部と中央の領域のそれぞれに ContentPane があります。

この段階でファイルを保存して Web ブラウザーにロードすると、図 3 に示したような画面が表示されます。

Tree コンポーネントを追加する

次に、アプリケーションの左側に Tree コンポーネントを追加します。Dijit の Tree コンポーネントは複合コンポーネントであり、適切に機能させるためには、いくつかのサポート・コンポーネントが必要になります。具体的には、まず、Dojo の Identity API に対応するデータ・ストアを用意し、JSON データのソースとなるようにする必要があります。サンプル・アプリケーションでは、この JSON データは PHP スクリプト data/groups.php によって生成されるため、この Identity API にツリーのデータ・ストアを関連付けることができます。次に Tree コンポーネントに必要なのは、このデータ・ストアの構造と、読み取る対象となるデータの両方を定義するモデル・コンポーネントです。これらの必要を満たした上で、Tree コンポーネントを追加して、このモデルと関連付けます。

まず始めに Dojo に対して、必要なコンポーネントをロードしてアプリケーションで使用できるように指示する必要があります。それには、script.js ファイルにリスト 8 に記載する 2 行のコードを追加します。

リスト 8. ツリー関連のコンポーネントをロードする
dojo.require("dojo.data.ItemFileWriteStore");
dojo.require("dijit.Tree");

書き込み可能なデータ・ストアが使用されていることに疑問を感じるかもしれませんが、Dojo の Tree コンポーネントを操作するのは非常に複雑で、(このコンポーネントを ContentPane に組み込んで、ContentPane 全体を最新情報で更新しない限り) コンポーネントを直接、最新情報で非同期に更新することはできません。そのため、ツリーの項目を動的に追加/更新/削除するには、書き込み可能なデータ・ストアに接続しなければならないというわけです。

次に、index.html ファイルの「Tree to go here」セクションを見つけて、リスト 9 のコードに置き換えます。このコードはデータ・ストア、ツリー・モデル、そしてツリー自体を定義して、ツリーをアプリケーションの左側に配置します。ツリーにはコンテキスト・メニューも追加しますが、そのメニュー項目は現時点では使用することができません。コンテキスト・メニューは後で実装します。

リスト 9. ツリーをアプリケーションに追加する
<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>

お気付きかもしれませんが、リスト 9 では、それぞれのコンポーネントごとに jsId 属性が使用されています。これは、Dojo パーサーに対し、これらのコンポーネントのそれぞれに対応する JavaScript 変数を作成するように指示する属性で、作成された変数には、各属性に指定された名前が付けられます。こうすることにより、以降のコンポーネントの操作が遥かに簡単になります。それは、ウィジェットを見つける際に、dijit.byId を使って DOM に対するクエリーを実行する必要がなくなるためです。さらに、速度の遅い Web ブラウザーでは尚更のこと、DOM に対するクエリーは、アプリケーションの実行速度と応答性の低下を招くため、クエリーを実行する必要がなくなれば、パフォーマンスが向上することにもなります。

この時点でブラウザーをリロードすると、アプリケーションは図 4 のウィンドウのように表示されます (PHP スクリプトが正しくセットアップされていて、ツリーのデータが実際にロードされることが前提です)。

図 4. 実際の Dijit Tree
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ

Grid コンポーネントを追加する

グリッドをアプリケーションに追加する方法は、ツリー・コンポーネントと同様です。ただしツリーとは異なり、グリッドの場合には、データ・ストアを最新情報で更新することによって、簡単に非同期でリロードできるため、モデルがなくても読み取り専用データ・ストアを支障なく使用することができます。まず、script.js ファイルに以下の行を追加して、グリッド・コンポーネントをアプリケーションで使用できるようにしてください。

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

データ・グリッド・ウィジェットは、Dijit ライブラリーではなく、DojoX に含まれていることに注意してください。DojoX コンポーネントには、追加のスタイルシートをロードしなければならない場合がよくあります (この例の場合、これらのスタイルは前に作成した基本テンプレートにすでに組み込まれています)。

これで、グリッドをメイン・アプリケーション・ファイルに追加できるようになりました。Dojo のデータ・グリッドは標準的な <table> 要素に適用できるため、使用したい見出しを簡単に定義することができます。グリッドは、グリッドの動作 (例えば、クライアント・サイドで列のソートを有効にするかどうかなど) を設定する一連の属性を定義することができます。index.html ファイルの「Grid to go here」セクションを見つけて、リスト 10 のコードに置き換えてください。

リスト 10. グリッドをアプリケーションに追加する
<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>

ツリーと同じく、グリッドでもコンテキスト・メニューを使用できるようになっています。これらのメニューを有効にする方法については、後のセクションで説明します。上記のコードで、インライン Dojo メソッドの <script> ブロックがあることに疑問を感じている読者のために説明すると、<script> ブロックがある理由は、DataGrid コンポーネントのバグによるものです。この空のブロックが組み込まれていなければ、コンテキスト・メニューが表示されません。指摘しておく価値のある点として、これらのブロックを使用してレイアウト・コードに直接 JavaScript コードを組み込むこともできます。しかし個人的には、アプリケーション・ロジックはできるだけ UI 要素から切り離しておくという方法のほうが好みです。

グリッドがデータをロードするために使用するのは、API data/contacts.php に関連付ける読み取り専用データ・ストアです。グリッドはクライアント・サイドで、「苗字 (Last Name)」、「名前 (First Name)」の順で自動的にデータをソートします。上記のグリッドは、一度に選択できる行を 1 行に制限し、データが見つからない場合に表示されるエラー・メッセージを定義しています。

この段階で、アプリケーションは図 5 のように表示されるようになっています (この場合も、ソース・コード・バンドルの PHP スクリプトを追加してあること、そしてサーバー・サイドが正しく構成されていることが前提です)。

図 5. 実際の DojoX データ・グリッド
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ。右上のペインには、「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」の列が並んでいます。
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ。右上のペインには、「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」の列が並んでいます。

非表示のアプリケーション・コンポーネント: dijit.Dialog ウィンドウ

これまでの手順で、メイン・アプリケーション UI は完成しました。このセクションでは、隠しダイアログ・ウィンドウを追加し、メニューを選択すると、それらのダイアログ・ウィンドウがメイン・アプリケーション・ウィンドウの上にモーダル・ウィンドウのように表示されるようにします。これらのポップアップ・ウィンドウは、そのほとんどがグループや連絡先のさまざまなプロパティーを定義できる Web フォームを表示するものですが、ここでは JavaScript による標準的なアラート・ダイアログ・ボックスよりも多少見栄えの良い単純な「OK」メッセージ・ウィンドウも作成します。

追加する隠しダイアログ・ウィンドウは全部で 5 個あります。これらのダイアログ・ウィンドウには、ValidationTextBoxFilteringSelectButton を含め、各種の Dijit フォーム・フィールドがあります。このすべての追加ウィジェットを組み込むには、まず dojo.require を使って読み込む必要があるので、リスト 11 のコードを script.js ファイルに追加します。

リスト 11. ダイアログおよびフォーム・コンポーネントを読み込む
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");

これで、ダイアログ・ボックスを追加する準備ができました。これらのコンポーネントがページの他のすべての要素の上に表示されるようにするには、メイン・アプリケーションの BorderContainer の外側にコンポーネントを追加する必要があります。したがって、コンポーネントの追加を開始する場所は、index.html ファイルの終わり近くにある最初の <script> 要素の直前ということになります。

最初に、「New Group (新規グループ)」および「Rename Group (グループの名前変更)」ダイアログ・ボックスを追加します。これらのウィンドウのコードは、リスト 12 のとおりです。

リスト 12. 「New Group (新規グループ)」および「Rename Group (グループの名前変更)」ダイアログ・ボックスのコード
<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>

「New Group (新規グループ)」ダイアログに表示されるのは、PHP スクリプト data/new_group.php に送信されるフォームです。このフォームに含まれる ValidationTextBox フォーム・コントロールは Dijit スタイルのテキスト・ボックスで、入力が自動的に検証されます。この例の場合、required 属性が true に設定されているため、このコントロールには必須コントロールとしてのフラグが立てられています。ダイアログには「Submit (送信)」ボタンと「Cancel (取り消し)」ボタンもあります。このフォーム (および、このセクションで作成するその他すべてのフォーム) を処理するコードは、後ほど実装します。

「Rename Group (グループの名前変更)」ダイアログでは、ユーザーがグループの名前を変更することができます。このダイアログは、読み取り専用の TextBox コントロールに現在のグループ名を表示し、新しいグループ名を入力するための ValidationTextBox を提供します。このダイアログにも、ボタンのペアがあります。

次に、「Move Contact (連絡先の移動)」ダイアログ・ウィンドウを追加します。このダイアログ・ウィンドウは、リスト 13 のようになっています。

リスト 13. 「Move Contact (連絡先の移動)」ダイアログのマークアップ
<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>

このダイアログは、選択された連絡先の名前と、その連絡先が現在属しているグループを表示します。そしてその下に、ユーザーが連絡先の移動先となる新しいグループを選択できるように FilteringSelect ドロップダウン・リストを表示します。このドロップダウン・リストの内容は、実際にはインターフェースの左側にグループを一覧表示する Tree コントロールと同じデータ・ストアから取得されます。ただし、FilteringSelect コンポーネントでは、ユーザーが Groups ルート要素を移動先として選択できないように、query 属性を使用して、Dojo にこのルート要素を含めないように指示することができます。

次に追加するダイアログは、「Edit Contact (連絡先の編集)」ダイアログです。このダイアログは、実際には新規連絡先を追加する場合と、既存の連絡先を変更する場合の両方に使用されます。このダイアログのコードはリスト 14 のとおりです。

リスト 14. 「Edit/Add Contact (連絡先の編集/追加)」ダイアログのマークアップ
<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>

このフォームにはかなりの量のマークアップがありますが、それは、ダイアログに含まれるフィールドの数が多いというだけの理由です。決して「Move Contact (連絡先の移動)」ダイアログより複雑なわけではありません。「Contact ID (連絡先 ID)」値の表示専用フィールドは、新しい連絡先を作成する場合には JavaScript で [NEW] に設定されます (ID は MySQL で自動的に生成されます)。このダイアログには、「Group (グループ)」(「Move Contact (連絡先の移動)」ダイアログと同じドロップダウン・リスト)、「Last Name (苗字)」、「First Name (名前)」、「Email Address (e-メール・アドレス)」、「Home phone (自宅の電話番号)」、「Work phone (職場の電話番号)」、「Twitter (Twitter アカウント)」、「Facebook (Facebook アカウント)」、そして「LinkedIn (LinkedIn アカウント)」などのフォーム・フィールドもあります。

作成しなければならない最後のダイアログは、標準的な「OK」ダイアログ・ボックスです。これは、XHR/Ajax を使って保存操作が行われるたびに、成功メッセージまたはエラー・メッセージを表示するためのウィンドウとして使用されます。このダイアログのコードをリスト 15 に記載します。

リスト 15. 「OK」メッセージ・ダイアログ・ボックスのマークアップ
<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>

このダイアログは、まったく複雑ではありません。ここにはメッセージと、ダイアログを閉じるためのボタンが含まれているだけです。このダイアログの作成が完了したところで、アプリケーションのインターフェース・コードについての説明は終わりです。お望みであれば、ブラウザーにインターフェースをリロードすることもできますが、表示されるインターフェースは図 5 のスクリーンショットとまったく変わりません。これらのダイアログは、まだ JavaScript を使用して互いに関連付けられていないためです。次のセクションで、その方法を説明します。

アプリケーション・インターフェースのロジック

ユーザー・インターフェースのマークアップを作成したら、次は JavaScript を使用して各種のコンポーネントを相互に関連付ける作業に取り掛かります。

ロード時のロジックを追加する

追加するあらゆる JavaScript コードは、DOM がロードを完了した時点で実行されなければなりません。したがって、何よりもまず始めに必要なのは、アプリケーションの js サブディレクトリー内にある script.js ファイルの終わりに、リスト 16 のコードを追加することです。

リスト 16. ロード時のロジックを追加する
dojo.addOnLoad(function() {
    //Application code goes here
});

以降に追加するすべてのソース・コードは、この関数ブロック内に追加してください。まずは、ツリー内のノードをクリックすると選択されたグループに含まれる連絡先がグリッドに表示されるように、ツリーとデータ・グリッドを関連付けます。

ツリーをグリッドに関連付ける

dojo.addOnLoad 関数ブロック内に、リスト 17 に示す一連の関数を追加します。

リスト 17. ツリーからデータ・グリッドを更新する
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);
        }
    }
}

refreshGrid 関数は、グリッド内とメインの連絡先表示ペイン内の選択をすべて解除し、関連するメニューの選択項目を無効にするだけに過ぎません。updateGrid 関数は、グリッドのデータ・ストアを最新情報で更新して、選択されたグループに含まれる連絡先を探すように指示します。最後に記載されている selectNode 関数は、選択されたノードをツリーに設定し、そのノードに応じてメニューの選択項目を有効/無効にします (メニューの選択項目は、実際のグループが選択されている場合には有効になり、ルートの Groups ノードが選択されている場合には無効になります)。

今度は、上記の関数をイベントに関連付けて、ツリーとグリッドが互いに対話できるようにします (リスト 18 を参照)。

リスト 18. ツリーをグリッドに関連付ける
dojo.connect(groupsTree, "onClick", null, updateDataGrid);
dojo.connect(groupsTree, "onMouseDown", null, selectNode);

上記の 2 つのイベントにより、ノードを右クリックした場合でも、確実にツリーのノードが選択されるようになります。さらに、ツリーのノードをクリックした場合には、データ・グリッドが更新されるようにもなっています。アプリケーションをブラウザーにリロードしてみてください。ツリーを選択すると、該当するグループの連絡先がグリッドに表示されるようになっているはずです。また、グループを選択すると (Groups ルート・ノードを選択するのではありません)、コンテキスト・メニューとアプリケーション・メニューの項目が選択可能になっていることも確認することができます。もちろん、この段階ではメニューは実際に機能しません。メニューの機能を実装する方法は、もう少ししてから説明します。図 6 に、グループが選択されてコンテキスト・メニューが有効になっている状態のアプリケーションを示します。

図 6. 互いに関連付けられたツリーとデータ・グリッド
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ。右上のペインには、「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」の列が並んでおり、グループの名前変更または削除を選択できるポップアップ・ボックスも表示されています。
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ。右上のペインには、「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」の列が並んでおり、グループの名前変更または削除を選択できるポップアップ・ボックスも表示されています。

グリッドを連絡先表示ペインに関連付ける

次に必要となる作業は、グリッドとメインの連絡先表示ペイン (インターフェース右側の下半分のセクション) を関連付けることです。まず、リスト 18 で script.js ファイルに追加したコードの下に、リスト 19 に記載する 2 つの関数を追加してください。

リスト 19. グリッドから連絡先ビューを更新する
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);
}

selectRow 関数を使用して、ユーザーがグリッドを右クリックして選択した行を設定します。もう一方の displayContact 関数は、連絡先の詳細が表示される ContentPanehref 値を設定する関数です。この関数は、選択された連絡先の ID を PHP スクリプト contact.php に渡し、このスクリプトから非同期で詳細表示の内容をロードします。この関数はまた、選択内容に応じてメニューを有効にします。この 2 つの関数は、イベントに関連付けない限り機能しません。そこで早速、以下のようにして関連付けます (リスト 20 を参照)。

リスト 20. 関数をイベントに関連付けることで、グリッドを連絡先表示ペインに関連付ける
dojo.connect(contactsGrid, "onRowContextMenu", null, selectRow);
dojo.connect(contactsGrid, "onSelected", null, displayContact);

これでブラウザーにアプリケーションをリロードすると、グリッドで連絡先を選択した場合に、その連絡先の詳細がメインの表示ペインに表示されるようになったはずです。アプリケーション・メニューとコンテキスト・メニューの関連する項目も選択可能になっています。この結果を図 7 に示します。

図 7. 互いに関連付けられたグリッドと連絡先表示ペイン
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ。右上のペインには、「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」の列が並んでおり、右下のペインには、Joe Lennon の連絡先の詳細が表示されています。
左ペインにグループが表示され、右側が上下 2 つのペインに分割された「Dojo Contacts (Dojo 連絡先)」ウィンドウ。右上のペインには、「苗字 (Last Name)」、「名前 (First Name)」、「e-メール・アドレス (E-mail Address)」の列が並んでおり、右下のペインには、Joe Lennon の連絡先の詳細が表示されています。

メニューの項目とダイアログ・ウィンドウとの関連付け

これまでのところ、アプリケーションのメニュー項目のなかで、実際に機能するものはまだ 1 つもありません。それと同じく、一連のダイアログ・ウィンドウも実際に表示されることはありません。このセクションで、メニューとダイアログ・ウィンドウを関連付けて、それぞれのメニュー項目に対応するダイアログ・ボックスが表示されるようにします。

ダイアログ・ウィンドウを構成するための関数を作成する

まず始めに、リスト 21 に記載する一連の関数を作成します。これらの関数が、選択された操作のダイアログ・ウィンドウを適切に構成します。

リスト 21. 選択された操作に応じてダイアログ・ウィンドウを構成する関数
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();
}

次に、上記の関数を、dojo.connect を使用して該当するメニュー項目に関連付けます (リスト 22 を参照)。

リスト 22. メニュー項目を関数に関連付ける
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);

リスト 22 での「New Group (新規グループ)」メニュー項目は、「New Group (新規グループ)」ダイアログを開くための行がたった 1 行しかないことから、匿名関数に関連付けられています。したがって、1 つの固有の関数を保証するわけではありません。もちろん、ここで使用する関数のすべてをこれと同様の匿名ブロック内に含めることもできますが、関数をそれぞれに切り離しておいたほうが説明しやすいので上記のようにしました。

これで、アプリケーションをリロードして任意のメニュー (「Delete (削除)」を除く) をロードすると、関連するダイアログ・ウィンドウが開き、適切な内容がロードされます。すぐに気付くはずですが、これらのダイアログのボタンはどれも、この段階では実際に機能しません (右上の非表示「X」アイコンは例外です)。ボタンの機能は、この後すぐに実装します。図 8 に一例として、実行中の「Move Contact (連絡先の移動)」ダイアログ・ウィンドウを示します。

図 8. 有効な「Move Contact (連絡先の移動)」ダイアログ・ウィンドウ
「Contact Name (連絡先の名前)」、「Current Group (現在のグループ)」、および連絡先の移動先となる「New Gropu (新規グループ)」を入力するフィールドが表示された「Move Contact (連絡先の移動)」ダイアログ・ウィンドウ
「Contact Name (連絡先の名前)」、「Current Group (現在のグループ)」、および連絡先の移動先となる「New Gropu (新規グループ)」を入力するフィールドが表示された「Move Contact (連絡先の移動)」ダイアログ・ウィンドウ

グループおよび連絡先を削除する

前のセクションでは、すべてのメニューをそれぞれに関連するダイアログに関連付ける方法を学びましたが、削除アクションについてはまだ説明していません。それは、削除アクションにはダイアログ・ウィンドウがないためです。ダイアログ・ウィンドウを表示する代わりに、アプリケーションはユーザーに対して、グループまたは連絡先を本当に削除してよいのか確認を促します。「削除してよい」に該当するボタンをユーザーがクリックすると、アプリケーションは削除操作を行います。

Dojo にはごくわずかな欠点しかありませんが、残念ながらその 1 つが、すぐに使える確認ダイアログ・ボックスがないことです。しかし幸いなことに、それに代わるカスタム・ダイアログを至って簡単に作成することができます。確認ダイアログ・ボックスを作成するには、リスト 23 に記載する関数を script.js ファイルに追加してください。

リスト 23. 確認ダイアログ・ボックスを作成するための関数
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();
}

上記の関数は、3 つの引数を取ります。具体的には、ダイアログに表示するタイトル、表示するメッセージ、そしてユーザーが「OK」または「Cancel (キャンセル)」をクリックした後に呼び出されるコールバック関数です。コールバック関数は、ユーザーが「OK」をクリックした場合には true の値、「Cancel (キャンセル)」をクリックした場合には false の値が設定されて呼び出されます。

この関数を使えば、メニューの「Delete Group (グループの削除)」および「Delete Contact (連絡先の削除)」用の確認ダイアログを作成することができます。削除操作 (そして、次のセクションで取り上げるその他の CRUD 操作) を実行すると、okDialog ボックスが表示されることになります。そこで、前もってこのボックスに表示されるメッセージに対処すると同時に、ダイアログを非表示にする「OK」ボタンに対処しておきます (リスト 24 を参照)。

リスト 24. 「OK」ボタンを使用してダイアログを非表示にする
var okDialogMsg = dojo.byId("okDialogMessage");
dojo.connect(okDialogOK, "onClick", null, function(e) {
dijit.byId("okDialog").hide();
});

リスト 25 に、グループの削除を処理する関数のコードを記載します。

リスト 25. deleteGroup 関数
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();
                }
            });
        }
    });
}

上記のコードには説明しておかなければならない新しい事実がありますが、とりあえず、この関数とよく似た deleteContact 関数を記載してから、両方の関数について相前後して説明します。

リスト 26. deleteContact 関数
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();
                }
            });
        }
    });
}

以上の 2 つの関数は、どちらも新しい確認ダイアログ関数を使用してユーザーに削除の確認を求めます。グループを削除すると、そのグループに含まれるすべての連絡先も同時に削除されるため、ユーザーがグループを削除しようとすると、そこに含まれる連絡先も削除されることになるという警告が出されます。それに対してユーザーが確認すると、関数は削除対象に応じてグループのツリーまたは連絡先グリッドのいずれかに進み、削除する必要がある項目の詳細を取得します。続いて Ajax の dojo.xhrPost 関数を呼び出すことで、該当するサーバー・サイドの PHP API が非同期で呼び出されます。この呼び出しの結果として受け取った JSON レスポンスが構文解析されて、成功またはエラーのいずれか該当するメッセージを表示するために使用されます。

最後に、これらの関数を対応するイベント (つまり、メニュー項目) に関連付ける必要があります。そのためのコードは、リスト 27 のとおりです。

リスト 27. メニュー項目を削除関連の関数に関連付ける
dojo.connect(mnuDeleteContact, "onClick", null, deleteContact);
dojo.connect(ctxMnuDeleteContact, "onClick", null, deleteContact);
dojo.connect(mnuDeleteGroup, "onClick", null, deleteGroup);
dojo.connect(ctxMnuDeleteGroup, "onClick", null, deleteGroup);

これで、アプリケーションを保存してリロードすると、連絡先を削除しようとした場合に、図 9 に示すメッセージが表示されることを確認してください。

図 9. 削除の確認ダイアログ・ボックス
削除の確認ダイアログ・ボックス

「OK」をクリックすると、連絡先の削除操作が実際に行われ、以下のメッセージが表示されます (図 10 を参照)。

図 10. 削除の成功メッセージ
連絡先が正常に削除されたことを伝えるメッセージ・ボックス
連絡先が正常に削除されたことを伝えるメッセージ・ボックス

上記のメッセージが表示されるだけでなく、該当する連絡先がグリッドから削除されていることにもお気付きのことと思います。グループを削除する場合にも同じような動作が行われます (ただし、グループを削除すると、そのグループに含まれるすべての連絡先も削除されることになります)。

Ajax を使用した、ダイアログ・ボックスのデータの保存

アプリケーションに追加しなければならない関数は残すところ、グループおよび連絡先の新規作成、グループの名前変更、そして既存の連絡先の編集/移動を行うダイアログ・ボックスを実装する関数だけとなりました。これから、これらの関数を実装してアプリケーションを仕上げます。

前のセクションで説明した削除関数の実装方法では、dojo.xhrPost 関数を使用して、Ajax で PHP スクリプトを呼び出しました。他の操作を実行する場合にも同じような関数の呼び出しを追加することになりますが、削除以外の操作では、フォームの送信アクションを利用してフォームのデフォルト機能を無効にするという点が異なります。

グループの追加および名前変更の場合

グループの追加および名前変更から取り掛かりましょう。script.js ファイルに、リスト 28 に記載する 2 つの関数を追加してください。

リスト 28. グループの追加および名前変更を行うための関数
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();
            }
        });
    }
}

上記の 2 つの関数は、ユーザーが「New Group (新規グループ)」または「Rename Group (グループの名前変更)」フォームを送信しようとすると呼び出されます。呼び出された関数は、デフォルトの送信アクションを無効にし、フラグを更新することで、Ajax を使用してフォームが送信されており、JSON レスポンスが受信されるはずであることを示します。次に、フォームの action 属性に指定された URL に対して XHR POST が実行されます。そしてレスポンスを受信すると、そのレスポンスに応じてツリーが更新され、成功を伝えるダイアログ・メッセージが表示されるという仕組みです。これらの関数をテストするには、まず、フォームに関数を関連付けなければなりません。そのためのコードをリスト 29 に記載します。

リスト 29. フォームの送信アクションを関数に関連付ける
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();
});

フォームの値は、ダイアログ・ボックスが表示されるとリセットされます。また、ダイアログ・ボックスで「Cancel (取り消し)」をクリックすると、ダイアログ・ボックスが画面から消えます。以上で、このアプリケーションでグループを追加することも、グループの名前を変更することも可能になりました。

連絡先の追加、編集、移動の場合

連絡先の追加と編集は、実際には同じダイアログで行います。新規連絡先と既存の連絡先を区別するための関数はすでに用意したので、追加/編集操作を処理するために必要な関数は、あと 1 つだけです。ここでは、この関数の他に連絡先のグループ間での移動を処理する関数も作成します。リスト 30 に、これらの関数を定義します。

リスト 30. 連絡先を追加、編集、移動するための関数
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();
            }
        });
    }
}

上記の関数は、それぞれに対応する「グループ」関数と同じように機能するため、あまり詳しく説明しませんが、これらの関数はツリーを更新するのではなく、グリッド・コントロールを非同期でリロードします。また、doEditContact 関数には追加のロジックがあることにも注意してください。これは、ユーザーがフォームで新規連絡先を追加したのか、それとも既存の連絡先を編集したのかを判別して、正しいメッセージをユーザーに表示するためのロジックです。

最後に必要なコードは、これらの関数をそれぞれに対応するフォームに関連付けるためのものです。そのためのコードをリスト 31 に記載します。

リスト 31. 連絡先の追加、編集、移動のアクションを関連付ける
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();
});

「Move Contact (連絡先の移動)」ダイアログは、表示される際にグループのドロップダウン・リストにデータ・ストアからグループのデータをリロードすることで、新しく追加されたグループや名前変更されたグループがリストに含まれるようにし、削除されたグループは含まれないようにします。上記のコードを追加した後、アプリケーションを保存してブラウザーにロードすると、このコードの素晴らしい動作を確認することができます。

改善案

このチュートリアルで作成したサンプル・アプリケーションは完全に機能するとは言え、さらにユーザー・エクスペリエンスを向上させて、より一層見事なアプリケーションにするために追加できる機能は他にも多数あります。残念ながら、このチュートリアルではこれらの機能を追加するだけの余裕はありませんでしたが、自分で追加するとしても比較的簡単に追加できるはずです。以下に、考えられる改善内容の一部を記載します。

  • グループにサブグループを追加できるようにする
  • 連絡先を複数のグループに追加できるようにする
  • ドラッグ・アンド・ドロップ操作による連絡先の移動
  • ドラッグ・アンド・ドロップ操作によるグループの並び替え
  • 柔軟な連絡先フィールド (個々の連絡先にユーザー独自のフィールドを追加できるようにする)
  • 連絡先のインライン編集
  • グリッドのインライン編集
  • 複数行を同時に選択、削除できるようにする
  • 削除した項目の「ごみ箱」機能を追加し、復元およびドラッグ・アンド・ドロップ操作による削除を可能にする
  • Dojo および PHP のコードをオブジェクト指向で実装し、コードを簡潔にする
  • グループを削除するときに、ユーザーが削除するグループの連絡先を別のグループに移動できるようにする
  • PHP スクリプトで MySQLi を使用してセキュリティーを強化する
  • 複雑な検証ルールを追加する
  • ログイン・システムを追加して複数のユーザーがログインできるようにする

まとめ

このチュートリアルで重点を置いたのは、連絡先を管理できるようにする機能豊富なサンプル・アプリケーションの作成方法です。Dojo ウィジェットを使ってユーザー・インターフェースを作成する方法から、一連の PHP API スクリプトに対する XHR/Ajax 呼び出しによって MySQL データベースと通信する方法まで、ステップ・バイ・ステップで手順を紹介しました。このチュートリアルで紹介したサンプル・アプリケーションに改良を加えて、独自の複合 Dojo アプリケーションを作成することで、Dojo Toolkit を利用して見栄えの良い RIA を作成するための幸先良いスタートが切れるはずです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=788484
ArticleTitle=Dojo Toolkit を使って Ajax アプリケーションを構築する
publish-date=01272012