Monday, 25 March 2013

The pyramified Drupal double search ajax form

I was working on a complex form today and thought I share my use case. It was one of those forms where you have to update the items in order to reflect a certain state.

Imagine a huge tree. (Not a real tree, it's tech blog, come on.) Each node has zero to N children. It represents relating nodes in Drupal. Now imagine you build a form, each consecutive item is the next level in the tree. We're talking about big numbers, so you just cannot show them all at once. Neither you can use a simple autocomplete as you don't want to remember the names. So what we need to do is setting up a dependency list. When you select the Nth item it will prepopulate the (N+1)th item. So you only go one level further. And that's a convenient experience to select the deeper levels.

Now the funny thing is that this is a search filter form. And if you see what we did above - we basically made the search form a search form.

Now the next step is to make the form responsive. Again - selecting a value will update the consecutive field's values. I was a little bit scared of using the Drupal Form Ajax API - thought it's gonna be a massive multi-level array-juggling. But apparently it's dead simple. You define an ajax callback on the form item that triggers the update. In my case I added it to N-1 form items (all except the last):

$form['item_i'] = array(
  '#type' => 'select',
  '#options' => get_options_for('item_i'),
  '#ajax' => array(
    'callback' => 'on_change_item_i',
    'wrapper' => 'item_j_wrapper',

Here function on_change_item_i() will be called whenever I change my value on the form. This callback expects one thing to be returned - the new updated form item:

function on_change_item_i($form, $form_state) {
  return $form['item_j'];

item_j is following item_i in my example. How this code knows which HTML node to update? That's the wrapper parameter in my previous form definition - an ID string. So let's quickly wrap the next form item:

$form['item_j'] = array(
  '#type' => 'select',
  '#options' => get_options_for('item_j'),
  '#prefix' => '<div id="item_j_wrapper">',
  '#suffix' => '</div>',

That's the basics, really not scary, am I right? Now comes the fun part. What happens when you selected the dependency from item 0 to i (i > 2) and you changed your mind somewhere between 0 and i-2? It's a tricky one. First you need a model that actually swaps subtree and not just updating the following item. From our example it's clearly visible that one field change only updates 1 item. In Drupal we came to the obvious conclusion - subarrays. If we make something like a pyramid - by updating the next level actually updates all other levels below it. That's perfect:

$form['item_0'] = array(/* ... */);
$form['item_0']['item_1'] = array(/* ... */);
$form['item_0']['item_1']['item_2'] = array(/* ... */);

So when you call the ajax callback via the field update and replace the following field you actually replacing all following fields. Sounds right. Except one thing - you still see that updating a field will only clear/update the next on field. It's maybe irrational to think here but I give you a little time to think about why - avoiding cheating here you are my tinkering image:

All right. So it's the sneaky form_state array. Since you're building up your form and values by each previous field states you have to use the form_state array when building the form. Now the ajax update will indeed remove the following item - because you maybe deselected a value - but form_state still contains the selection for the emptied item - thus the second one will still remain the same. And that can easily cause an invalid form error.

Now what? Now what has to happen is whenever you collect the values for a form item you have to synchronize the form_state array. Because that's kinda the old state - before you did the update. By doing so you can indeed go through the whole subtree and refresh all the following form items.

By using this technique I changed my fields to multi-value fields (selects to lists) and it perfectly reflected all state changes. As it was a list I could select multiple subtrees - and when I removed items randomly the appropriate subtrees were removed from the composition.



No comments:

Post a Comment