DrupalのAHAHフォーム作成
Posted by Kohei Hayashi on
AHAH (Asynchronous HTML and HTTP)はAJAXの一種ですが、DrupalでAHAHと言うとほとんどの場合Form APIで作成したフォームの要素をリロード無しで更新することを指します。一度把握してしまえば簡単なので、まとめてみました。
まず一番重要なことは、Drupalのフォームはサーバー側にキャッシュされ、いかなる時でもサーバー側のフォームとクライアント側に表示されているフォームは同一でなければならない、ということです。この法則を守らないでクライアント側のフォームだけjavascriptで更新しようとすると、不正にフォームが変更されたとみなされ、送信の際にvalidationに引っかかるので注意が必要です。
以上の点を理解した上で、実際のプログラミングの手順は以下のようになります。
1. Form APIでフォームを定義する際にどの要素でAHAHを使用するかを#ahahキーで指定します。また、ブラウザーからの送信内容によってフォーム定義が動的に変化するようにしておきます。
2. Drupalがフォームを作成、サーバーにキャッシュし、ブラウザーに表示します。
3. 1で指定した要素(例:セレクトボタン)でAHAHによるHTTPリクエストを送信し、Drupalのメニュー関数を通じてメニューコールバックにアクセスします。
4. サーバーでキャッシュされていたフォームを呼び出します。
5. drupal_process_formでフォームをプロセスします。下記の例では使用しませんが、この際にvalidateハンドラーとsubmitハンドラーもコールバックされるので、$form_stateの内容を変更することが可能です。
6. drupal_rebuild_formによりフォームを再構築します。この際、$_POSTは破棄されるので、submitハンドラーが再びコールバックされることはありません。また、フォーム再構築の際は$form_stateによりフォーム定義が変化し、
再構築されたフォームでサーバーのキャッシュが更新されます。
7. この時点ではサーバーのキャッシュとクライアントに表示されているフォームが同一でないため、drupal_renderで差異のある部分のみをレンダリングします。
8. ブラウザーがデータをjsonで受け取り、フォームの必要な部分のみが自動的に更新されます。これで、再びサーバーキャッシュ=クライアント側のフォーム、という公式が成り立ちます。
例として、料理の種類を選ぶとメニューが変わるというフォームを作成してみましたので、ご参考まで。
<?php
/**
* Implementation of hook_menu().
*/
function ahahtest_menu() {
$items['ahahtest'] = array(
'title' => 'AHAH test',
'page callback' => 'drupal_get_form',
'page arguments' => array('ahahtest_form'),
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
//3. AHAHによるHTTPリクエスト用のマッピング
$items['ahahtest/js'] = array(
'title' => 'AHAH js callback',
'page callback' => 'ahahtest_js',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* フォーム定義。
*/
function ahahtest_form($form_state) {
$cuisines = array(
'chinese' => '中華',
'italian' => 'イタリアン',
'american' => 'アメリカン',
);
//1. AHAHをバインドする
$form['cuisine'] = array(
'#type' => 'select',
'#title' => '料理のカテゴリ',
'#options' => $cuisines,
'#ahah' => array(
'path' => 'ahahtest/js',
'wrapper' => 'ahah-menus',
),
);
//デフォルトのメニュー
$menus = array('ラーメン', '餃子', '麻婆豆腐');
//AHAHでフォーム再構築する際に使用。
if (isset($form_state['values']['cuisine'])) {
if ($form_state['values']['cuisine'] == 'italian') {
$menus = array('ピザ', 'パスタ', 'リゾット');
}
elseif ($form_state['values']['cuisine'] == 'american') {
$menus = array('ハンバーガー', 'フライドポテト', 'ステーキ');
}
}
//AHAHのターゲットID。wrapperの内側にあるHTMLが更新されます。
$form['wrapper'] = array(
'#prefix' => '<div id="ahah-menus" style="clear:both">',
'#suffix' => '</div>',
);
$form['wrapper']['menus'] = array(
'#title' => 'メニュー',
'#type' => 'select',
'#options' => $menus,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => '送信',
);
return $form;
}
/**
* AHAH menu callback.
*/
function ahahtest_js() {
$form_state = array('storage' => NULL, 'submitted' => FALSE);
$form_build_id = $_POST['form_build_id'];
//4. サーバーキャッシュからフォーム定義を呼び出す。
$form = form_get_cache($form_build_id, $form_state);
$args = $form['#parameters'];
$form_id = array_shift($args);
$form_state['post'] = $form['#post'] = $_POST;
$form['#programmed'] = $form['#redirect'] = FALSE;
//5. フォームをプロセス。
drupal_process_form($form_id, $form, $form_state);
//6. フォームを再構築。$form['wrapper']['menus']が送信内容に応じて変化。
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
//7. 変化した部分のみ抽出
$output = drupal_render($form['wrapper']['menus']);
//8. jsonでブラウザーにデータを送信。
drupal_json(array('status' => True, 'data' => $output));
}