Drupal 6 AHAH forms: Making New Fields Work

03.28.2008

Today, I was working with drupal 6's AHAH form elements. Initially, I was delighted at how well they worked. That delight turned to confusion once I realized that the form elements I had put in the menu callback of the #ahah['path'] was missing its name attribute. After doing a bit of research in how the poll module handled the formapi voodoo, I created a generalized function to aid in building AHAH callbacks. If there is a better way to do this, I wasn't able to find it.

<?php
// this is an example menu_callback that would be referenced by the #ahah['path'] property
function easy_ahah_form_field() {
 
// all you have to worry about is the new form field that will be inserted via #ahah
 
$form = array(
   
'#type' => 'select',
   
'#title' => 'You selected that because...',
   
'#options' => array(
     
'1' => 'drugs',
     
'2' => 'I do what I want.',
     
'3' => "I'm feeling lucky..."
   
),
   
  );
 
// ahah_render is where the magic happens.
  // 'the value of this field will show up as $form_value['user_problem']
 
$output = ahah_render($form, 'user_problem');
  print
drupal_to_js(array('data' => $output, 'status' => true));
  exit();
}
/*
  This function is largely based on the poll module, its been simplified for reuse.
  $fields is the specific form elements you want to attach via ahah,
  $name is the form fields array key... e.g. the name for $form['title'] is "title"
*/
function ahah_render($fields, $name) {
 
$form_state = array('submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
 
$form = form_get_cache($form_build_id, $form_state);
 
$form[$name] = $fields;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );
 
// Rebuild the form.
 
$form = form_builder($_POST['form_id'], $form, $form_state);

  // Render the new output.
 
$new_form = $form[$name];
  return
drupal_render($new_form); 
}
?>

Comments

thanks so much for this info!

thanks so much for this info! this should be added to drupal somewhere :)

Has anyone gotten

Has anyone gotten #default_value to work in form elements loaded via this method? That form_builder at the end of ahah_render seems to negate the power of my #default_value s.

Had the same problem, tbh,

Had the same problem, tbh, applying this tip to different situation. It ain't pretty.

I try a very simple test

