jQueryのAjaxでSelect入替

アプリケーションプログラムの制作でどこに多くの時間を費やすか、と言ったら、ユーザインタフェースである、と答える人は自分も含めて多いと思う。 スタンドアロンからWebアプリケーションになってもそれは変わらず、リッチクライアントとか呼ばれて、「なかなか楽にならんなぁ」と思う限りだ。それで も、CakePHPを利用することで開発スピードは劇的に上がっている、と感じている。

さて、データを投入する際に少しでも利用者の負担を減らすため、セレクト要素の選択機能は極めてよく使う。そうすると次に、グループを選択することでグループに属する集合を変更したくなる。

ここでは表題通りで、グループを会社の部署やプロジェクトチームとし、グループを選択した後、そのグループに所属するユーザを選択する際に、Ajaxを利用してユーザの集合を入替える機能を実現する。

データについて

グループテーブルを「groups」とし、ユーザテーブルを「users」とする。グループとユーザの関係は多対多(hasAndBelongsToMany)として、「groups_users」テーブルを用意する。

CREATE TABLE `groups` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) default '',
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
)

CREATE TABLE `users` (
  `id` int(11) NOT NULL auto_increment,
  `fdel` tinyint(2) default '0',
  `name` varchar(255) NOT NULL default '',
  `created` datetime default NULL,
  `modified` datetime default NULL,
  PRIMARY KEY  (`id`)
)

CREATE TABLE `groups_users` (
  `id` int(11) NOT NULL auto_increment,
  `group_id` int(11) default '0',
  `user_id` int(11) default '0',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `group_user_key` (`group_id`,`user_id`)
)

ユーザテーブルの「fdel」は、ユーザ削除で直接データを削除せず、この値を「-1」にセットすることとする。

モデルやコントローラ、ビューの作成は「bake」を利用する。「bake」を実行する際、カレントディレクトリを「APP_DIR」で行うが、 「cake.bat」コマンドにパスを通しておく必要がある。パスの通し方は、カレントを「ROOT\cake\console」にした後、

>set path=%path%;%cd%

と実行すればよい(sdozonoさんのページ)。なお、Windows上でのコマンド実行である。

動作のアルゴリズム

実行手順を考えて見よう。そして、実行を実現する機能を拾い出す。

  1. グループを表示するセレクトボックスから、グループを選択する
  2. 選択のイベントで、Ajaxを利用して「/users/userlist/グループID」を実行
  3. userlistアクションで、そのグループに所属するユーザを拾い出す
  4. userlistのビューで、ユーザリストを<option>タグを利用したHTMLで戻す
  5. <select>タグの子を、受信した<option>のリストで入替える

セレクトボックスのデータ

セレクトボックスのデータとフォームデータはbakeが作ってくれるが、コントローラのソースを見ると

$groups = $this->Group->find('list');
$this->set(compact('groups'));

として、リストデータを検索し、ビューに渡している。ビューでは、

<?php echo $form->input('User.group_id', array('options'=>$groups)); ?>

とすれば、

<div><label for="UserGroupId">Group</label>
<select name="data[User][group_id]" id="UserGroupId">
<option value="1">C++</option>
<option value="2">C#</option>
<option value="3">BASIC</option>
</select></div>

のように、HTMLを吐き出してくれる。’User.group_id’は、便宜上セットした。

CakePHPは、慣れていないと分かりにくい機能がいくつもある。ここでは、セレクトボックスに利用するデータ検索だ。モデルクラスのメソッド 「find(‘list’)」を利用すると、検索結果のデータ配列として、「array(id=>name,…)」を戻してくる。

jQueryを使ってデータの入替えをする

jQueryを利用すると、わずか数行で機能を実現できる。日本でも書籍が出版されたようなので、JavaScriptも身近になると思う。

さて、jQueryを利用するためライブラリを読込むのだが、多くの人は<body>より<head>部分で読み込んだ り、プログラムを書き込んだりすると思う。ところでCakePHPを利用する場合、layoutビューに<head>部分を納めて共通で利用 することが多い。この<head>部分に動的にJavaScriptを読み込んだり、JavaScriptプログラムを書き込んだりするには どうするか?ここでは、ビュー(*.ctp)の中からそれらを行う方法を示す。

1.準備
layoutビューの<head>部分に

<?php echo $scripts_for_layout; ?>

を含めておく。コントローラでは、「Javascript」ヘルパーを追加する。

var $helpers = array('Html', 'Form', 'Javascript');

2.ビュー(*.ctp)でjQueryの読込みとプログラムを書き込む
jQueryのパックされたライブラリを、サイトに「/js/jquery.js」としてアップロードしておき、セレクトボックスのあるビューの先頭に

<?php $javascript->link('jquery', false); ?>

と指定する。

