drupal

The dual aspect of Drupal forms and what this means for your AHAH callback

Over the last week or two I've spent a lot of time on an aspect of my Quick Tabs module that I am certain none of its users will care a hoot about. It wasn't a case of adding a new feature or fixing a bug or even improving usability, but a question of, to put it succinctly, cutting down on its evilness. The admin form for creating and editing Quick Tabs blocks (where you choose either a block or a view for each tab) had a serious amount of ahah functionality: click a button to instantly add a new tab, click a button to instantly remove one of your tabs, select a view for your tab and have the view display drop-down be instantly populated with the correct options for that view. It was pretty user-friendly; there was just one problem: it flew in the face of Form API best practices.

When it comes to AHAH forms, there's the JavaScript side of things - where a behaviour is attached to, say, the onclick event of a button and new content is retrieved and inserted into the DOM - and then there's the PHP side of things, the AHAH callback where you rebuild your form with new or altered elements. Now, my understanding of FAPI voo-doo had been poor to begin with - I had competely copied poll.module for this side of things - and so when chx told me there was bad stuff going on in my AHAH callback and tried to point me in the direction of FAPI righteousness, I was pretty lost. Thankfully, he took the time to go through it with me, discovering that poll module was flawed in the same way, and though I didn't attain enlightenent straight away, the murk did eventually clear, I cleaned up my AHAH callback, and I think I have reached a stage where I can explain the crux of the problem to others.

You see, you need to think of your module's form (or any Drupal form) as being essentially dual in nature: there's the "material" form - what you actually see rendered on the page - and then there's the "spiritual" form, if you will, the form that exists out there in the ether (aka the form cache :-P). The material form and the spiritual form need to be representative of each other at all times. In fact, the material form should always be a "physical" manifestation of the spiritual form. If either gets altered without the other being altered accordingly, bad things will happen - at best, your form won't work properly, at worst, you will open the gates to FAPI Hell. For example, if you successfully add new elements to the spiritual form, so that the version of the form held in the cache now contains 4 poll choice textfield elements, but the material form, the form rendered on the page, still only has three poll choice textfield elements, then when you hit submit it will give you a validation error to the effect that you have left the fourth field blank, assuming such validation has been applied. On the other hand, say for example you have a dependent dropdown, where you populate the options in one dropdown based on a selection in another, if those new options don't exist in the spiritual form, you will get an error to the effect that "an illegal choice has been detected" when you submit. There are good and bad ways around this latter problem.

One way, the way many modules up to now, including my own, were doing it was to retrieve the form from the cache, tack on the new element, re-save it to the cache, rebuild the form and render the altered portion. This is illustrated below:

