ahah_helper.module

Tracking 6.x-2.x branch
  1. drupal
    1. 6 contributions/ahah_helper/ahah_helper.module

Constants

NameDescription
AHAH_HELPER_CALLBACK_PATH
AHAH_HELPER_PARENTS_ENTIRE_FORM

Functions & methods

NameDescription
ahah_helper_form_item_loadWildcard loader for form items; assumes that a passed form item also exists.
ahah_helper_generic_submitSubmit callback; for any button that's used when JS is disabled (i.e. to trigger an update of the displayed fields: add/remove fields).
ahah_helper_menuImplementation of hook_menu().
ahah_helper_pathHelper function to generate a path that corresponds to a tree of parents in the form structure, or the special "entire form" wildcard.
ahah_helper_real_submitSubmit callback; gets rid of storage at the end, so that we can redirect.
ahah_helper_registerThis function records in which file your form definition lives and which file therefor should be loaded in case of an AHAH callback.
ahah_helper_renderGiven 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.
drupal_rebuild_form_and_apply_form_values
_ahah_helper_disable_validation
_ahah_helper_get_form_itemGiven a form by reference and an array of parents, return the corresponding form item by reference. If $parents is FALSE or '<form>', the entire $form array will be returned. If it doesn't exist, return NULL.

File

