AHAH helper module

published on August 29, 2008

AHAH-powered forms were virtually impossible in Drupal 5 (see the note though). In Drupal 6, this is much easier, thanks to the #ahah property. However, it still is really painful to actually use it.

The flaw

You have to write a menu callback for each AHAH-enabled form item of your form. You have to repeat small variations of this piece of code for each callback:

// Build our new form element.
$form_element = _mymodule_add_something_to_form();

// Build the new form.
$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['somewhere']['very']['deep'] = $form_element;
form_set_cache($form_build_id, $form, $form_state);
$form += array(
  '#post' => $_POST,
  '#programmed' => FALSE,
);

// Rebuild the form.
$form = form_builder('mymodule_someform', $form, $form_state);

// Render the new output.
$subform = $form['somewhere']['choice'];
$output = theme('status_messages') . drupal_render($subform);

drupal_json(array('status' => TRUE, 'data' => $output));

Ok, it does make sense. But it takes some time to get used to — too much — and is a treshold that’s big enough for many developers to just not implement AHAH forms. It simply shouldn’t be this hard.
This current approach of AHAH forms in Drupal is time consuming, hard to maintain and hard to write tests for.

The other flaws

There also are other flaws:

  1. It’s impossible to write functional tests, because you can’t test the part that only works when JavaScript is enabled (SimpleTest’s browser does not support JavaScript, it simply uses curl). Even if that were possible, you’d have to write the same tests twice; the tests differ when JavaScript is either enabled or disabled, because the logic is separate. This also implies that you wouldn’t get your graceful degradation “for free”, because you don’t have to write the logic once, but twice.
  2. Even when a form item has been added for the first time, it’s being validated. This is wrong. You wouldn’t like it if your form was being validated the first time it’s being displayed, right? Well, this is the same thing.
  3. If new AHAH-enabled form items are added during an AHAH callback, they don’t work, because Drupal.settings doesn’t get updated.

The solution: the AHAH helper module

This module simplifies that. It allows you to:

  1. not write any menu callback at all.
  2. still not write any JavaScript at all.
  3. have a sole, central form definition function that has some if-tests to support a changing form based on the user’s input, i.e. by checking $form_state['values'] and/or $form_state['storage']. This is in fact the exact same system you’ve been applying if you’ve already written multi-step forms. This makes sense, because AHAH forms are in fact normal multi-step forms, that just happen to be updatable through AHAH as well.

    You still have to use the #ahah property and set a wrapper, but you provide a “magical path” that will automatically rebuild and render the desired part of the form. If the part of the form that you want to be rendered is $form['fapi']['rocks'] then you would do 'path' => ahah_helper_path(array('fapi', 'rocks')) and that’s it.

    Adding graceful degradation just became really easy: just create buttons with the appropriate text, set '#submit' => array('ahah_helper_submit'), and off you go. You’d probably create such a button for every AHAH-powered form item. The exact same code will be used as when JavaScript would be enabled. (If you’ve got a AHAH-powered select called ‘Usage’, you’d probably name the button ‘Update usage’. You get the point.)

    And thanks to these buttons, writing functional tests now becomes trivial as well. Because the same code is used when JavaScript is disabled (through the buttons) or enabled (through AHAH callbacks), just press the buttons in your tests and you’ll be fine!

  4. skip form validation for form items that exist in the form for the first time (i.e. that are added dynamically). Check if the #first_time property exists in your validate callbacks.
  5. have new AHAH-powered form items added in an AHAH callback (previously not supported).

Try it!

This module is being used right now on Mollom.com, so it’s ready for production. This also obviously is a contribution of Mollom towards the Drupal community, so don’t just thank me (supposing that you’re thankful), but also thank Dries Buytaert and Benjamin Schrauwen.
The module obviously is available at Drupal.org and is considered ready for production use. A demo module is included, which allows you to see what the code looks like. Download it right away!
As always, constructive criticism is welcomed, so see you in the issue queue!

Plans

While it works really nicely, I’m sure some aspects are eligible for improvements. I’m aware that a couple of things could be made easier — look at the included TODO.txt.
I talked to Nathan ‘quicksketch’ Haug — who wrote the current AHAH forms support — about this, here at DrupalCon Szeged. He immediately agreed that this really really needs to be fixed. So it will make it into core as well, although probably not in the current shape.

Note

I made AHAH forms work in Drupal 5, because I needed it for my Hierarchical Select module. I haven’t had the time yet to do a write-up about this, let me know in the comments if you’d like me to do so.

Comments

fago's picture

