'); // REMARK: drupal_rebuild_form prevents $form_state['values'] from being // available to the form. Which means everything will have to be set in and // retrieved from $form_state['storage']! // If you switch the code in ahah_helper_render() to use the alternatively // provided drupal_rebuild_form_and_apply_form_values(), you can continue // using $form_state['values']. But then any #default_values you set using // $form_state['storage'] will be overridden: if no value existed for a form // item in $_POST, then it will be set to the empty string or zero. Unless you // set your #values to $form_state['storage'], but then $form_state['values'] // has no effect. // As you can see, there currently is no sane way for this to work. So … stick // to storing everything in $form_state['storage'] … until FAPI gets better. //---------------------------------------------------------------------------- // Drupal core hooks. /** * Implementation of hook_menu(). */ function ahah_helper_menu() { $items[AHAH_HELPER_CALLBACK_PATH . '%ahah_helper_form_item'] = array( 'page callback' => 'ahah_helper_render', 'page arguments' => array(1), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); return $items; } //---------------------------------------------------------------------------- // Menu system callbacks. /** * Wildcard loader for form items; assumes that a passed form item also exists. */ function ahah_helper_form_item_load($form_item) { return $form_item; } //---------------------------------------------------------------------------- // Exposed functions. /** * Helper function to generate a path that corresponds to a tree of parents in * the form structure, or the special "entire form" wildcard. */ function ahah_helper_path($form_item = FALSE) { $path = AHAH_HELPER_CALLBACK_PATH; $path .= (is_array($form_item)) ? implode('][', $form_item) : AHAH_HELPER_PARENTS_ENTIRE_FORM; return $path; } /** * This function records in which file your form definition lives and which * file therefor should be loaded in case of an AHAH callback. * * @param $form * The form structure. * @param $form_state * The form state variable. */ function ahah_helper_register(&$form, &$form_state) { static $js_file_added; // This is an AHAH-driven form, so enable caching. $form['#cache'] = TRUE; // When not set.. try to remember the old action if (!isset($form_state['storage']['old_action'])) { $form_state['storage']['old_action'] = isset($form['#action']) ? $form['#action'] : $_SERVER['REQUEST_URI']; } // If the form action change, restore the old one. if (isset($form_state['storage']['old_action']) && ($form['#action']!=$form_state['storage']['old_action'])) { $form['#action'] = $form_state['storage']['old_action']; } // Register the file. if (!isset($form_state['storage']['#ahah_helper']['file'])) { $menu_item = menu_get_item(); if ($menu_item['file']) { $form_state['storage']['#ahah_helper']['file'] = $menu_item['file']; } } // We remember all values the user last entered, even for fields that are // currently invisible. That way, we can restore the last entered value // when a user switched to Personal usage and back to Company usage. if (isset($form_state['values'])) { // $form_state['storage'] must be initialized for array_smart_merge() to // work. if (!isset($form_state['storage'])) { $form_state['storage'] = array(); } $form_state['storage'] = array_smart_merge($form_state['storage'], $form_state['values']); } // Add our JS file, which has some Drupal core JS overrides. if (!isset($js_file_added)) { drupal_add_js(drupal_get_path('module', 'ahah_helper') . '/ahah_helper.js', 'footer'); $js_file_added = TRUE; } } /** * Submit callback; for any button that's used when JS is disabled (i.e. to * trigger an update of the displayed fields: add/remove fields). */ function ahah_helper_generic_submit($form, &$form_state) { $form_state['rebuild'] = TRUE; } /** * Given a POST of a Drupal form (with both a form_build_id set), this * function rebuilds the form and then only renders the given form item. If no * form item is given, the entire form is rendered. * * Is used directly in a menu callback in ahah_helper's sole menu item. Can * also be used in more advanced menu callbacks, for example to render * multiple form items of the same form and return them separately. * * @param $parents */ function ahah_helper_render($form_item_to_render = FALSE) { $form_state = array('storage' => NULL, 'submitted' => FALSE); $form_build_id = $_POST['form_build_id']; // Get the form from the cache. $form = form_get_cache($form_build_id, $form_state); $args = $form['#parameters']; $form_id = array_shift($args); // We will run some of the submit handlers so we need to disable redirecting. $form['#redirect'] = FALSE; // We need to process the form, prepare for that by setting a few internals // variables. $form['#post'] = $_POST; $form['#programmed'] = FALSE; $form_state['post'] = $_POST; // $form_state['storage']['#ahah_helper']['file'] has been set, to know // which file should be loaded. This is necessary because we'll use the form // definition itself rather than the cached $form. if (isset($form_state['storage']['#ahah_helper']['file'])) { require_once($form_state['storage']['#ahah_helper']['file']); } // If the form is being rebuilt due to something else than a pressed button, // e.g. a select that was changed, then $_POST['op'] will be empty. As a // result, Forms API won't be able to detect any pressed buttons. Eventually // it will call _form_builder_ie_cleanup(), which will automatically, yet // inappropriately assign the first in the form as the clicked button. The // reasoning is that since the form has been submitted, a button surely must // have been clicked. This is of course an invalid reasoning in the context // of AHAH forms. // To work around this, we *always* set $form_state['submitted'] to true, // this will prevent _form_builder_ie_cleanup() from assigning a wrong // button. When a button is pressed (thus $_POST['op'] is set), then this // button will still set $form_state['submitted'], // $form_state['submit_handlers'] and $form_state['validate_handlers']. // This problem does not exist when AHAH is disabled, because then the // assumption is true, and then you generally provide a button as an // alternative to the AHAH behavior. $form_state['submitted'] = TRUE; // Continued from the above: when an AHAH update of the form is triggered // without using a button, you generally don't want any validation to kick // in. A typical example is adding new fields, possibly even required ones. // You don't want errors to be thrown at the user until they actually submit // their values. (Well, actually you want to be smart about this: sometimes // you do want instant validation, but that's an even bigger pain to solve // here so I'll leave that for later…) if (!isset($_POST['op'])) { // For the default "{$form_id}_validate" and "{$form_id}_submit" handlers. $form['#validate'] = NULL; $form['#submit'] = NULL; // For customly set #validate and #submit handlers. $form_state['submit_handlers'] = NULL; $form_state['validate_handlers'] = NULL; // Disable #required and #element_validate validation. _ahah_helper_disable_validation($form); } // Build, validate and if possible, submit the form. drupal_process_form($form_id, $form, $form_state); // This call recreates the form relying solely on the form_state that the // drupal_process_form set up. //$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id); $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id); // Get the form item we want to render. $form_item = _ahah_helper_get_form_item($form, $form_item_to_render); // 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($form_item), 'settings' => array('ahah' => $settings['ahah']), )); } //---------------------------------------------------------------------------- // Private functions. /** * Given a form by reference and an array of parents, return the corresponding * form item by reference. If $parents is FALSE or '
', the entire $form * array will be returned. If it doesn't exist, return NULL. * * @param $form * A form. * @param $parents * An array of parrents by which the wanted form item is identified, or * imploded with ']['. * @return * The requested form item. */ function &_ahah_helper_get_form_item(&$form, $parents = FALSE) { if ($parents == AHAH_HELPER_PARENTS_ENTIRE_FORM || !$parents) { return $form; } // Allow $parents to be either an array of the element's parents or the name // of an element. if (is_string($parents)) { if (strpos($parents, ']') !== FALSE) { $parents = explode('][', $parents); } else { $parents = array($parents); } } // Recursively seek the form element. if (count($parents)) { $parent = array_shift($parents); if (isset($form[$parent])) { return _ahah_helper_get_form_item($form[$parent], $parents); } else { return NULL; } } else { return $form; } } function _ahah_helper_disable_validation(&$form) { foreach (element_children($form) as $child) { $form[$child]['#validated'] = TRUE; _ahah_helper_disable_validation($form[$child]); } } //---------------------------------------------------------------------------- // Additional PHP functions. /** * Smarter version of array_merge_recursive: overwrites scalar values. * * From: http://www.php.net/manual/en/function.array-merge-recursive.php#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; } //---------------------------------------------------------------------------- // Demonstrative functions. function drupal_rebuild_form_and_apply_form_values($form_id, &$form_state, $args, $form_build_id = NULL) { // Remove the first argument. This is $form_id.when called from // drupal_get_form and the original $form_state when called from some AHAH // callback. Neither is needed. After that, put in the current state. $args[0] = &$form_state; // And the form_id. array_unshift($args, $form_id); $form = call_user_func_array('drupal_retrieve_form', $args); if (!isset($form_build_id)) { // We need a new build_id for the new version of the form. $form_build_id = 'form-'. md5(mt_rand()); } $form['#build_id'] = $form_build_id; drupal_prepare_form($form_id, $form, $form_state); // Now, we cache the form structure so it can be retrieved later for // validation. If $form_state['storage'] is populated, we'll also cache // it so that it can be used to resume complex multi-step processes. form_set_cache($form_build_id, $form, $form_state); /** * UNTIL HERE THIS FUNCTION IS IDENTICAL TO drupal_rebuild_form(). */ // Clear out all post data, as we don't want the previous step's // data to pollute this one and trigger validate/submit handling, // then process the form for rendering. //$_POST = array(); //$form['#post'] = array(); //drupal_process_form($form_id, $form, $form_state); // Set POST data. $form['#post'] = $_POST; $form = form_builder($form_id, $form, $form_state); return $form; }