Starbow Consulting

Home

Tao's Blog

Contact

Search

Review of "Drupal 5 Views Recipes"

The Problem with Popups

Effort to fix Install Profiles in Drupal 7

The joys of following directions (cron.php edition)

Those clever clever spambots

Popups API Modules in Action

Good on your 2 days 21 hours ago

but you tried! 3 days 18 hours ago

policymakers ratified cause 4 days 7 hours ago

assumptions rays differing iii slow 4 days 7 hours ago

melts per 4 days 7 hours ago

probably primary stricter feedback 4 days 7 hours ago

Thanks for writing a review 6 days 5 hours ago

Thanks! This is the first 6 days 6 hours ago

Modal Windows 3 weeks 3 days ago

Usability / user experience 3 weeks 3 days ago

tstarbow: Thought my first book review might be my last, but it looks like packt doesn't blacklist you for being honest. Nice to know.

tstarbow: Milo's 1st birthday party was great (dispite google event system invites only getting to half the invite list)

tstarbow: Having coder 6-1.0 around, even unenabled, kills Drush. Easy to find the all issues now that I now the cause.

tstarbow: @add1sun Wow, how did I live without knowing about polkatronica? Thanks!

Setting up Eclipse PDT and XDebug

A dynamically expanding AJAX/AHAH Drupal form element.

CSS Drop Down Primary Links

Wikis in Drupal 6 - Module Review

Notes on Drag & Drop with Interface

Ahah_forms reloaded

Request new password

ajax

Drupal

formAPI

_*Update*: I stand by most of the information in this article, but I have learned a couple of things in the last two month. I now recommend against pulling information directly out of $_POST or using form_builder. My Ahah Forms package can make a lot of this easier. Please checkout Secure Dynamic Forms and Subforms, Ahah_forms reloaded, and the examples in the Ahah Forms package._

Goal To create a form widget that consists of an unlimited number of subwidgets, expands without needed a full page refresh, and can be easily added multiple times into any Drupal form.

Plan Use hook_element to define the element, hook_nodeapi to handle the db interactions, and jQuery-based AHAH code to do the incremental page updates (AHAH – Asynchronous HTTP And HTML , not AJAX b/c there is not XML involved, and people seem to think AJAH isn’t sexy enough).

Approaches Rejected for attaching element to form

Using hook_form_alter and nodeapi, like upload or event – rejected b/c you can only add a single field to a form with this method.

CCK – currently there is no great way to deploy cck based solutions (import/export has been added, so this problem is probably going away).

Just += widget_create(), which works fine, but doesn’t seem to be The Drupal Way

Defining the dynamic Widget element We start by defining out new widget element with hook_elements() (To follow along with the code, download the module as a zip).

I would love to define the element’s #process function here, but unfortunately we need the $node to do the expansion when we are doing an edit of an existing node with subwidgets attached. So we are forced to add our element into forms like this:

Sadly non-elegant. Hopeful there will the a more aesthetically appealing way to do this in the future.

Now, when the main form gets built, widget_expand will be called. The widget_expand function, is responsible for doing some bookkeeping: it attaches the JavaScript files; marks the node type as containing a widget; builds a tree in the form for the multiple widget names; and builds the wrapper for the dynamic bits. Then it calls _widget_build_widget.

This bit took me a while to work out, it creates a tree in the form that grows for each widget that gets added into the same form.

Similarly, this code builds the same array and passes it to the JavaScript (and handles getting the basePath to the js):

The _widget_build_widget function is where our dynamic widget actually gets built. It will also be called by the JavaScript, so it needs to be discreet from the bookkeeping that only gets called once per full page load. This function builds the widget element in four steps, each broken out into a helper function:

