symfonyのDoubleList実装用ウィジェットを使ってみる

こんにちは。松田です。

今日はsymfonyで簡単にDoubleListのフォームを実装する、sfWidgetFormSelectDoubleListを使ってみます。
DoubleListとは、左右に表示されるセレクトボックス間で要素を移動させることで必要な要素だけを絞り込むための仕組みです。
symfonyにこれをまるごと作成できるプラグインが用意されているため、これを使用して作成します。

これを使えば下の画像のようなフォームを一瞬で作成してくれます。

DoubleListを作成するには、sfFormExtraPluginを導入する必要があります。
下記のURLからダウンロードし、プロジェクト直下のpluginsディレクトリに設置しておきます。
http://www.symfony-project.orstsg/plugins/sfFormExtraPlugin

次にプラグインを有効にするため、config/ProjectConfiguration.class.php を編集します。

// config/ProjectConfiguration.class.php


<?php
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins('sfDoctrinePlugin');
    $this->enablePlugins('sfFormExtraPlugin'); // これを追加
  }
・・・

これで下準備は完了です。

次に、DoubleListを使用するためのFormを用意します。
DoubleListを使用するには、sfWidgetFormDoctrineChoiceの"renderer_class"オプションに sfWidgetFormSelectDoubleList を指定します。
modelには表示したいテーブルのモデル名を記述してください。

// TestClassForm.class.php


<?php 
class TestForm extends sfForm
{
  public function configure()
  {
    $this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
      'model'            => 'Staff', // リストに表示するモデルの名前
      'renderer_class'   => 'sfWidgetFormSelectDoubleList',
    )); 
  }
}

このFormをactionで呼び出し、テンプレートで描画します。
// actions.class.php


<?php
(略)
  public function executeDoubleList(sfWebRequest $request)
  {
    $this->form = new TestForm();
  }

// doubleListSuccess.php


<div style="margin: 100px 100px 100px 100px;">
  <form>
    <?php echo $form['staff_list'] ?>
  </form>
</div>

これで描画は出来るのですが、sfWidgetFormSelectDoubleListを使用するには専用のJavaScriptを読み込んでおく必要があります。
sfWidgetクラスを継承しているwidgetには、getJavascripts()メソッドが実装されており、これを使用するとそれぞれのwidgetFormで必要なjavascriptを知ることができるようになっています。

さらに、use_javascripts_for_form ヘルパーを使えば、formのgetJavascripts()で取得できるjsをすべて読み込んでくれるので、これを使ってしまいましょう。

// doubleListSuccess.php


<?php use_javascripts_for_form($form) ?>  
<div style="margin: 100px 100px 100px 100px;">
  <form>
    <?php echo $form['staff_list'] ?>
  </form>
</div>

sfWidgetFormSelectDoubleListでは、"/sfFormExtraPlugin/js/double_list.js" が必要となっています。
実際には /plugins/sfFormExtraPlugin/web/js に存在するファイルとなるはずなので、web以下にsfFormExtraPluginへのシンボリックリンクを貼るなどして対応してください。

これで設置完了です。
表示すると画像の状態になっているはずです。

セレクトボックス内の要素を選択し中央の矢印をクリックすると、選択した要素を左右に移動させることができるようになっています。

表示するだけであればこれだけで完成ですが、せっかくなので少しカスタマイズしてみます。
基本的にはsfWidgetFormDoctrineChoiceに渡すオプションを変更することでほとんどが対応可能です。

ぱっと見あまり変わってませんが、最終的に次の画像のような状態を目指します。

■候補リストに表示されるオブジェクトをクエリで選択する
今のままではStaffが全員表示されてしまうので、表示したい人だけ表示するように変更します。
具体的にはDoctrine_Queryを渡すことでクエリの結果のStaffだけを表示することができます。

例えば、下記のように記述すれば、TestFormのコンストラクタで渡したユーザーを除外してリストを表示できます。


<?php
(略)
  $query = StaffTable::getInstance()->createQuery();
  $default_id_list = $this->getDefaults();
  if (count($default_id_list)) {
    $query = $query->andWhereNotIn('id', $default_id_list);
  }
  $this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
    'model'            => 'Staff',
    'query'            => $query,  // これを追加
    'renderer_class'   => 'sfWidgetFormSelectDoubleList',
  ));

■リスト上部の文字列を変更する
renderer_optionsに "label_unassociated" "label_associated" を渡す。


<?php
(略)
$this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
  'model'            => 'Staff',
  'renderer_class'   => 'sfWidgetFormSelectDoubleList',
  'renderer_options' => array(
    'label_unassociated' => '【候補一覧】',              // これと
    'label_associated'   => '【選択しているスタッフ】',  // これ追加
  ),
));

■リストを左右入れ替える
初期状態では左が選択済みリスト、右が候補リストになっていますがこれを逆にします。
renderer_optionsの"associated_first"にfalseを渡すことで可能です。


<?php
(略)
$this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
  'model'            => 'Staff',
  'renderer_class'   => 'sfWidgetFormSelectDoubleList',
  'renderer_options' => array(
    'associated_first' => false,  // これ
    'label_unassociated' => '【候補一覧】',
    'label_associated'   => '【選択しているスタッフ】',
  ),
));