Great work, thanks! This is really really useful!

Have you noticed the problems when using storage and cached ahah forms together? → http://drupal.org/node/302240 It looks like you set the storage during the form built too, so this issue should apply to your solution too. chx told me that storage currently is supposed to be set during #submit - I think we should improve that so that the form_state can be used for #cached forms without troubles too.

Greg's picture

Good post.

Btw, what is the input filter you’re using for PHP code which links function names to the Drupal API website? Are you doing it yourself, or is it an automatic input filter I’ve missed? I use the Code Filter module, but it doesn’t do that. That’s really cool! B)

Wim Leers's picture

Wim Leers

I’m using the Drupal GeSHi filter module, and I’m using the “Drupal” syntax. Easy to set up, works great :)

Fabio Accetta's picture

Fabio Accetta

Hi, i’m a newbie about Drupal 6 Ahah and i have known about “Ahah helper” project. I like very much your work and i’d like to talk to you about questions and features. Any help or answer is appreciated ^_^

First, i see that, when changing the select value on the form, a post request automatically submits the form (infact the messages about empty values appear). Is it possible to avoid automatic submit? Or i miss something?

Second, i tried to modify ahah_helper_demo to get my job to work. So i added the “file” property into menu items (ahah_helper_demo_menu) and moved all remaining functions and hooks (ahah_helper_demo.module) into “tables/functions.inc”, being “tables” a sub-folder of ahah_helper module, inside site/all/modules/. Doing so, cleaning cache and moving again at drupal/ahah_helper_demo, first of all i notice (thanks to a var_dump of a variable) that the page is loaded twice and subsequently, generating vat errors with fake values, error message “Invalid Vat number” doesn’t apper during blur event (only the waiting icon). This error message, instead, appears if i submit the form for the first time, and it keeps working (through ahah calls) also after new blur events with fake values. Perhaps i miss something else? Can’t i move my ahah functions to separated files? Is this a bug?

Many thanks and best regards. Fabio

dropcube's picture

dropcube

So it will make it into core as well, although probably not in the current shape.

Is there any issue/dicussion to make this kind of stuff into core.

Ahah forms in drupal 6 | Menhir-effect blog's picture

[…] to discribe. I found two nice articles on this: drupal 6 AHAH forms: the easy way by Nick Lewis and AHAH helper module by Wim […]

ppblaauw's picture

In the module you use default values like:

$form_state[‘values’][‘billing_info’][‘company_name’]

How can I use saved default values the first time a user enters a form and use the changed value if the user changed something.

Also default values from form items which don’t change depending on changing the select. How do i change these default values.

E.g in your demo a user enters a private address and saves and then changes the select to company the default values from the private address are used. How to change this to an already saved address or an empty value.

Thanks

ppblaauw

Dominic's picture

Thanks so much for posting this - you are an utter genious. Have been going round in circles with Drupal forms and AHAH for ages and you’ve nailed it! Thanks again.

Dominic's picture

I’ve got it fine with the code above to alter a form layout based on a drop-down box value - just got stuck trying to implement a simple button to add a field.

Like ‘enter your favourite colours in the (three) fields below’ - click the button to add a fourth. Clicking the button adds another field to the form.

The Drupal forms API ‘button’ seems to act as a ‘verify form and redraw’ command as far as I can see - can’t get it to override and just add a field in the same way that the drop down box works.

Any help gratefully received - thanks.

D

Claudiu Cristea's picture

Claudiu Cristea

I made AHAH forms work in Drupal 5, because I needed it for my Hierarchical Select module. I haven’t had the time yet to do a write-up about this, let me know in the comments if you’d like me to do so.

I’m interested how can be the above code backported to Drupal 5 while we miss there some functions like form_get_cache().

Thank you!

Wim Leers's picture

Wim Leers

If you’d have asked back in the beginning of October, I might have been able to find the time to do a write-up. Right now however, this is impossible due to time constraints.

Ask again in the beginning of February :) If you can’t wait that long, you’ll have to analyze for yourself how Hierarchical Select does it. Hint: start by looking at the #after_build callback, that’ll link you to everything else.

Claudiu Cristea's picture

Thanks for your reply! Of course February is too far… so I decide to give a try. I’m using AHAH Forms to “inject” new markup in the existing form on-the-fly. That’s because the wonderful Drupal 6 #ahah property is not available in Drupal 5.

I looked into Hierarchical Select to understand how you make this happens. I wrote a piece of code but it still fail to work. I’m expecting that the $form_values array from test_form_validate() and test_form_submit() to contain the new form element ("other_text")… but it does not. Bellow is my try. Maybe you can take a look. Maybe I miss something simple…