View source
  1. <?php
  2. define('AHAH_HELPER_CALLBACK_PATH', 'ahah_helper/');
  3. define('AHAH_HELPER_PARENTS_ENTIRE_FORM', '<form>');
  4. // REMARK: drupal_rebuild_form prevents $form_state['values'] from being
  5. // available to the form. Which means everything will have to be set in and
  6. // retrieved from $form_state['storage']!
  7. // If you switch the code in ahah_helper_render() to use the alternatively
  8. // provided drupal_rebuild_form_and_apply_form_values(), you can continue
  9. // using $form_state['values']. But then any #default_values you set using
  10. // $form_state['storage'] will be overridden: if no value existed for a form
  11. // item in $_POST, then it will be set to the empty string or zero. Unless you
  12. // set your #values to $form_state['storage'], but then $form_state['values']
  13. // has no effect.
  14. // As you can see, there currently is no sane way for this to work. So … stick
  15. // to storing everything in $form_state['storage'] … until FAPI gets better.
  16. //----------------------------------------------------------------------------
  17. // Drupal core hooks.
  18. /**
  19. * Implementation of hook_menu().
  20. */
  21. function ahah_helper_menu() {
  22. $items[AHAH_HELPER_CALLBACK_PATH . '%ahah_helper_form_item'] = array(
  23. 'page callback' => 'ahah_helper_render',
  24. 'page arguments' => array(1),
  25. 'access callback' => TRUE,
  26. 'type' => MENU_CALLBACK,
  27. );
  28. return $items;
  29. }
  30. //----------------------------------------------------------------------------
  31. // Menu system callbacks.
  32. /**
  33. * Wildcard loader for form items; assumes that a passed form item also exists.
  34. */
  35. function ahah_helper_form_item_load($form_item) {
  36. return $form_item;
  37. }
  38. //----------------------------------------------------------------------------
  39. // Exposed functions.
  40. /**
  41. * Helper function to generate a path that corresponds to a tree of parents in
  42. * the form structure, or the special "entire form" wildcard.
  43. */
  44. function ahah_helper_path($form_item = FALSE) {
  45. $path = AHAH_HELPER_CALLBACK_PATH;
  46. $path .= (is_array($form_item)) ? implode('][', $form_item) : AHAH_HELPER_PARENTS_ENTIRE_FORM;
  47. return $path;
  48. }
  49. /**
  50. * This function records in which file your form definition lives and which
  51. * file therefor should be loaded in case of an AHAH callback.
  52. *
  53. * @param $form
  54. * The form structure.
  55. * @param $form_state
  56. * The form state variable.
  57. */
  58. function ahah_helper_register(&$form, &$form_state) {
  59. static $js_file_added;
  60. // This is an AHAH-driven form, so enable caching.
  61. $form['#cache'] = TRUE;
  62. // When not set.. try to remember the old action
  63. if (!isset($form_state['storage']['old_action'])) {
  64. $form_state['storage']['old_action'] = isset($form['#action']) ? $form['#action'] : $_SERVER['REQUEST_URI'];
  65. }
  66. // If the form action change, restore the old one.
  67. if (isset($form_state['storage']['old_action']) &&
  68. ($form['#action']!=$form_state['storage']['old_action'])) {
  69. $form['#action'] = $form_state['storage']['old_action'];
  70. }
  71. // Register the file.
  72. if (!isset($form_state['storage']['#ahah_helper']['file'])) {
  73. $menu_item = menu_get_item();
  74. if ($menu_item['file']) {
  75. $form_state['storage']['#ahah_helper']['file'] = $menu_item['file'];
  76. }
  77. }
  78. // We remember all values the user last entered, even for fields that are
  79. // currently invisible. That way, we can restore the last entered value
  80. // when a user switched to Personal usage and back to Company usage.
  81. if (isset($form_state['values'])) {
  82. // $form_state['storage'] must be initialized for array_smart_merge() to
  83. // work.
  84. if (!isset($form_state['storage'])) {
  85. $form_state['storage'] = array();
  86. }
  87. $form_state['storage'] = array_smart_merge($form_state['storage'], $form_state['values']);
  88. }
  89. // Add our JS file, which has some Drupal core JS overrides.
  90. if (!isset($js_file_added)) {
  91. drupal_add_js(drupal_get_path('module', 'ahah_helper') . '/ahah_helper.js', 'module', 'footer');
  92. $js_file_added = TRUE;
  93. }
  94. // Add our submit function, which will clean up form storage, so that redirects will work.
  95. $form['#submit'][] = 'ahah_helper_real_submit';
  96. if ($form['#id'] == 'node-form') {
  97. foreach ($form['buttons'] as $name => &$button) {
  98. $button['#submit'][] = 'ahah_helper_real_submit';
  99. }
  100. }
  101. }
  102. /**
  103. * Submit callback; for any button that's used when JS is disabled (i.e. to
  104. * trigger an update of the displayed fields: add/remove fields).
  105. */
  106. function ahah_helper_generic_submit($form, &$form_state) {
  107. $form_state['rebuild'] = TRUE;
  108. }
  109. /**
  110. * Submit callback; gets rid of storage at the end, so that we can redirect.
  111. */
  112. function ahah_helper_real_submit($form, &$form_state) {
  113. unset($form_state['storage']);
  114. }
  115. /**
  116. * Given a POST of a Drupal form (with both a form_build_id set), this
  117. * function rebuilds the form and then only renders the given form item. If no
  118. * form item is given, the entire form is rendered.
  119. *
  120. * Is used directly in a menu callback in ahah_helper's sole menu item. Can
  121. * also be used in more advanced menu callbacks, for example to render
  122. * multiple form items of the same form and return them separately.
  123. *
  124. * @param $parents
  125. */
  126. function ahah_helper_render($form_item_to_render = FALSE) {
  127. $form_state = array('storage' => NULL, 'submitted' => FALSE);
  128. $form_build_id = $_POST['form_build_id'];
  129. // Get the form from the cache.
  130. $form = form_get_cache($form_build_id, $form_state);
  131. $args = $form['#parameters'];
  132. $form_id = array_shift($args);
  133. // Are we on the node form?
  134. $node_form = FALSE;
  135. if (preg_match('/_node_form$/', $form_id)) {
  136. $node_form = TRUE;
  137. module_load_include('inc', 'node', 'node.pages');
  138. }
  139. // We will run some of the submit handlers so we need to disable redirecting.
  140. $form['#redirect'] = FALSE;
  141. // We need to process the form, prepare for that by setting a few internals
  142. // variables.
  143. $form['#post'] = $_POST;
  144. $form['#programmed'] = FALSE;
  145. $form_state['post'] = $_POST;
  146. // $form_state['storage']['#ahah_helper']['file'] has been set, to know
  147. // which file should be loaded. This is necessary because we'll use the form
  148. // definition itself rather than the cached $form.
  149. if (isset($form_state['storage']['#ahah_helper']['file'])) {
  150. require_once($form_state['storage']['#ahah_helper']['file']);
  151. }
  152. // If the form is being rebuilt due to something else than a pressed button,
  153. // e.g. a select that was changed, then $_POST['op'] will be empty. As a
  154. // result, Forms API won't be able to detect any pressed buttons. Eventually
  155. // it will call _form_builder_ie_cleanup(), which will automatically, yet
  156. // inappropriately assign the first in the form as the clicked button. The
  157. // reasoning is that since the form has been submitted, a button surely must
  158. // have been clicked. This is of course an invalid reasoning in the context
  159. // of AHAH forms.
  160. // To work around this, we *always* set $form_state['submitted'] to true,
  161. // this will prevent _form_builder_ie_cleanup() from assigning a wrong
  162. // button. When a button is pressed (thus $_POST['op'] is set), then this
  163. // button will still set $form_state['submitted'],
  164. // $form_state['submit_handlers'] and $form_state['validate_handlers'].
  165. // This problem does not exist when AHAH is disabled, because then the
  166. // assumption is true, and then you generally provide a button as an
  167. // alternative to the AHAH behavior.
  168. $form_state['submitted'] = TRUE;
  169. // Continued from the above: when an AHAH update of the form is triggered
  170. // without using a button, you generally don't want any validation to kick
  171. // in. A typical example is adding new fields, possibly even required ones.
  172. // You don't want errors to be thrown at the user until they actually submit
  173. // their values. (Well, actually you want to be smart about this: sometimes
  174. // you do want instant validation, but that's an even bigger pain to solve
  175. // here so I'll leave that for later…)
  176. if (!isset($_POST['op'])) {
  177. // For the default "{$form_id}_validate" and "{$form_id}_submit" handlers.
  178. $form['#validate'] = NULL;
  179. $form['#submit'] = NULL;
  180. // For customly set #validate and #submit handlers.
  181. $form_state['submit_handlers'] = NULL;
  182. $form_state['validate_handlers'] = NULL;
  183. // Disable #required and #element_validate validation.
  184. _ahah_helper_disable_validation($form);
  185. }
  186. // Build, validate and if possible, submit the form.
  187. drupal_process_form($form_id, $form, $form_state);
  188. if ($node_form) {
  189. // get the node from the submitted values
  190. $node = node_form_submit_build_node($form, $form_state);
  191. // hack to stop taxonomy from resetting when the form is rebuilt
  192. $form_state['node']['taxonomy'] = taxonomy_preview_terms($node);
  193. }
  194. // This call recreates the form relying solely on the form_state that the
  195. // drupal_process_form set up.
  196. //$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
  197. $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
  198. // Get the form item we want to render.
  199. $form_item = _ahah_helper_get_form_item($form, $form_item_to_render);
  200. // Get the JS settings so we can merge them.
  201. $javascript = drupal_add_js(NULL, NULL, 'header');
  202. $settings = call_user_func_array('array_merge_recursive', $javascript['setting']);
  203. drupal_json(array(
  204. 'status' => TRUE,
  205. 'data' => theme('status_messages') . drupal_render($form_item),
  206. 'settings' => array('ahah' => $settings['ahah']),
  207. ));
  208. }
  209. //----------------------------------------------------------------------------
  210. // Private functions.
  211. /**
  212. * Given a form by reference and an array of parents, return the corresponding
  213. * form item by reference. If $parents is FALSE or '<form>', the entire $form
  214. * array will be returned. If it doesn't exist, return NULL.
  215. *
  216. * @param $form
  217. * A form.
  218. * @param $parents
  219. * An array of parrents by which the wanted form item is identified, or
  220. * imploded with ']['.
  221. * @return
  222. * The requested form item.
  223. */
  224. function &_ahah_helper_get_form_item(&$form, $parents = FALSE) {
  225. if ($parents == AHAH_HELPER_PARENTS_ENTIRE_FORM || !$parents) {
  226. return $form;
  227. }
  228. // Allow $parents to be either an array of the element's parents or the name
  229. // of an element.
  230. if (is_string($parents)) {
  231. if (strpos($parents, ']') !== FALSE) {
  232. $parents = explode('][', $parents);
  233. }
  234. else {
  235. $parents = array($parents);
  236. }
  237. }
  238. // Recursively seek the form element.
  239. if (count($parents)) {
  240. $parent = array_shift($parents);
  241. if (isset($form[$parent])) {
  242. return _ahah_helper_get_form_item($form[$parent], $parents);
  243. }
  244. else {
  245. return NULL;
  246. }
  247. }
  248. else {
  249. return $form;
  250. }
  251. }
  252. function _ahah_helper_disable_validation(&$form) {
  253. foreach (element_children($form) as $child) {
  254. $form[$child]['#validated'] = TRUE;
  255. _ahah_helper_disable_validation($form[$child]);
  256. }
  257. }
  258. //----------------------------------------------------------------------------
  259. // Additional PHP functions.
  260. /**
  261. * Smarter version of array_merge_recursive: overwrites scalar values.
  262. *
  263. * From: http://www.php.net/manual/en/function.array-merge-recursive.php#82976.
  264. */
  265. if (!function_exists('array_smart_merge')) {
  266. function array_smart_merge($array, $override) {
  267. if (is_array($array) && is_array($override)) {
  268. foreach ($override as $k => $v) {
  269. if (isset($array[$k]) && is_array($v) && is_array($array[$k])) {
  270. $array[$k] = array_smart_merge($array[$k], $v);
  271. }
  272. else {
  273. $array[$k] = $v;
  274. }
  275. }
  276. }
  277. return $array;
  278. }
  279. }
  280. //----------------------------------------------------------------------------
  281. // Demonstrative functions.
  282. function drupal_rebuild_form_and_apply_form_values($form_id, &$form_state, $args, $form_build_id = NULL) {
  283. // Remove the first argument. This is $form_id.when called from
  284. // drupal_get_form and the original $form_state when called from some AHAH
  285. // callback. Neither is needed. After that, put in the current state.
  286. $args[0] = &$form_state;
  287. // And the form_id.
  288. array_unshift($args, $form_id);
  289. $form = call_user_func_array('drupal_retrieve_form', $args);
  290. if (!isset($form_build_id)) {
  291. // We need a new build_id for the new version of the form.
  292. $form_build_id = 'form-'. md5(mt_rand());
  293. }
  294. $form['#build_id'] = $form_build_id;
  295. drupal_prepare_form($form_id, $form, $form_state);
  296. // Now, we cache the form structure so it can be retrieved later for
  297. // validation. If $form_state['storage'] is populated, we'll also cache
  298. // it so that it can be used to resume complex multi-step processes.
  299. form_set_cache($form_build_id, $form, $form_state);
  300. /**
  301. * UNTIL HERE THIS FUNCTION IS IDENTICAL TO drupal_rebuild_form().
  302. */
  303. // Clear out all post data, as we don't want the previous step's
  304. // data to pollute this one and trigger validate/submit handling,
  305. // then process the form for rendering.
  306. //$_POST = array();
  307. //$form['#post'] = array();
  308. //drupal_process_form($form_id, $form, $form_state);
  309. // Set POST data.
  310. $form['#post'] = $_POST;
  311. $form = form_builder($form_id, $form, $form_state);
  312. return $form;
  313. }