<?php
function myform_ahah() {
 
$delta = count($_POST['addable_elements']);

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

 
// 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 element's 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['my_ahah_wrapper']['addable_elements'][$delta] = $form_element;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );

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

 
// Render the new output.
 
$form_portion = $form['my_ahah_wrapper']['addable_elements'];
  unset(
$form_portion['#prefix'], $form_portion['#suffix']); // Prevent duplicate wrappers.
 
$form_portion[$delta]['#attributes']['class'] = empty($form_portion[$delta]['#attributes']['class']) ? 'ahah-new-content' : $form_portion[$delta]['#attributes']['class'] .' ahah-new-content';
 
$output = theme('status_messages') . drupal_render($form_portion);

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

The main problem here is that in between the form being re-saved to the cache and it being rebuilt, it is further altered by the lines

  $form += array(
    '#post' => $_POST,
    '#programmed' => FALSE,
  );

$_POST should not be used at all in the rebuilding of the form. Everything should come from $form_state, so that the rendered output is still a "physical manifestation", as I put it earlier, of the form in the cache.

In order to achieve this, a couple of things need to be done differently, the ahah callback needs to be altered - but this actually involves mostly removing code; and the function that generates the form needs to be structured so as to react to the contents of $form_state (when building the form it first checks $form_state for previously submitted information, then checks if it is receiving information from the database, then finally if it is not receiving information from anywhere, it renders a brand new clean form). The ahah callback then looks like this:

<?php
function myform_ahah() {
 
$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;
 
drupal_process_form($form_id, $form, $form_state);
 
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
 
$form_portion = $form['my_ahah_wrapper']['addable_elements'];
  unset(
$form_portion['#prefix'], $form_portion['#suffix']); // Prevent duplicate wrappers.
 
$output = theme('status_messages') . drupal_render($form_portion);
 
drupal_json(array('status' => TRUE, 'data' => $output));
}
?>

The form is retrieved from the cache, processed, and rebuilt. During rebuilding, $_POST gets destroyed and the form is re-saved to the cache. You can see that there is no room for alterations to the rendered form that aren't mirrored in the cached form - so, then, where do changes to the form take place? Well, changes happen not in the ahah callback but in the submit handler for the element that triggered the callback. Your form is going to get completely rebuilt, so in the submit handler, which gets called during drupal_process_form, you can specify exactly how you want it rebuilt. Here's my submit handler for the "Add tab" button in quicktabs:

<?php
function qt_more_tabs_submit($form, &$form_state) {
  unset(
$form_state['submit_handlers']);
 
form_execute_handlers('submit', $form, $form_state);
 
$quicktabs = $form_state['values'];
 
$form_state['quicktabs'] = $quicktabs;
 
$form_state['rebuild'] = TRUE;
  if (
$form_state['values']['tabs_more']) {
   
$form_state['qt_count'] = count($form_state['values']['tabs']) + 1;
  }
  return
$quicktabs;
}
?>

There are two important steps here: we're passing all the form values (what has been entered on the form) to $form_state['quicktabs'] so that when the form-generating function is called, it will check here first to see if there are already values available for the elements it's building; we are also incrementing the number of tabs by 1 and passing this number in $form_state['qt_count'], which is where we tell our form function to check first for the number of tabs to build. [Note: when I'm talking about tabs here I'm referring to sets of elements, where each set corresponds to a tab of content in a final Quick Tabs block - not to be confused with the myriad other meanings of the word in Drupal]. With those in place we know that the entire form can be rebuilt from scratch, will be rendered faithfully on the page, and will contain everything we need it to contain, new elements and all.

Here is the submit handler for the views dropdown on the quicktabs admin form:

<?php
function qt_get_displays_submit($form, &$form_state) {
  unset(
$form_state['submit_handlers']);
 
form_execute_handlers('submit', $form, $form_state);
 
$quicktabs = $form_state['values'];
 
$form_state['quicktabs'] = $quicktabs;
 
$form_state['rebuild'] = TRUE;
  return
$quicktabs;
}
?>

Notice that it doesn't seem to make any change to the form at all - the form is simply going to be rebuilt but with a different selected value for this dropdown (coming from $form_state), and that will change the options in the display dropdown. The views dropdown uses the very same ahah callback function as the "Add tab" button, as does the "Remove tab" button - they only differ in their submit handlers. The code is therefore cleaner, easier to maintain, and most important of all - more secure.

So, despite its dual nature, with its "material" side and its "spiritual" side, the Drupal form can attain perfect oneness when this approach is taken :-)

To see the full Quick Tabs code, visit http://drupal.org/project/quicktabs and download the latest dev snapshot of the 6.x-1.x branch (2.x branch to be updated shortly). To see the form in action, click here. You will need to change the tab type to "View" in order to see the views display dropdown working.

Further Reading:
Doing AHAH Correctly in Drupal 6 and beyond

Quick Tabs for D6 beta release - now with Views 2.0 integration, but was it worth it?

At last I overcame my fear of Views 2.0 and have added it to my D6 effort for Quick Tabs. (Previously my D6 version only allowed you to add blocks to your tabs.) My colleague, Hubert a.k.a. couzinhub, having quickly jumped in and familiarised himself with the wonders of Views 2.0 (well, after all he did help with the UI), helped me out with a very enthusiastic run-though of displays, overriding default displays, etc. and generally getting an overview of the lay of the land in the beta4 release of this super-module.