function test_menu($may_cache) { $items = array(); if(!$may_cache) { $items[] = array( 'path' => 'subform/add', 'callback' => 'test_subform_add', 'type' => MENU_CALLBACK, 'access' => TRUE, ); } return $items; }

function test_form() { $form['some_text'] = array( '#type' => 'textfield', '#title' => t('Some text'), ); $form['button'] = array( '#type' => 'submit', '#value' => t('Show subform'), '#ahah_bindings' => array( array( 'event' => 'click', 'wrapper' => 'subform_div', 'path' => 'subform/add', ), ), ); $form['wrapper'] = array( '#value' => '

', ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), ); $form['#after_build'] = array('test_after_build'); return $form; }

function test_subform() { $subform['other_text'] = array( '#type' => 'textfield', '#title' => t('Other text'), ); return $subform; }

function test_subform_add() { $test_form_build_id = $_POST['test_form_build_id']; $cached = cache_get($test_form_build_id, 'cache'); $storage = unserialize($cached->data); $form_id = $_POST['form_id'] = $storage['parameters'][0];

$form = call_user_func_array('drupal_retrieve_form', $storage['parameters']); drupal_prepare_form($form_id, $form);

$subform = test_subform(); $output = drupal_render($subform);

print $output; exit; }

function test_after_build($form, $form_values) { if (!isset($POST['test_form_build_id'])) { $parameters = (isset($form['#parameters'])) ? $form['#parameters'] : array(); $storage = array( 'parameters' => $parameters, ); $expire = 21600; $test_form_build_id = 'test_form'. md5(mt_rand()); cache_set($test_form_build_id, 'cache', serialize($storage), $expire); } else if (isset($_POST['test_form_build_id'])) { $test_form_build_id = $_POST['test_form_build_id']; }

$form_element = array( '#type' => 'hidden', '#value' => $test_form_build_id, '#parents' => array('test_form_build_id'), ); $form['test_form_build_id'] = form_builder($form['form_id']['#value'], $form_element); $form['#submit']['_test_submit'] = array($_POST['test_form_build_id']); return $form; }

function _test_submit($form_id, $form_values, $test_form_build_id) { cache_clear_all($test_form_build_id, 'cache'); }

function test_form_submit($form_id, $form_values) { dpm('SUBMIT:'); dpm($form_values); }

function test_form_validate($form_id, $form_values) { dpm('VALIDATE:'); dpm($form_values); }

Thanks!

Wim Leers's picture

Wim Leers

In my opinion, that module is badly written and even worse in documentation. I found it very hard to understand and never got it to work reliably and in a manner that I could write simple, understandable, maintainable code.

I don’t recommend using AHAH forms (note the lowercase “forms”, meaning I’m talking in general and not about the module) in Drupal 5, unless you use Hierarchical Select’s technique and then put another layer on top of it to abstract all the AHAH stuff, i.e. if you also port AHAH helper to Drupal 5. If you don’t do both, AHAH forms are a pain to write and maintain. They’ll drive you crazy.

Claudiu Cristea's picture

Thanks Wim,

OK. I’m not using AHAH Forms… but how do I add HTML (form elements) to the page? In Hierarchical Select there is no obvious how to make this… I assume that is something related to JSON… But anyway… adding HTML fragments (form elements) works fine… I don’t know how to update the server-side part (form cache in D6?) of the form in order to have the new elements (added on-the-fly) available in the $form_values array in formname_validate() and formname_submit().

Any input on how to do this is very valuable…

Wim Leers's picture

Wim Leers

I don’t use a form cache. IMO this is a design flaw in the Forms API, and for example the AHAH helper module, which simplifies the code for AHAH forms a lot, doesn’t use it either. Well, it updates it as is expected by Drupal core, but it doesn’t use it to keep the “latest version of a form”. It simply uses the original version of the form to generate the new version and then extracts the piece that is being updated, renders it and prints it.

Hierarchical Select is a form element. So I don’t have to update any cache or form at all. I only maintain a state of what was selected previously, to be able to validate the selection in AHAH callbacks and in the final submit. Making entire forms AHAH-powered in Drupal 5 is more challenging than just a form element; it’d be a superset of features in comparison to what I did for Hierarchical Select.

I’m sorry, but I really can’t help you any more than this without actually diving into form.inc and figuring out how things work.

Kristen's picture

I beat my head for too long yesterday trying to get the #ahah stuff working… I looked at several examples and each had different code so I was having a hard time figuring out which pieces were necessary or not. Then, I was fortunate enough to find your ahah helper module and IT WAS A PIECE OF CAKE!!!!!!!!!!!!!!!!!!!!!! It definitely should go into the drupal 7 core! If you are ever in Santa Cruz, email me and I’ll take you out for some coffee and real cake.

Wim Leers's picture

Wim Leers

Heh :)

This is exactly what I hoped the effect would be … I too found it waaaay too hard and insanely non-self-explanatory how to get AHAH forms working. Glad you like it! :)