■中央の矢印ボタンをボタンタグに変える
renderer_optionsの associate, unassociateにタグを直接記入します。


<?php
(略)
$this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
  'model'            => 'Staff',
  'renderer_class'   => 'sfWidgetFormSelectDoubleList',
  'renderer_options' => array(
    'associated_first' => false,
    'label_unassociated' => '【候補一覧】',
    'label_associated'   => '【選択しているスタッフ】',
    'associate'   => '<input type="button" value="追加する → " />',  // これと
    'unassociate' => '<input type="button" value=" ← 外す "  />',  // これね
  ),
));

■リストをTableタグで描画する
このDoubleList、divタグとfloatを駆使してデザインされているため、IE6ではしっかりと崩れます。
どーーしてもIE6に対応したい!しなきゃいけないんだよちくしょう!という人は、tableタグで無理やり対応しちゃいましょう。
renderer_optionsの template オプションを使用して実装します。


<?php
(略)
$this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
  'model'            => 'Staff',
  'renderer_class'   => 'sfWidgetFormSelectDoubleList',
  'renderer_options' => array(
    'associated_first' => false, // template指定すると無意味。。
    'label_unassociated' => '【候補一覧】',
    'label_associated'   => '【選択しているスタッフ】',
    'associate'   => '<input type="button" value="追加する → " />',
    'unassociate' => '<input type="button" value=" ← 外す "  />',
// ↓ここから
    'template' => <<<STR
<table width="50%" border="0" cellpadding="0" cellspacing="0">
  <tr valign="top">
    <td width="200" align="center" style="white-space:nowrap;">%label_unassociated%</td>
    <td align="center"> &nbsp;</td>
    <td width="200" align="center" style="white-space:nowrap;">%label_associated%</td>
  </tr>
  <tr>
    <td align="center" valign="top">%unassociated%</td>
    <td align="center">
      %associate%
      
      
      
      %unassociate%
    </td>
    <td align="center" valign="top">
      %associated%
    </td>
  </tr>
</table>
<script type="text/javascript">
  sfDoubleList.init(document.getElementById('%id%'), '%class_select%');
</script>
STR
// ↑ここまで追加っ
  ),
));

%から%で囲まれた文字列が必要な要素に置き換えられます。
何が何に置き換えられるかはsfWidgetFormSelectDoubleListのソースを読んでみましょう。

■初期状態で選択されている要素を設定する
編集画面等、すでに設定されている要素を選択済リストに入れて表示させたいことがあります。
オプションで設定できてもいいような気もするのですが、やり方がわからなかったのでjsで作ってみました。
長くなってきたので省略しつつ書いて行きます。

まずはセレクトボックスにclassを指定し、jsから指定できるようにしておきます。
これで候補リストのセレクトボックスのclassが「staff-list」、選択済みリストのclassが「staff-list-selected」となります。

// TestForm.class.php


<?php
(略)
  public function configure()
  {
    $this->widgetSchema['staff_list'] = new sfWidgetFormDoctrineChoice(array(
      'model'            => 'Staff',
      'renderer_class'   => 'sfWidgetFormSelectDoubleList',
      'renderer_options' => array(
       'class_select' => 'staff-list', // jsから指定するためのclassを追加
(略)
  }
  // use_javascripts_for_formで読み込まれるjsファイルを追加
  public function getJavaScripts()
  {
    $javascripts = parent::getJavaScripts();
    $javascripts = array_merge($javascripts, array(
      'jquery.js',
      'show_default.js.php?id_list=' . $this->id_list, // ここはうまいこと実装してね
    ));  
    return $javascripts;
  }
}

次に、jqueryファイルと下記のjsファイルを読み込ませます。
上記のようにgetJavaScripts()を実装することで勝手に読み込まれるファイルが増えるはずです。
あとはshow_default.js.phpに初期表示のidリストを渡し、なんやかんやしてshow_default.js.phpのdefault_id_listを作成してください。

// show_default.js.php jsファイルをphpで出力するスクリプト


<?php
// $_GETでうけとったデータからIDの配列を作る
$default_id_list = 略
?>
$(function() {
  var default_id_list = <?php echo $default_id_list ?>;
  if (default_id_list.length) {
    $.getJSON(
      "/staff/get_default_json",  // 初期表示のスタッフをJSON形式で出力するURL
      { default_id_list: default_id_list },
      showSelectboxByJson
    );
  }
});
// 受け取ったjsonデータからoption要素を作成
// [{"id": "1", "name": "なまえ"}, ... ] ←こんなJSONデータを想定
function showSelectboxByJson(json)
{
  var options = '';
  var list = json.list ? json.list : [];
  for (var i = 0; i < list.length; i++) {
    options += '<option value="' + list[i].id + '">' + list[i].name + '</option>';
  }
  $(".staff-list-selected").html(options);
}

これで/staff/get_default_json部分もちゃんと作られていれば、初期選択されている画面が作成できるはず。

基本実装はとっても簡単なので、ぜひ機会があれば使ってみてください。