As far as Quick Tabs integration went, all I needed was a way of providing a drop-down list of all available Views so that people could add one for display in a tab. I had previously used the views_build_view() function then to display the view. There were a couple of hiccups in getting this to work in Drupal 6. Views_build_view() no longer exists but there is views_embed_view() which is slightly different because of the fact that you now have different displays per View. Probably the biggest challenge was getting a dependent drop-down to show all the displays for whichever View is selected. Let's just say I had an interesting journey through the Forms API, after my initial naïve effort that simply used AJAX to replace the options in the drop-down (synopsis: you can't add new elements or even select options to Drupal forms via AJAX without telling Drupal about them - see here for more info: http://drupal.org/node/150859). And the end result is a Quick Tabs creation form which may well get the prize for highest concentration of AJAX and AHAH craziness - which is not necessarily a good thing, as I'm worried my issue queue will prove soon enough.

Actually, the real low point came after I had done the bulk of the work and then suddenly it seemed that the new Views was so powerful it totally reduced the benefit of being able to add a View directly into a Quick Tabs block over just adding that View's block display (i.e. just adding a block that happens to come from a View). Well, fortunately, that's not quite true. The ability to use the same View in multiple tabs but passing in different arguments each time (an example of which is the "My Favourite..." QT block in my right sidebar) is, I think, justification enough for my continuing to include Views integration in Quick Tabs. I'm looking forward to feedback on this question.

Next on my to-do list: SimpleTest unit tests for Quick Tabs. And I might even request some Drupal Tough Love for it... if I dare!

Javascript Group

Looking for Szeged co-presenters for jQuery Tutorial

[Cross-posting from the Raincity blog]

I've submitted a proposal to the DrupalCon Szeged site for a tutorial session on jQuery in Drupal. I'd reallly like to get two co-presenters on board to structure it into three 30-minute tutorials, each covering two or three of the following suggested topics:

  • how jQuery and Drupal relate to each other
  • keeping up with jQuery versions
  • the Drupal js object
  • how behaviors are handled in Drupal 6
  • AJAXifying Drupal with jQuery
  • debugging JavaScript with Firebug
  • AHAH and Drag&Drop

The topics I'd personally like to cover are i) keeping up with jQuery versions and ii) the Drupal js object. Maybe debugging as well. I'm totally open to suggestions regarding specific topics; the aim is that we will provide a really well-rounded, instructive and comprehensive session on using jQuery in Drupal in 90 minutes.

So, if you are heading to DrupalCon Szeged, haven't decided on what, if any, sessions to present, and feel comfortable with and interested in the above topics, please get in touch with me via my contact form.

Alternatively, if you're not planning on presenting but would be interested in attending such a session, I'd love to hear your feedback as to the content so please leave your comments.

Hopefully helpful ramblings about jQuery in Drupal

I just posted Part Two of a fairly lengthy discussion of jQuery in Drupal over on the Raincity Studios website. Check out both installments:
The Lowdown on jQuery - Part One
The Lowdown on jQuery - Part Two

My DCV08 Presentation Slides

Drupal Camp Vancouver '08 was a roaring success and I enjoyed it enormously. There was a really interesting crowd at the event, with people from Seattle, Victoria, even as far away as Saskatoon and parts of Alberta. It was an amazing opportunity for networking and just getting to know more of the local (and obviously not-so-local) Drupal scene. My colleague Steve Krueger and I were delighted at the level of interest shown in our jQuery talks (we had to get moved to the big room because we couldn't all fit in our originally allocated space :-)). But then jQuery does somehow have that effect, probably because it's just so damn cool - and so putting it and Drupal together is a recipe for "sweet love" in the realm of code.