Anonymous's picture

Anonymous

I’m currently learning PHP fundamentals and also trying to wrap my head around FAPI and custom modules (starting to get it slowly but surely). I’m creating a Drupal 6 site for which I want most of my content to be linked to a city ( I didn’t like the way the location module handled this ) for categorizing and mapping.

I created and populated 3 relational tables in my Drupal DB with data for most of the world’s cities. Tables: 1. continents (fields: continentID, continentName) 2. countries (fields: countryID (PK), continentID (FK), countryName, latitude, longitude) 3. cities (fields: cityID (PK), countryID (FK), cityName, region, latitude, longitude)

Now I want to create a hierarchical select form so users can filter through the content select continent → select country → select city AND I also want the same type of form to link city data to my content during content creation (user’s travel journal for example). On submission the form will populate a table that relates city data to each journal entry.

Can Hierarchical Select do this? Or can I use FAPI and AHAH Helper on a custom module? Or do I have to find another way without either of your awesome modules?

I know you’re extremely busy so any tips to steer me in the right direction would be greatly appreciated.

Thanks, Freddy

Wim Leers's picture

Wim Leers

Just a couple of days ago, I committed the Drupal 6 port of HS. HS itself is very stable, as it has barely changed in the port. It’s the integration with other modules that’s not yet finished/stable. So, HS itself is stable to use.

This makes writing a module that implements HSAPI the easiest option. You could write something yourself using AHAH helper, but why would you, if you can take advantage of all the features of HS? :)

Look at the included API.txt for all documentation and at the existing HS API implementations in the “modules” directory of HS. Got more questions? Please ask them in HS’ issue queue!

Freddy's picture

Freddy

I didn’t even know HS had an API. You’ve just made my day. As I’m fairly new to Drupal finding the right info is half the battle for me. I’ve been stuck on this for days.

I almost posted my original question in the issue queue but I didn’t know if it was the right place, now I know.

Best of luck in your studies and thanks again!

Wim Leers's picture

Wim Leers

Development-oriented questions can always be asked in #drupal. That’ll help you get to the right resources much faster :)

Freddy's picture

Freddy

I just wanted to thank you for your awesome AHAH helper module which I made my Hierarchical Select clone (Continent→Country→City) form with. You suggested I use HS API for this but my lack of coding experience prevented me from understanding it. AHAH helper was a different story however, mainly due to the included demo module. I was even able to replace my city select field with an autocomplete field, which makes the form a lot more user friendly IMO. To think a couple of weeks ago I didn’t even know what a callback function was I thinks speaks to the awesomeness of your module.

Besides a few issues I had to work out (autocomplete form items don’t like being inside if statements for instance) I’ve almost got it working the way I’d like.

Also thanks for the #drupal tip, it’s really cool. Freddy

oligoelemento's picture

oligoelemento

I have tried to create a node with an AHAH helper module form, but when I choose the desired option in the select list ($form[‘education’][‘edulevel’]) then the form elements dissapear. I think is a problem with $form_state, but I’m a Drupal starter and I don’t know how to solve this. I’d thank any suggestion.