I try a very simple test based on your example but I had a big javascript error box that show me the source code of my html page (with HTML entities)... Even if I test the ahah example in the Pro Drupal Dev on chapter 10 :-(
I think I made a mistake somewhere but I can't find where !

<?php
function cotation_menu() {
   
$items = array();
   
$items['cotation'] = array(
       
'title' => 'Demande de cotation',
       
'page callback' => 'drupal_get_form',
       
'page arguments' => array('cotation_form'),
       
'access arguments' => array('access content'),
       
'type' => MENU_CALLBACK
   
);
   
$items['cotation/message_js'] = array(
       
'page_callback' => 'cotation_message_js',
       
'access arguments' => array('access content'),
       
'type' => MENU_CALLBACK,
    );
    return
$items;
}

function cotation_form() {
   
$form['target'] = array(
    
'#type' => 'markup',
    
'#prefix' => '<div id="mytarget">',
    
'#value' => t('Click the button here and this text will be replaced'),
    
'#suffix' => '</div>',
    );
   
$form['submit'] = array(
    
'#type' => 'submit',
    
'#value' => t('Click me'),
    
'#ahah' => array(
      
'event' => 'click',
      
'path' => 'cotation/message_js',
      
'wrapper' => 'mytarget',
      
'method' => 'append',
      
'effect' => 'fade',
     ),
    );
    return
$form;
}
/*
function cotation_message_js() {
    $output = t('COTATION POOF !');
    drupal_json(array('status' => TRUE, 'data' => $ouput));
}
*/

// this is an example menu_callback that would be referenced by the #ahah['path'] property
function cotation_message_js() {
 
// all you have to worry about is the new form field that will be inserted via #ahah
 
$form = array(
   
'#type' => 'select',
   
'#title' => 'You selected that because...',
   
'#options' => array(
     
'1' => 'drugs',
     
'2' => 'I do what I want.',
     
'3' => "I'm feeling lucky..."
   
),
   
  );
 
// ahah_render is where the magic happens.
  // 'the value of this field will show up as $form_value['user_problem']
 
$output = cotation_render($form, 'user_problem');
  print
drupal_to_js(array('data' => $output, 'status' => true));
  exit();
}
/*
  This function is largely based on the poll module, its been simplified for reuse.
  $fields is the specific form elements you want to attach via ahah,
  $name is the form fields array key... e.g. the name for $form['title'] is "title"
*/
function cotation_render($fields, $name) {
 
$form_state = array('submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
 
$form = form_get_cache($form_build_id, $form_state);
 
$form[$name] = $fields;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );
 
// Rebuild the form.
 
$form = form_builder($_POST['form_id'], $form, $form_state);
 
// Render the new output.
 
$new_form = $form[$name];
  return
drupal_render($new_form); 
}
?>

Thank you...

Is it possible to add another

Is it possible to add another AHAH element by firing an event on an element which is added using AHAH previously?

Has anyone modified the

Has anyone modified the helper function here to work with the recommended approach discussed on drupal.org (http://drupal.org/node/331941)?

Hi... i just want to know if

Hi...
i just want to know if you get it worked. I mean the select list dynamically.
Because I did, but since I am a newbie with drupal 6, I am not really like the way I did it. I just took the same poll.module functions and made some changes like the above example.

<?php
  $items
['score_calc/client'] = array(
   
'title' => t('JavaScript reload programs'),
   
'page callback' => 'score_calc_program_js',
   
'access arguments' => array('access content'),
   
'description' => t('Reload Client Programs'),
   
'type' => MENU_CALLBACK,
  );

  return $items;
}

function score_calc_form() {
  return
drupal_get_form('score_calc_f1_form');
}

// We add some logic to our form builder to give it two pages. It checks a
// value in $form_state['storage'] to see if we should display page 2.

function _get_score_calc_programs($client = 0){
   
// Get each option and populate the options array.
 
 
$options = array(0 => 'Select Program');
 
$sql = sprintf("SELECT DISTINCT program_id, program_name FROM SCalc_program WHERE client_id = %d", $client);
 
  if (
$r = db_query($sql)){
    while (
$row = db_fetch_array($r)) {
       
$options[$row['program_id']] = $row['program_name'];
    }   
  }
 
  return
$options;
}

function score_calc_f1_form($form_state) {

  global $user;
 
$form = array('#cache' => TRUE);
 
 
// Get each option and populate the options array.
 
$options = array(0 => 'Select Client');
 
$sql = "SELECT DISTINCT * FROM SCalc_client ORDER BY client_id";
 
$r = db_query($sql);
  while (
$row = db_fetch_array($r)) {
   
$options[$row['client_id']] = $row['client_name'];
  }
 
 
$form['cltpro'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Client and Program'),
  );

  $form['cltpro']['client'] = array(
   
'#type' => 'select',
   
'#title' => t('Select Client'),
   
'#options' => $options,
   
'#ahah' => array( 'path' => 'score_calc/client',
               
'wrapper' => 'score_calc_test',
             
'event' => 'change',
                     
'method' => 'replace',
             
'effect' => 'none',
                  ),
   
'#default_value' => variable_get('client','Select Client'),
   
  );

// Get each option and populate the options array.
 
$options = array(0 => 'Select Program');
 
$form['cltpro']['program'] = array(
   
'#type' => 'select',
   
'#title' => t('Select Program'),
   
'#options' => $options,
   
'#default_value' => variable_get('program','Select Program'),
   
'#prefix' => '<div id="score_calc_test" style="width: 150px;border: 1px solid red;">',
   
'#suffix' => '</div>',
  );

  $form['continue'] = array(
   
'#type' => 'submit',
   
'#value' => 'Continue',
  );
 
  return
$form;

}

function score_calc_program_js(){

$client = $_POST['client'];

$options = _get_score_calc_programs($client);
$form_element = array(
   
'#type' => 'select',
   
'#title' => t('Select Program'),
   
'#options' => $options,
   
'#default_value' => variable_get('program','Select Program'),
   
'#prefix' => '<div id="score_calc_test" style="width:150px;border: 1px solid red;">', //I only set the border red just to know what part of my code is going to be updated.
   
'#suffix' => '</div>',
);
 
drupal_alter('form', $form_element, array(), 'score_calc_program_js');

$form_state = array('submitted' => FALSE);
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
if (!$form = form_get_cache($form_build_id, $form_state)) {
   exit();
}

$form['cltpro']['program'] = $form_element;
form_set_cache($form_build_id, $form, $form_state);
$form += array(
  
'#post' => $_POST,
  
'#programmed' => FALSE,
);

// Rebuild the form.
$form = form_builder($form_build_id, $form, $form_state);   
$program_form = $form['cltpro']['program'];
unset(
$program_form['#prefix'], $program_form['#suffix']); // Prevent duplicate wrappers.
$output = drupal_render($program_form); //theme('score_calc_program');
drupal_json(array('status' => TRUE, 'data' => $output));
}

function score_calc_f1_form_validate($form, &$form_state){
 
$client = $form_state['values']['cleint'];
 
$program = $form_state['values']['program'];

  if ($client == '0' ) {
   
form_set_error('client', 'Please select a client from the list.');
  }
  if (
$program == '0' ) {
   
form_set_error('program', 'Please select a Program from the list.');
  }
}

function score_calc_f1_form_submit($form, &$form_state){
   
drupal_set_message('Your form has been submitted');
}
?>

Good work! If I had one bit

Good work! If I had one bit of constructive criticism it would be that you could do the same thing with 2-3 less functions. Too many functions is always a great way to confuse yourself.

Sorry .......

Sorry .......

Hey, my bad actually, had

Hey, my bad actually, had some input filter issues. Will take a look in a while (unfortunately working atm).

Seconded

Is there any work being done - issue in issue queue - on improving the rendering of just a part of the form in Drupal 7?

my use case
"add an item to a select list dynamically"
so i need clean way of swapping out the select list only

menu callbacks

I'm having a hard time understanding the menu callback system. The functions that Nick has posted make sense to me, but I'm a little unclear how to implement the menu callback. Any advice would be appreciated, as I'm becoming a little frustrated. I know once I break through it will be great. Thanks all!

mark

As i understand it you cannot

As i understand it you cannot call easy_ahah_form_field() directly. You have to add an item to your implementation of hook_menu that calls this function. For example:

<?php
 
...
 
$items['test/js'] = array(
   
'title' => t('test callback'),
   
'page callback' => 'easy_ahah_form_field',
   
'type' => MENU_CALLBACK,
  );
...
?>

Your button (or whatever fires the ahah function) uses 'test/js' as path.

<?php
    $form
['add_form_element'] = array(
   
'#type' => 'button',
   
'#ahah' => array(
     
'path' => 'test/js',
     
'wrapper' => 'here_it_should_go',
     
'method' => 'after',
     
'effect' => 'slide'),
    ); 
?>

I hope thats what you meant. But caution: i am a beginner :)
akm2b