My presentation slides are below. Oh, and here's the link to the AJAX demo: http://www.katbailey.net/demo/node/56

Drupal Camp Vancouver

In March I attended my first DrupalCon and next weekend I will experience my first Drupal Camp - right here in Vancouver. OK, so it won't be quite the grand affair that DrupalCon Boston was, but there are lots of interesting talks lined up and it will be great to meet more of the local Drupalers. Some of the sessions that have caught my eye so far include "Building Community with Organic Groups", "Intro to Panels 2" and the session on the Forms API.

I'll be giving a talk on using jQuery with Drupal, aimed primarily at module developers, providing an overview of how the two fit together and discussing some tools and best practices.

Full details of Drupal Camp Vancouver can be found at http://drupalcampvancouver.org/

Introducing Slot Machine ... soon!

I've been working with Marco Carbone of Advomatic on a powerful content scheduling module. Marco originally built Slot Machine for FastCompany.com and it proved such a powerful tool for organising content display it was obvious that contributing it to the community was the next step. As its name suggests, the module allows you to 'slot' content into different queues according to topics, which come from a particular vocabulary, and define the update frequency for each slot in each topic. So, for example, you could have an 'Entertainment' page with a 'Latest Buzz' slot that updates every 4 hours, a 'Daily Image' slot that updates daily at 6am, a 'Latest Video' slot that updates every two days, etc... And then you could have a 'Technology' page with the same slots but different frequencies (maybe the technology 'buzz' isn't quite as fast and furious as celebrity scandals and the like...) Anyway, you get the picture.

So at Raincity I've been working on generalising this wonderful functionality into something that can be used on any D5 site - removing dependencies on other modules, adding an admin interface for creating your own feature types etc., and adding a simple system for displaying the slotted content. One of the more difficult tasks has been the generalisation of a feature which allows content to automatically get bumped down to a lower priority slot after it is removed from its original slot. Originally the slots in question were hard-coded into the module and so converting that into a scenario where you're saying "Let any content type that can reside in more than one slot rotate along them in order of priority" has been a challenge. But I think we're nearly there.

Anyway, it has been a great learning experience for me to work on this (thanks to NextScreen for sponsoring the work) and I look forward to us getting the first release out. Here's the project page:
http://drupal.org/project/slot_machine

jQuery in Drupal

I gave an introductory presentation on jQuery to the Vancouver League of Drupallers the week before DrupalCon Boston. It covered the basics of the jQuery framework and an explanation of how to use it in Drupal. Here is the slideshare of the presentation:

But of course at DrupalCon I was bombarded with new ideas about using jQuery, especially in Drupal 6. One of the main to-dos on my list since returning from Boston is to get up to speed on things like Drag and Drop, AHAH, the jQuery UI plugin in general and all the new ways that jQuery is being implemented in contrib modules. And who knows... maybe a jQuery presentation Part 2 will come of it...

Quick Tabs 1.0 Released!

I finally got over my "just one more tweak..." attempt at perfectionism and decided to release quicktabs properly. Yes, there are still features that need to be added, in particular better editing flexibility for existing quicktabs blocks. But the main functionality is there and there don't seem to be any serious bugs. And people want it! So here it is: http://drupal.org/project/quicktabs

And the blurb from the project page, in case you don't want to go there just now, that explains what it does:

The Quick Tabs module allows you to create blocks of tabbed content, specifically views and blocks. You can create a block on your site containing up to six tabs with corresponding content. Clicking on the tabs makes the corresponding content display instantly (it uses jQuery). The content for each tabbed section can be either a view or an existing block. It is an ideal way to do something like the Most Popular / Most Emailed stories tabs you see on many news websites.

Once created, the Quick Tabs blocks show up in your block listing, ready to be configured and enabled like other blocks.

Multiple Quick Tabs blocks can be placed on a single page.

For theming of the tabs, 9 styles have been provided for you to choose from, along with the default option of no style, if you prefer to add your own theming instead.