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で「こんな書き方もあるのかな?」と言う意味で使ってみた。もう少し考える余地がありそうなので、その時は追記するようにする。


  • トラックバック 停止中
  • コメント (5)
    • yoshi
    • 2010年 12月9日 8:44pm

    cakephpを使用しています。
    セレクトボックスを連動して該当する項目のみ表示させるようにしたいのですがどうやればいいか教えていただけないでしょうか?

      肉
    野菜

    ロース
    カルビ
    タン
    モヤシ
    なす
    カツオ

    • yoshi
    • 2010年 12月9日 8:46pm

    cakephpを使用しています。
    セレクトボックスを連動して該当する項目のみ表示させるようにしたいのですがどうやればいいか教えていただけないでしょうか?

     肉
     野菜
     魚

     ロース
     カルビ
     タン
     モヤシ
     なす
     カツオ

    肉を選択したら下記のように表示されるようにしたいのですがどう実装すればいいか教えていただけないでしょうか?

     ロース
     カルビ
     タン

  1. @yoshi さん、こんにちは。
    Ajaxを使って簡単に動作するサンプルを作ってみました。CakePHP側はSampleコントローラーとそのビューだけです。JavaScriptはjQueryを利用しています。

    プログラムは長くありませんので、分からないコマンドは調べていただきながら読み解いてみてください。

    【CakePHP Sampleコントローラー】

    <?php
    class SampleController extends AppController {
    
    	var $name = 'Sample';
    	var $uses = array();
    	var $components = array('RequestHandler');
    
    	function index() {
    	}
    
    	function get_option($sort='') {
    		$ingredients = array(
    			'meat' => array('ロース', 'カルビ', 'タン'),
    			'green' => array('もやし', 'なす'),
    			'fish' => array('カツオ'),
    		);
    
    		if (!$this->RequestHandler->isAjax()) {
    			die('Not found');
    		}
    
    		if (!isset($ingredients[$sort])) {
    			$sort = 'meat';
    		}
    
    		configure::write('debug', 0);
    		$this->set('ingredients', $ingredients[$sort]);
    	}
    }
    

    【indexビュー】

    <?php
    $html->script(array('jquery-1.4.4.min'), array('inline'=>false));
    $html->scriptBlock( <<<JSPRG
    
    jQuery(document).ready(function($) {
    	$("#sort").change(function() {
    		$.get('sample/get_option/' + $(this).val(), function(data) {
    			$("#ingredient").html(data);
    		});
    	});
    
    });
    
    JSPRG
    , array('inline' => false));
    
    ?>
    
    <select id="sort">
    	<option value="meat">肉</option>
    	<option value="green">野菜</option>
    	<option value="fish">魚</option>
    </select>
    
    <select id="ingredient">
    	<option value="ロース">ロース</option>
    	<option value="カルビ">カルビ</option>
    	<option value="タン">タン</option>
    </select>
    

    【get_optionビュー】

    <?php foreach ($ingredients as $ingredient) : ?>
    <option value="<?php echo $ingredient ?>"><?php echo $ingredient ?></option>
    <?php endforeach; ?>
    
    • ぴら
    • 2011年 12月18日 3:44pm

    Selectメニューの連動表示について、ずっと悩んでいたのですが、こちらの記事が大変参考になり、やっと解決しました!どうも有り難うございます!

  2. @ぴら さん、はじめまして。
    拙い記事がお役に立ったとの事、嬉しく思います。
    ご丁寧にお礼のコメントを下さりありがとうございました。

コメント 停止中