Issue solved, please see the 3rd comment below for an example of making it all work.

Thanks to Wim Leers and Kat Bailey, and alot of sites that were talking about AHAH. I also thank my brain for staying in my head while I beat it into submission around the whole concept of AHAH in Drupal 6.

Comments

katbailey’s picture

You need to first of all make sure that your form will react to $form_state, i.e. that it will always get built according to what's in $form_state. What this means in your particular case is that in your form function when you are building your rooms dropdown, you first check to see if you have a selected location id and populate it based on that. So you'll call a function to look up the rooms for the selected location and that's where your SQL query will go, NOT in your ahah callback. That way, when your ahah callback renders the form, it'll do so with an actual value for lid, which your rooms dropdown will then react to. The Quick Tabs module does this for its View displays dropdown so you could have a look there to get a better understanding of how it works.

Predated’s picture

Thanks Kat :)

I actually found your blog post about the updates you made in Quicktabs where you discussed all that, I just hadn't posted back about it. I'm pretty sure I have it hammered out at this point, but another project I was working on got it's priority bumped up, and I haven't had a chance to get back to it.

I think I was making AHAH a lot more complicated than it is, and I was getting really frustrated with it. Going back to it after not looking at it for a couple of days made quite a difference. I'll have to post back here when I have it working.

Predated’s picture

I got it working some time ago, and just never got around to posting it back here. But in the interest of saving someone some frustration down the road, there is an example of the bare minimum you'd need down below. Please note that the example code hasn't been tested.

You'll want to have a look at:
Doing AHAH in D6 the right way
Wim Leers' blog post about ahah_helper
Kat Bailey's blog post on the duality of forms

First of all you'll need a menu callback:

function simple_menu() {
  $items['simple/js'] = array(
    'type' => MENU_CALBACK,
    'page callback' => 'simple_ahah_render',
    'access callback' => TRUE,
  );
  return $items;
}

And of course, you'll need a form:

function simple_form($form_state) {  
  $form['content'] = array(
    '#tree' => TRUE,
    '#prefix' => '<div id="simple-select-wrapper">',
    '#suffix' => '</div>',
  );
  $form['content']['simple_select_parent'] = array(
    '#type' => 'select',
    '#default_value' => $form_state['storage']['category']['select_cat'],
    '#options' => simple_get_options(),  // Get some options for this select from the DB
    '#ahah' => array(
      'path' => 'simple/js',
      'wrapper' => 'simple-select-wrapper',
    ),
  );
  
  // Here we react to $form_state, adding content to the form, depending on the
  // submitted value of simple_select_parent, above.
  if ($form_state['storage']['content']['simple_select_parent']) {
    $form['content']['simple_select_child'] = simple_ahah_select($form_state);
  }
  
  return $form;
}

And you'll need to get the options for your select field somehow as well - this function would pull it from a database:

/**
 * Return a FAPI select element whose options are based on values submitted by a
 * separate select field.
 *
 * @param array $form_state 
 *     The submitted values of a form. We're mostly interested in $form_state['storage'] 
 *     which is where all the pertinent information will be.
 * @return array $element
 */
function simple_ahah_select($form_state) {
  $sql = "SELECT id, name FROM {your_db_table} WHERE id = %d";
  
  if ($form_state['storage']['content']['simple_select_parent']) {
    $result = db_query($sql, $form_state['storage']['content']['simple_select_parent']);
    while (($data = db_fetch_object($result)) !== FALSE) {
      $opt[$data->id] = $data->name;
    }
  }
  $element = array(
    '#type' => 'select',
    '#options' => $opt,
  );
  return $element;
}

The magic all happens with your AHAH callback function, in this case, simple_ahah_render:

function simple_ahah_render() {
  $form_state    = array('storage' => NULL, 'submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  
  $form    = form_get_cache($form_build_id, $form_state);
  $args    = $form['#parameters'];
  $form_id = array_shift($args);
  
  $form['#post']       = $_POST;
  $form['#redirect']   = FALSE;
  $form['#programmed'] = FALSE;
  $form_state['post']  = $_POST;
  
  // Prevents _form_builder_ie_cleanup() from incorrectly assigning the
  // first button in the form as the clicked button.
  // Wim Leers' AHAH Helper Module has more in-depth information.
  // @see the ahah_helper project
  $form_state['submitted'] = TRUE;
  
  drupal_process_form($form_id, $form, $form_state);
  
  // Once drupal_rebuild_form is called, we no longer have access to 
  // $form_state['values'], so we merge it into $form_state['storage'].
  if (isset($form_state['values'])) {
    if (!isset($form_state['storage'])) {
      $form_state['storage'] = array();
    }
    $storage = $form_state['storage'];
    $values  = $form_state['values'];
    $form_state['storage'] = array_smart_merge($storage, $values);
  }
  
  // Rebuild the form.
  $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
  
  // IMPORTANT:
  // This is a simple example, and we know what the new form element is called. In a
  // real world scenario, you would need to pass a callback function to this AHAH
  // render function that would return the new content, or get the new content in
  // some other way - this is just a simple example.
  $output = $form['content']['simple_select_child'];
  
  // Get the JS settings so we can merge them.
  $javascript = drupal_add_js(NULL, NULL, 'header');
  $settings = call_user_func_array('array_merge_recursive', $javascript['setting']);
  
  drupal_json(array(
    'status'    => TRUE,
    'data'      => theme('status_messages') . drupal_render($output),
    'settings'  => array('ahah' => $settings['ahah']),
  ));
}


/**
 * Smarter version of array_merge_recursive: overwrites scalar values.
 * 
 * This also came (like a God send) from Wim's AHAH helper module. Really, that's the
 * easiest way to go, and the module works like a charm - but I wanted to get my
 * head around the whole AHAH thing, and maybe you do to, or maybe you can't or don't
 * want to be dependant on a different module.
 * 
 * @see PHP Manual on: array-merge-recursive comment #82976.
 */
function array_smart_merge($array, $override) {
  if (is_array($array) && is_array($override)) {
    foreach ($override as $k => $v) {
      if (isset($array[$k]) && is_array($v) && is_array($array[$k])) {
        $array[$k] = array_smart_merge($array[$k], $v);
      }
      else {
        $array[$k] = $v;
      }
    }
  }
  return $array;
}
javier.ortiz.llerena’s picture

I got it to work,
forget about my last message

Hi,
first of all thank you for helping with your work and sharing with the drupalers,
but for those like me that are more newbies in the durpal world would be very
useful if you could post the entire code of a working example.

I have been looking at this for two days and I couldn't make it work.
Sorry about that.

wheelercreek’s picture

I think my brain did explode, but thanks a ton for writing this up!! Finally I'm not getting "Drupal detected an illegal action" errors.

rgutierrezc’s picture

Thanks for posting this, it's exactly what I was looking for.

By building around your code, I've been able to create the form I was expecting; yet when I submit it it won't use the $form['#redirect'] attribute I've tried setting in pretty much every part of the code; it keeps going back to the form instead. Doesn't make sense since, theoretically, the callback function only reloads the code delimited by the wrapper, but obviously I'm getting something wrong here.

Any ideas on getting around this?

timos’s picture

Hi and thanks a lot for this howto !
I was looking for a solution to populate a list for an autocomplete textfield from a select field and your topic gave me a first step to do that.
I give some precision on http://drupal.org/node/812956 if you'd like to have a look. If you could, i would be very interested by a feedback.
But now, i'll go to check what ahah helper module do, maybe it could be useful ! (But i saw that the usage statistic curve is a little bit special... do you know why there is a very big increase and decrease in only few monthes ?)

Tim Baret

Craz’s picture

I tried to apply your code to my module but as with another example im getting a popup with 'an error occured'

here is the whole code

in matches.module

function matches_menu() {

  $items = array();

  $items['admin/esport'] = array(
    'title' => 'Esport',
    'description' => 'Dedicated esport module',
    'page callback' => 'matches_admin_page',
    'page arguments' => array('matches_admin'),
    'access arguments' => array('access administration pages'),
	'file' => 'matches.list.admin.inc'
   );

  $items['admin/esport/js'] = array(
    'type' => MENU_CALBACK,
    'page callback' => 'matches_ahah_render',
    'access callback' => TRUE,
	'file' => 'matches.list.admin.inc'
  );
  
   $items['admin/esport/matches'] = array(
    'title' => 'Matches',
    'description' => 'Set up and manage your teams results',
    'page callback' => 'matches_admin_page',
    'page arguments' => array('matches_admin'),
    'access arguments' => array('access administration pages'),
	'file' => 'matches.list.admin.inc',
    'type' => MENU_DEFAULT_LOCAL_TASK
   );  
   $items['admin/esport/matches/edit'] = array(
    'title' => 'edit',
    'page callback' => 'matches_admin_edit',
    'access arguments' => array('access administration pages'),
	'file' => 'matches.edit.admin.inc',
    'type' => MENU_CALLBACK
   );
  $items['admin/esport/tournaments'] = array(
    'title' => 'tournaments',
    'description' => 'Set up and manage your teams results',
    'page callback' => 'matches_admin_page_tournaments',
    'access arguments' => array('access administration pages'),
	'file' => 'matches.tournament.list.admin.inc',
    'type' => MENU_LOCAL_TASK
   );
  $items['admin/esport/games'] = array(
    'title' => 'games',
    'description' => 'Set up and manage your games',
    'page callback' => 'matches_admin_page_games',
    'access arguments' => array('access administration pages'),
	'file' => 'matches.games.list.admin.inc',
    'type' => MENU_LOCAL_TASK
   );
  $items['admin/esport/teams'] = array(
    'title' => 'Teams',
    'description' => 'Set up and manage your teams',
    'page callback' => 'matches_teams_admin_page',
    'access arguments' => array('access administration pages'),
	'file' => 'matches.teams.list.admin.inc',
    'type' => MENU_LOCAL_TASK
   );
  $items['matches'] = array(
    'title' => 'Matches full view',
    'page callback' => 'matches_all',
    'access arguments' => array('access matches content'),
    'type' => MENU_CALLBACK
  );
  
   $items['matches/details'] = array(
    'title' => 'Detailed match view',
    'page callback' => 'matches_details',
    'access arguments' => array('access matches content'),
    'type' => MENU_CALLBACK
  );

  return $items;
}

matches.list.admin.inc

/////////////////////////
/////////////////////////
/////MATCHES FORM/////
/////////////////////////
/////////////////////////


function matches_admin($form_state) {
  $form = array();
  
  $query = "SELECT * FROM " . "{teams_entries}";
  $query_result =  db_query($query);
  
  $query2 = "SELECT * FROM " . "{matches_tournaments}";
  $query_result2 =  db_query($query2);
  
  $form['Add_form'] = array(
    '#type' => 'fieldset',
    '#title' => t('Add a new match'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['Add_form']['Opponement'] = array(
    '#type' => 'fieldset',
    '#title' => t('Opponement details'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  while ($matches = db_fetch_object($query_result)) { 
  $teams_names[$matches->tid] = $matches->name;
  }
  $form['Add_form']['Opponement']['home_team'] = array(
	'#type' => 'select',
	'#title' => t('Home team'),
	'#options' => $teams_names,
	'#default_value' => $form_state['Add_form']['Opponement']['home_team'],
	'#description' => t('Select the playing team in this match.'),
	'#ahah' => array(
	  'path' => 'admin/esport/js',
	  'wrapper' => 'simple-select-wrapper',
	), 
  );
	$form['Add_form']['Opponement']['testing'] = array(
	    '#tree' => TRUE,
	    '#prefix' => '<div id="simple-select-wrapper">',
	    '#suffix' => '</div>',
	);
  $form['Add_form']['Opponement']['matches_home_lineup'] = array(
    '#type' => 'textfield',
    '#title' => t('Home team lineup'),
    '#default_value' => '',
    '#size' => 30,
    '#maxlength' => 255,
    '#description' => t("Enter the home team lineup."),
    '#required' => FALSE,
  );
  $form['Add_form']['Opponement']['matches_opponement'] = array(
    '#type' => 'textfield',
    '#title' => t('Opposite team'),
    '#default_value' => '',
    '#size' => 25,
    '#maxlength' => 255,
    '#description' => t("Enter the opposite team."),
    '#required' => TRUE,
  );
  $form['Add_form']['Opponement']['matches_opponement_lineup'] = array(
    '#type' => 'textfield',
    '#title' => t('Opposite team lineup'),
    '#default_value' => '',
    '#size' => 30,
    '#maxlength' => 255,
    '#description' => t("Enter the opposite team lineup."),
    '#required' => FALSE,
  );
  while ($tournaments = db_fetch_object($query_result2)) { 
  $tournaments_names[] = $tournaments->name;
  }
  $form['Add_form']['Opponement']['tournaments'] = array(
    '#type' => 'select',
    '#title' => t('Tournament'),
    '#options' => $tournaments_names,
    '#description' => t('Select the tournament.'),
  );
  $form['Add_form']['Opponement']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Match description'),
    '#cols' => 60,
    '#rows' => 5,
    '#description' => t('Write details about the match.'),
  );
  $form['Add_form']['Map_1'] = array(
    '#type' => 'fieldset',
    '#title' => t('Map 1'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['Add_form']['Map_1']['matches_map_1_SCORE'] = array(
    '#type' => 'textfield',
    '#title' => t('Home score'),
    '#default_value' => '',
    '#size' => 15,
    '#maxlength' => 255,
    '#description' => t("Enter the home score for the first map."),
    '#required' => FALSE,
  );
  $form['Add_form']['Map_1']['matches_map_1_SCORE_VISITOR'] = array(
    '#type' => 'textfield',
    '#title' => t('Visitor score'),
    '#default_value' => '',
    '#size' => 15,
    '#maxlength' => 255,
    '#description' => t("Enter the visitor score for the first map."),
    '#required' => FALSE,
  );
  $form['Add_form']['Map_1']['matches__map_1_MAP_NAME'] = array(
    '#type' => 'textfield',
    '#title' => t('Map name'),
    '#default_value' => '',
    '#size' => 15,
    '#maxlength' => 255,
    '#description' => t("Enter the map name."),
     '#required' => FALSE,
  );
  $form['Add_form']['Map_2'] = array(
    '#type' => 'fieldset',
    '#title' => t('Map 2'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['Add_form']['Map_2']['matches_map_2_SCORE'] = array(
    '#type' => 'textfield',
    '#title' => t('Home score'),
    '#default_value' => '',
    '#size' => 15,
    '#maxlength' => 255,
    '#description' => t("Enter the home score for the second map."),
    '#required' => FALSE,
  );
  $form['Add_form']['Map_2']['matches_map_2_SCORE_VISITOR'] = array(
    '#type' => 'textfield',
    '#title' => t('Visitor score'),
    '#default_value' => '',
    '#size' => 15,
    '#maxlength' => 255,
    '#description' => t("Enter the visitor score for the second map."),
    '#required' => FALSE,
  );
  $form['Add_form']['Map_2']['matches__map_2_MAP_NAME'] = array(
    '#type' => 'textfield',
    '#title' => t('Map name'),
    '#default_value' => '',
    '#size' => 15,
    '#maxlength' => 255,
    '#description' => t("Enter the map name."),
     '#required' => FALSE,
  );
    $form['Add_form']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
	
  if ($form_state['Add_form']['Opponement']['home_team']) {
    $form['Add_form']['Opponement']['testing'] = matches_ahah_select($form_state);
  }
  
  return $form;
}

function matches_ahah_select($form_state) {
  $sql = "SELECT mid, name FROM matches_maps WHERE gid = %d";
  
  if ($form_state['Add_form']['Opponement']['home_team']) {
    $result = db_query($sql, $form_state['Add_form']['Opponement']['home_team']);
    while (($data = db_fetch_object($result)) !== FALSE) {
      $opt[$data->mid] = $data->name;
    }
  }
  $element = array(
    '#type' => 'select',
    '#options' => $opt,
  );
  return $element;
}

function matches_ahah_render() {
  $form_state    = array('storage' => NULL, 'submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  
  $form    = form_get_cache($form_build_id, $form_state);
  $args    = $form['#parameters'];
  $form_id = array_shift($args);
  
  $form['#post']       = $_POST;
  $form['#redirect']   = FALSE;
  $form['#programmed'] = FALSE;
  $form_state['post']  = $_POST;
  
  // Prevents _form_builder_ie_cleanup() from incorrectly assigning the
  // first button in the form as the clicked button.
  // Wim Leers' AHAH Helper Module has more in-depth information.
  // @see the ahah_helper project
  $form_state['submitted'] = TRUE;
  
  drupal_process_form($form_id, $form, $form_state);
  
  // Once drupal_rebuild_form is called, we no longer have access to 
  // $form_state['values'], so we merge it into $form_state['storage'].
  if (isset($form_state['values'])) {
    if (!isset($form_state['storage'])) {
      $form_state['storage'] = array();
    }
    $storage = $form_state['storage'];
    $values  = $form_state['values'];
    $form_state['storage'] = array_smart_merge($storage, $values);
  }
  
  // Rebuild the form.
  $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
  
  // IMPORTANT:
  // This is a simple example, and we know what the new form element is called. In a
  // real world scenario, you would need to pass a callback function to this AHAH
  // render function that would return the new content, or get the new content in
  // some other way - this is just a simple example.
  $output = $form['Add_form']['Opponement']['testing'];
  
  // Get the JS settings so we can merge them.
  $javascript = drupal_add_js(NULL, NULL, 'header');
  $settings = call_user_func_array('array_merge_recursive', $javascript['setting']);
  
  drupal_json(array(
    'status'    => TRUE,
    'data'      => theme('status_messages') . drupal_render($output),
    'settings'  => array('ahah' => $settings['ahah']),
  ));
}


/**
* Smarter version of array_merge_recursive: overwrites scalar values.
* 
* This also came (like a God send) from Wim's AHAH helper module. Really, that's the
* easiest way to go, and the module works like a charm - but I wanted to get my
* head around the whole AHAH thing, and maybe you do to, or maybe you can't or don't
* want to be dependant on a different module.
* 
* @see PHP Manual on: array-merge-recursive comment #82976.
*/





///////////////////////////////////////
///////////////////////////////////////
/////RENDERING MATCHES ADMIN/////
///////////////////////////////////////
///////////////////////////////////////
function matches_admin_page() {
$test = '';

	$head = array(
		array('data' => t('ID'), 'field' => 'mid', 'sort' => 'asc', 'width' => '30'),
		array('data' => t('Opponement'), 'width' => '30'),
		array('data' => t('Home score')),
		array('data' => t('Visitor score')),
		array('data' => t('Opérations'))
	);
 
  	$sql = "SELECT * FROM matches_entries" . tablesort_sql($head);
 
   	$result = db_query($sql);
 



  	while ($matches = db_fetch_object($result)) {
		$score_home = $matches->map_1_score_home+$matches->map_2_score_home+$matches->map_3_score_home+$matches->map_4_score_home;
		$score_visitor = $matches->map_1_score_visitor+$matches->map_2_score_visitor+$matches->map_3_score_visitor+$matches->map_4_score_visitor;
		$rows[] = array(
			array('data' => $matches->mid),
			array('data' => $matches->team),
			array('data' => $score_home),
			array('data' => $score_visitor),
			array('data' => ''.l('edit','admin/team/matches/edit/'. $matches->mid).' - '.l('delete','admin/team/matches/delete/'. $matches->mid).'')
		);
  	}
$test .= theme_table($head, $rows);

$test .= drupal_get_form('matches_admin');
  return $test;
}

function matches_admin_submit($form, &$form_state) {
  db_query("INSERT INTO {matches_entries} (mid, team) VALUES ('','%s')", $form_state['values']['matches_opponement']);
  drupal_set_message(t('Your form has been saved.') );
}

im really really frustrated about that issue .. i really cannot get any example to work and it looks like ahah_helper is not made for what i wanna do ...

If someone could enlight me it would be REALLY appreciated ...

timos’s picture

why do you think ahah_helper can't do what you want ?
I didn't read all your code, what do you want to do actually ?

Tim Baret

Craz’s picture

I want to populate a select list from another one, i found a few nodes talking about that but all the examples (including this post's one) i tried, failled and i don't know where is located the probleme. I'd really appreciate that someone give a look to my code and help me figure out what's wrong ... :/

Craz’s picture

up on this, still didn't fix the issue and i really need to get this working :/

nickcaballero’s picture

Thank you for this

_snake_’s picture

Thanks,

I was looking for that!

If hierarchical select is enabled the following error appears:

[Tue Apr 13 18:43:51 2010] [error] [client 192.168.10.50] PHP Fatal error:  Cannot redeclare array_smart_merge() (previously declared in /srv/www/htdocs/webs/nova/sites/all/modules/poof/poof.module:182) in /srv/www/htdocs/webs/nova/sites/all/modules/views/modules/hierarchical_select/hierarchical_select.module on line 2272, referer: http://192.168.100.39/
f0ns’s picture

I almost died the last days on getting AHAH to work, I really recommend everyone that wants to use AHAH to check out the AHAH helper module! It enlightened me and has a nice demo added (look into the code!!!).

Rajan M’s picture

AHAH helper module is really great for ahah, using this module we no need to implement all ahah framework, just need to register some helper functions, which minimize lots of code and complexity too.

Cheers,
Rajan

Craz’s picture

Hi,

would you mind giving me an example of how you implented it ? would be really appreciated :) thanks

Rajan M’s picture

I have implemented an example http://bit.ly/aTLfjV, which allows to modify/set options of select box one depending upon selected option another select box.

Cheers,
Rajan

vinothbabuog’s picture

Hi Rajan,
Thank you for your tutorial. I have implemented your code in my application, While selecting the first drop down value "Option 2" its remain "Options 1". Could you please help me on this.

Also, I have created the form using the content type. I am trying to implement this in hook_form_alter instead of the hook_form. Could you please help me on this.

Rajan M’s picture

AHAH helper module is really great for ahah, using this module we no need to implement all ahah framework, just need to register some helper functions, which minimize lots of code and complexity too.

Cheers,
Rajan

tpainton’s picture

subscribing. Tons of great info in this thread.