<?php function idibay_profile_form(&$node) { $type = node_get_types(‘type’, $node); $form = array(); ahah_helper_register($form, $form_state); if ($type→has_title) { $form[‘title’] = array( ‘#type’ ⇒ ‘hidden’, ‘#required’ ⇒ TRUE, ‘#default_value’ ⇒ $node→uid, ); } $form[‘education’] = array( ‘#type’ ⇒ ‘fieldset’, ‘#title’ ⇒ t(‘Education Data’), ‘#prefix’ ⇒ ”, // This is wrapper div. ‘#suffix’ ⇒ ”, ‘#tree’ ⇒ TRUE, ); $form[‘education’][‘edulevel’] = array( ‘#type’ ⇒ ‘select’, ‘#title’ ⇒ t(‘Education level’), //’#default_value’ ⇒ isset($node→edulevel) ? $node→edulevel : ”, ‘#options’ ⇒ array( ” ⇒ t(‘- Select -‘), ‘Bachelor’ ⇒ t(‘Bachelor’), ‘Master’ ⇒ t(‘Master’), ), ‘#default_value’ ⇒ $form_state[‘values’][‘education’][‘edulevel’], ‘#ahah’ ⇒ array( ‘event’ ⇒ ‘change’, ‘path’ ⇒ ahah_helper_path(array(‘education’)), ‘wrapper’ ⇒ ‘education-wrapper’, ), ); $form[‘education’][‘update_edulevel’] = array( ‘#type’ ⇒ ‘submit’, ‘#value’ ⇒ t(‘Update education level’), ‘#submit’ ⇒ array(‘ahah_helper_generic_submit’), ‘#attributes’ ⇒ array(‘class’ ⇒ ‘no-js’), ); if ($form_state[‘values’][‘education’][‘edulevel’] == ‘Bachelor’) { $form[‘education’][‘bachelor_name’] = array( ‘#type’ ⇒ ‘select’, ‘#title’ ⇒ t(‘Detailed education’), ‘#options’ ⇒ array( ” ⇒ t(‘- Select -‘), ‘bacmed’ ⇒ t(‘Bachelor in Medicine’), ‘bacvet’ ⇒ t(‘Bachelor in Veterinary’), ), ‘#default_value’ ⇒ $form_state[‘values’][‘education’][‘bachelor_name’], ); } else { $form[‘education’][‘master_name’] = array( ‘#type’ ⇒ ‘select’, ‘#title’ ⇒ t(‘Detailed education’), ‘#options’ ⇒ array( ” ⇒ t(‘- Select -‘), ‘masbiof’ ⇒ t(‘Master in Biochemistry’), ‘maschem’ ⇒ t(‘Master in Chemistry’), ), ‘#default_value’ ⇒ $form_state[‘values’][‘education’][‘master_name’], ); } return $form; } ?>
Drupal module tutorial pt.2 &amp;amp;laquo; Taki sobie blog ;]'s picture

[…] Ponadto jego używanie jest tak mało intuicyjne, że aż powstał moduł mający temu zaradzić (AHAH helper module).     Poza tym w kodzie widać moje pierwsze próby przekazywania zmiennych za […]

Jeff Brown's picture

Jeff Brown

Hi Wim, I found your module from links in the ‘definitive’ AHAH document:

http://drupal.org/node/331941

I follow most of what’s going on with your code, and I follow the case they represent.

I think it would be nice if you had a full demo, where you do something more than just a toggle the view. For instance if you want to do anything with the data, then you need to have the ‘#submit’ functions defined,

And, I strongly suggest that anyone doing something with the form data, should move an ahah_helper_register() call to inside the submit function as well. You still need it in your form function to set $form_state[‘storage’][‘#ahah_helper’][‘file’].

If you don’t put the ahah_helper_register in your submit, you can get strange results where the ‘storage’ data lags to the last request.

I suggest that you break ahah_helper_register into two functions one (the original), which does the ‘file’ storage for registering. The second function could be called ahah_helper_migrate and that one should do the ‘values’ munging into ‘storage’ and some of the other tricks.

For now, anyone pushing ahah_helper in this way needs to remember to include ahah_helper_register() in your ‘#submit’ callback function.

I hope that this saves someone the 12 hours it took me to figure out.

frosties131's picture

frosties131

I think it would be nice if you had a full demo, where you do something more than just a toggle the view. For instance if you want to do anything with the data, then you need to have the ‘#submit’ functions defined

I totally agree : i can see the examples here, they seem REALLY attractive…but for a newie like me, i can’t figure out how to make it work on my own website. It’s such a pity : you made a real great job, but it’s not accessible to me (surely because of my lack of knowledge in Drupal) !

Thanks anyway !

Ankit Babbar's picture

Ankit Babbar

First of all thank you for this module….. Can you help me achieve file uploading using this module

Terri's picture

Terri

hi,

Due to a bug with Drupal 6’s AHAH implementation, I am trying to see if I could use your module instead. I’m not sure whether this is the proper place to ask you questions, but I am unable to get even the demo to work as is; the error message is:

An error occurred /ahah_helper/billing_info (no information available)

Is there something else I need to do to?

Thanks!

Terri's picture

Terri

I repeated the process on a different machine at work (it runs on a VM, so everything is supposed to be identical), and there it worked! So while I am glad, I am also mystified as to why there would be a difference in the execution.

Any suggestions?