次に、グループのセレクトボックス(id=”UserGroupId”)から選択されたイベントを拾って、グループのid値を取り出し、HTTP GETメソッドのAjaxで「/users/ulserlist」へid値を渡す。受信したユーザリストでユーザ選択のセレクトボックス (id=”PrjUserId”)を入替える。

<?php $this->addScript($javascript->codeBlock( <<<JSPROG
$(document).ready(function() {
  $("#UserGroupId").change(function() {
    $.get("/users/userlist/"+$(this).val(), function(data) {
      $("#PrjUserId").html(data);
    });
  });
});
JSPROG
)) ?>

何と、プログラム実質3行!。「あっと驚くタメゴロー」(古)だ(^_^;)。

jQueryの読込みもプログラムも、<head>部の「$scripts_for_layout」へ出力されるので確認してみて下さい。

リクエストヘッダーのAcceptは、上述の利用では「*/*」であるが、jQuery.get()の4番目のパラメータを「’xml’」などと指定することで変更できた。

サーバサイドからユーザリストを返す

サーバサイド側のプログラムで気を付ける点として、Ajax以外を受け付けるかどうするか?であろう。ここでは、ブラウザから普通に要求された場合は、404エラーを出力することにする。

アクションでAjaxによるリクエストか確認するため、コントローラに「RequestHandler」コンポーネントを指定しておく。

var $components = array('RequestHandler');

アクションプログラムで、Ajax通信の判定処理をする。

function userlist($gid=0) {
  if (!$this->RequestHandler->isAjax()) {
    $this->cakeError('error404');
  }

「groups_users」から指定のグループIDを持つユーザを検索し、idとnameを取得する。この部分の検索方法は、いくつか試してみた ので次の節で述べたい。ここでは以下の方法で行う。検索条件として、ユーザテーブルの「fdel」値が「-1」のデータは、削除扱いとして検索結果から除 外する。

  $this->User->recursive = 0;
  $users = $this->User->find('all', array(
    'conditions' => array(
      'fdel >=' => 0,
      'id' => $this->User->GroupsUser->find('list', array(
                'conditions' => array('group_id' => intval($gid)),
                'fields' => 'user_id'))),
      'fields' => array('User.id', 'User.name')));

  Configure::write('debug', 0);
  $this->layout = 'ajax';
  $this->set('users', $users);

ビューに渡す前に、デバッグ機能によるメッセージ出力とlayoutビューの出力を抑止しておく。次のビュー(userlist.ctp)では、ユーザリストを<option>の固まりで出力する。

<?php foreach($users as $usr) : ?>
  <option value="<?php echo $usr['User']['id'] . "\">"
    . $usr['User']['name'];?></option>
<?php endforeach; ?>

ところで、出力時のレスポンスヘッダーのContent-Typeであるが、この例では「text/html」になっている。 RequestHandlerのいくつかのメソッドを実行し、その他のタイプを試みたが特に変更できなかった(Firebug使用)。何か段取りがあると 思うので、機会があれば調べてみたいと思う。なお、RequestHandlerのメソッドを利用すると、layoutをajaxにセットしなくても自動 的に変更してくれるため、同じ結果を得ることができた。

テーブルの検索について

RDBMSやSQLについての浅薄さを曝け出すようで恥ずかしいのだが、もしかするとネ申からの啓示があるかも、と思ったのでちょっとだけ。

指定したグループのユーザを検索する際、例えばSQLで副照会(副問合わせ)を使って、

$qstr = "SELECT User.id,User.name FROM users AS User WHERE User.id IN "
 . "(SELECT GroupsUser.user_id FROM groups_users AS GroupsUser "
 . "WHERE GroupsUser.group_id=" . intval($gid)
 . ") AND User.fdel>=0;";
$users = $this->User->query($qstr);

と実行すると、1回のクエリ-で希望の結果が得られる。あるいは、

$qstr = "SELECT User.id,User.name FROM users AS User "
 . "JOIN groups_users AS GroupsUser ON (User.id=GroupsUser.user_id) "
 . "WHERE GroupsUser.group_id=3 AND User.fdel>=0;";
$users = $this->User->query($qstr);

でも1回のクエリ-である。実は、最初に検索したとき次のようにした。

$this->User->GroupsUser->unbindModel(array('belongsTo'=>array('Group')));
$users = $this->User->GroupsUser->find('all', array(
 'conditions'=>array('group_id'=>intval($gid)),
 'fields'=>array('User.id', 'User.name')));

ただし、groups_usersのモデル定義で「belongsTo」の「User」テーブルとの関係条件として、「conditions」に 「array(‘fdel >=’, 0)」と指定している。このため、「fdel=-1」のデータを空データとして検索結果に含めてしまう。

ビューで空データをはじき出すようにすれば、この方法も1回のクエリ-で検索結果を得られる。

前節の方法ではクエリ-を2回実行する。今回は、CakePHPで「こんな書き方もあるのかな?」と言う意味で使ってみた。もう少し考える余地がありそうなので、その時は追記するようにする。