Often useful Form API Reference: (http://api.drupal.org/api/5/file/developer/topics/forms_api_reference.ht...) Helpful handbook page: Adding a custom element type & expanding elements (http://drupal.org/node/37862)

Incremental Form Updates: AHAH! Now it gets interesting. We made sure our JavaScript got added to the page in widget_expand. Let’s see what widget.js does. Attaching the JavaScript events

Last things first. The last line of the js file is:

This gets run automatically as soon as the page is ready to go. In this context “attach” mean binding a jQuery listener to the widget UI elements that we want to respond to.

Widget.attach_all just iterates over the widget name array created back in widget_expand:

Widget.attach_widget is responsible for binding each widget’s form elements:

The hard work was done in the form generation, to make sure each form element had an id or class that allow us to attach to it. (One thing to watch out for with JQuery: $().click( fn ) registers a listener, but $().click( value ) actually triggers the event). I got bit by a bug trying to bind to the button’s id, but was able to use the class name. For each event type, we register the same listener. Everything the widget changes we are going to regenerate the entire widget and swap out the old view.

After all this setup, in Widget.update_widget we finally get to do our slick AHAH magic:

// build the params to post back to drupal var params = $('#node-form').formToArray( true ); // collect the form's parameters params.push( {name:'widget_name_js', value: widget_name} ); // add widget_name to params basePath = Drupal.settings.widget.basePath0; uri = basePath + "widget/widget_update_js";

$.post( uri, params, function(data) { $('#widget_wrapper_' + widget_name).html( data ); // replace the old widget with the new one $('#widget_wrapper_' + widget_name).highlightFade(); Widget.attach_widget( widget_name ); // re-attach event listeners to newly generated html $(this).attr("style", "cursor:auto"); } ); }

Here we gather up the parameters from the form (fromToArray), add in the name of the widget that has changed. Then we post back to Drupal, which returns the html for our updated widget in the data variable. Then jQuery .html method is used to replace the old widget with the updated html. highlightFade is a jQuery plugin to give the traditional AJAX “look at me, I have changed” fading yellow flash. Then it is essential that we reattach listeners to the new html.

Generating the Replacement Widget We have covered generating the initial widget when the form page is originally created, and how the JavaScript swaps out the old widget with the updated one. But we still need to create the html for the updated widget, back in the widget.module.

First we need to declare a callback in widget_menu to from the path “widget/widget_update_js” to the function _widget_update_js(), which just pulls the needed info out of $_POST and hands it to _widget_build_widget_js:

This function creates a tree, but without the div.widget_wrapper that is created in widget_expand. The jQuery.html call replaces all the html inside the div, but leaves the div alone. The form_builder is called to do it’s behind the scenes magic, and drupal_render turns it into html, suitable for returning to the jQuery.post that requested it.

Connecting up the database

Widget_expand handles all of the form interactions, storing the subwidget data in the post between requests, but eventually that info needs to get stored in the database. This is where hook_nodeapi comes in. It’s a little clunky moving from the formAPI side of the world back to the node side, but we need a nid to store the info. I am not going to talk about this bit much, there are plenty of other nodeapi resources on the web. The only tricky bit here is dealing with the deep array. Insert flattens it, and load re-expands it. Also, I am being lazy on update and just doing a delete all old and reinsert all new.

Hook_nodeapi docs: (http://api.drupal.org/api/5/function/hook_nodeapi)

Drupal Bugs & Limitations

<?php $form [ ‘foo ] = array( ‘ #type’ => ‘button’, ‘#attributes’ => array( ‘id’ => ‘my_button_id’ ) ); ?> produces incorrect HTML. It kicks out an input with two id’s, the one I requested, and the default id. I need to see if there is an open ticket for this! Update: nadjo pointed me to the form #id property. Yay!

produces incorrect HTML. It kicks out an input with two id’s, the one I requested, and the default id. I need to see if there is an open ticket for this! Update: nadjo pointed me to the form #id property. Yay!

<?php $form [ ‘foo’ ] = array( ‘ #type’ => ‘item’, ‘#attributes’ => array( ‘id’ => ‘my_item_id’, ‘class’ => ‘my_item_class’ ); ?> does nothing – probably just a documentation issue.

does nothing – probably just a documentation issue.

<?php #process => array( ‘fn’ => array(‘arg1’) ); ?> callsback to function fn( $element, $unknown, $arg1) – I haven’t been able to figure out what the $unknown is or why it is there.

callsback to function fn( $element, $unknown, $arg1) – I haven’t been able to figure out what the $unknown is or why it is there.

It would be great if there was some flag to signal that your element wants the node in hook_element, so the #process is not needed by the including form.

I include the jQuery form plug in, just for the formToArray function. This makes posting back to Drupal much easier, and something like it would be a good candidate for inclusion in drupal.js

Final Thoughts

All the pieces are in place for creating JavaScript-enabled dynamic forms in Drupal, and it isn’t rocket science, but it can be very fussy work. You need to hold multiple models of the same data in your head at the same time – the formAPI array, the node object, the generated HTML and the JavaScript. It is very easy to get the nesting wrong or attach the JavaScript to an incorrect key. Being able to debug both the php and JavaScript while the page executes is key. I used Eclipse PDT with XDebug for the PHP, and the Firebug extension for Firefox to debug the JavaScript, and to examine the HTML in real-time as it changed.

It was roughly 10 times harder to get this working with multiple widgets on a page, compared to a single one. Keeping track of which widget was making a request, and getting that info everywhere it needed to be was the biggest challenge. Also, I probably shouldn’t have called the example widget, since that is the same term CCK uses, but such is life.

Attachment

Size

widget.zip

12.75 KB

screenshot.png

91.87 KB

Add new comment

cool

reply

Ahah was the best thing that could happen in Drupal.

Thanks a lot

Mario Moura

Hi, Great Module, I have been trying to use this tool the last few days but my major problem right now is how to use the params array. In my problem I need pass the value of the selected index of a select form to the callback function because I need it generate results to use for another select box. How do I access the params? Thanks.

thank you for the write up, i was looking for such function.. do you have plan to update it to Drupal 6? and also made available on drupal.org?

Drupal 6 has this functionality built into it.

how do you group fields together in D6, like in the example screenshot – so that TWO fields are added as a line item.

at the moment all i can seem to do is just add one field as a line…

drupal 6 certainly has the ability to have a dynamic “add more” on a per field basis, if you configured your custom field to have “unlimited” values – but how do you make the “add more” apply to more than one field -as per your example?