i'm close

That's exactly what i was talking about, thanks so much for your reply. I found some other info on menu callbacks last night which agree with the example you posted. It'd really close to working now, but I'm getting a javascript alert with a 404 message for the test/js path. I'll keep playing with it, but once again any more advice is welcomed. Thanks again,

mark

Update:

Okay, I got it working. In order for the mapped function to become available, I had to disable and re-enable the module. Thanks again for a great blog and help with my question.

Sweet! :-D

Sweet! :-D

multiple fields

First, thanks for this great example.

After some little changes on your code I managed to add two fields at once per click . I can access the values in form_state['values']. But i can't seem to find a way to add them to a fieldset.

I posted the menu_callback function. ahah_render() is the same like in your example.
The $name argument of ahah_render has no influence on the name. In form_state['values'] these added fields appear with the name they get in the $form array.

So how can i add some fieldset information? Can anyone point me in the right direction.

<?php
function sqcb_js() {

  //Dummy Data. Real data will come from database
 
$N_fertilizer_options = array('1' => t('N fertilizer 1'), '0' => t('N fertilizer 2'));
 
//check wich number the new fields should have
 
$N_fert_formline = variable_get($min_fert_N_line_num,1);
 
//increase
 
$N_fert_formline++;
 
//set new value for next run
 
variable_set($min_fert_N_line_num,$N_fert_formline);
 
 
//make identifierstring with line number
 
$N_fert_type_row = 'N_fertilizer_type_'.$N_fert_formline;
 
$N_fert_ammount_row = 'N_amount_'.$N_fert_formline;
 
 
//define form elements that will be added
 
$form[$N_fert_type_row] = array(
   
'#type' => 'select',
   
'#title' => t('N fertilizer'),
   
'#options' => $N_fertilizer_options,
  );

  $form[$N_fert_ammount_row] = array(
   
'#type' => 'textfield',
   
'#title' => t('amount'),
  );

  // ahah_render is where the magic happens.
  // 'the value of this field will show up as $form_value['user_problem']
 
$output = ahah_render($form, 'min_fertilizer_'.$N_fert_formline);
  print
drupal_to_js(array('data' => $output, 'status' => true));
  exit();
}
?>

Maybe I'm missing something...

OK, maybe I'm missing something, but when I use this code it replaces my existing form with the new select element, rather than appending the new element to the form. Can anyone suggest what am I doing wrong? My code is based heavily off the poll module, as well. Many thanx for the help, Ben

Be sure to set your

Be sure to set your $form['#ahah']['wrapper'] to the css ID you wish to replace.

thanks

i'm not sure if i got it quite right, but if so thanks for the post. it might be quite useful.

Thank you very much!

I've been similarly elated and then disappointed/confused by AHAH fields in D6. This may help enormously.

Is there something similar,

Is there something similar, but for drupal 5 ?!

No. And don't quote me on

No. And don't quote me on this, but I think the formapi needed to be tweaked in 6 to allow AHAH to work too. So backporting it to 5 is probably not practical.

Finally I found something in

Finally I found something in this direction. But I need it for D5. There must be some possibility to stretch D5 to make this happen? Any idea on how to work around form_get_cache?

Could be improved in D7

This is certainly something I want to get improved before D7 rolls out. For one, I want to eliminate the #ahah['path'] property and put in a #ahah['callback'], where you can call a function directly without making a menu callback. Second, there *really* needs to be an easier way to render just a portion of a form. Even though the JavaScript side of things is really easy, we need better support on the FormsAPI side to render a small piece of the form.

Thanks for putting together this small example, hopefully it won't be needed forever. :)

Impractical

I want to eliminate the #ahah['path'] property and put in a #ahah['callback'], where you can call a function directly without making a menu callback.

You need a menu callback at some point along the line - whether or not this menu callback is a single one implemented by the Form API and invisible to your module. As long as you have to go through the menu system, you might as well do it directly...

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.