Drupal, webform and Dynamic Checkboxes

Below you will find my description of how I made a new component for Drupal's webform module that dynamically displays a selection of checkboxes. I will go over why I made it. Then I will explain the code step by step. I have also attached a copy of the file for download.

What's my motivation?

When I first approached this part of my project, I looked for examples of code that had already solved a similar problem. Unfortunately, nothing that I found did exactly what I wanted. However, I did find that there was a lot of other people looking for the same thing, so I promised to share my solution.

Please note that this component is just part of a larger registration piece that I built for a client site. It is not as robust as the core webform components, but I hope that others can use it as a framework to build similar components that they need.

The Code

I started by looking at the code for the webform select component and figuring out how I could modify it to do what I wanted. The first piece was fairly obvious. So, I created a new file in the webform/components directory called dynamicselect.inc and added the function below. Anyone who has done any Drupal development will recognize it.

<?php
function _webform_help_dynamicselect($section) {
 
  switch (
$section) {
    case
'admin/settings/webform#dynamicselect_description':
     
$output = t("A dynamic list of upcoming events.");
      break;
  }
 
  return
$output;

}
// end function _webform_help_dynamicselect($section)
?>

The form that makes the form

Next, I had to build the form element that would be used by the webform module to set up my dynamicselect element...

Confused? So was I. This is somewhat complicated. Here is what happens:

When you create a webform you have to fill out a form with things like the form's name and description and any other fields you might fill out to create any type of node.

In addition, you have to add the form elements, like textfields and markup. When you add the element, you are redirected to another form that asks about the element's properties, like its key value and whether or not it is mandatory. When you create a new component you have to create the form that asks these questions...

Maybe it would be better if I just showed you the function.

<?php
function _webform_edit_dynamicselect($currfield) {
 
 
$edit_fields = array();
 
$edit_fields['value'] = array(
   
'#type'          => "textfield",
   
'#title'         => t("Default value"),
   
'#default_value' => $currfield['default'],
   
'#description'   => t("The default value of the field.") .
     
"<br />" . webform_help("webform/helptext#variables"),
   
'#size'          => 60,
   
'#maxlength'     => 255,
   
'#weight'        => 0,
  );
 
  return
$edit_fields;

}
// end function _webform_edit_dynamicselect($currfield)
?>

That's it. Actually, all of this is just cut and pasted from the select component. All I am doing is setting up a field where you can enter a default value for your instance of the dynamicselect component. All the other form items like key, name, description, etc. are added later by the webform module.

Just in case you are wondering, I did not make a _webform_edit_validate_dynamicselect() function because I did not need to do anything beyond the built-in validation.

Rendering the dynamicselect element

Now, I need the code to actually build the dynamicselect element when the webform is displayed.

<?php
function _webform_render_dynamicselect($component, $data = false) {
 
 
$form_item = array(
   
'#title'       => htmlspecialchars($component['name'], ENT_QUOTES),
   
'#required'    => $component['mandatory'],
   
'#weight'      => $component['weight'],
   
'#description' => _webform_filtervalues($component['extra']['description']),
   
'#prefix'      => "<div class='webform-component-" .
     
$component['type'] . "' id='webform-component-" .
     
$component['form_key'] . "'>",
   
'#suffix'      => "</div>",
  );
 
 
// set the default value
 
$default_value = _webform_filtervalues($component['value']);
  if (
$default_value) {
   
$form_item['#default_value'] = $default_value;
  }
 
 
// set the component options
 
if ($data) { // $data is set
   
$options = _dynamicselect_display_options($data);
  } else {
// $data is not set
   
$options = _dynamicselect_load_options();
  }
 
$form_item['#options'] = $options;

 
// set display as a checkbox set
 
$form_item['#type'] = "checkboxes";
 
  return
$form_item;

}
// end function _webform_render_dynamicselect($component)
?>

Again, almost all of the code above is stripped out of the select component and simplified. If you need to build a different type of select, like a drop-down or radio buttons, you will need to change the code here.

Experienced Drupal developers may have noticed two things. First, I stripped the code that handles multiple default values. This is because I am only ever planning on setting the default value to %get[id]. Obviously, a more modular version of this component would not skip this step.

Second, you may have noticed that I added a second, optional parameter to the function: $data = false. This will make sense later when we display submission results. For now, the important line in the code above is this one.

<?php
    $options
= _dynamicselect_load_options();
?>

Dynamically loading the options

The _dyanmicselect_load_options() function is what makes this component unique. It dynamically generates a list of options each time the form is loaded. Here it is.

<?php
/**
* dynamically load events that are happening in the next 13
* weeks excluding events that are not published or not open
* for registration
*/
function _dynamicselect_load_options() {
 
 
$options = array();
 
$options[-1] = "Other";
 
 
$one_quarter = 7 * 13 * 24 * 60 * 60;
 
$query = "SELECT
  n.nid,
  n.title,
  e.event_start,
  e.event_end
FROM {node} n
JOIN {event} e ON n.nid = e.nid
JOIN {content_type_event} c ON n.vid = c.vid
WHERE n.status = 1
AND e.event_start > UNIX_TIMESTAMP()
AND e.event_start < UNIX_TIMESTAMP() +
$one_quarter
AND c.field_registration_value = "
Open"
ORDER BY e.event_start ASC"
;
 
 
$results = db_query($query);
 
  while (
$result = db_fetch_array($results)) {
   
$start = format_date($result['event_start'], "custom", "n/j");
   
$end = format_date($result['event_end'], "custom", "n/j");
   
$option = $result['title'] . " $start - $end";
   
$options[$result['nid']] = $option;
  }
// end while
 
 
return $options;

}
// end function _dynamic_select_load_options()
?>

As you can see this function is unique to my needs. This would need to be re-written if you need to filter your options differently. In addition, you may not want to do everything inside a query. In fact, if someone was really ambitious they could turn this into a "real" webform component by implementing filters and fields, like the views module.

In the end the code above returns an associative array that becomes the checkboxes on the webform. It looks something like this.

Array
(
  [-1] => Other
  [26] => Fraternity Event 12/7 - 12/9
  ...
)

That's all there is to creating the element. I don't even have to write a custom _webform_submit_dynamicselect() function because I don't need to change the values created by the default form functions.

Displaying the results

A very useful feature of the webform module is the fact that it stores all submissions in the database, and you can look at them in a variety of ways. However when the options are generated dynamically, displaying the submissions becomes more complicated. Here is the function for viewing one single submission.

<?php
function _webform_submission_display_dynamicselect($data, $component) {

 
$form_item = _webform_render_dynamicselect($component, $data);

 
// set the selected values as checked, i.e. default
 
foreach ((array)$data['value'] as $value) {
    if (
$value) {
     
$form_item['#default_value'][] = $value;
    }
  }

 
$form_item['#attributes'] = array("disabled" => "disabled");

  return
$form_item;

}
// function _webform_submission_display_dynamicselect()
?>

Again, most of this is just a simplified version of what is done in the select component, but, as you can see, this is where the second argument, $data, for _webform_render_dynamicselect() is used. This causes the rendering function to switch tracks when building the options.

<?php
 
if ($data) { // $data is set
   
$options = _dynamicselect_display_options($data);
  } else {
// $data is not set
   
$options = _dynamicselect_load_options();
  }
?>

There are several reasons that I want to display the options differently when I am showing submission results. First, I only want to show the events that were selected, not every open event in the next 13 weeks. Especially since I might be viewing the submission after the start date has passed or the event has been closed for registration. Also, I need to handle invalid data that might have gotten into the database, including events that have been deleted. Here is how I dealt with that.

<?php
function _dynamicselect_display_options($data) {
 
 
$options = array();
 
  foreach (
$data['value'] as $key => $val) {
    if (
$val == -1) {
     
$options[-1] = "Other";
    } else if (
$val && ctype_digit((string)$val)) {
     
$event_node = node_load($val);
      if (
$event_node->type == "event") {
       
$start = format_date($event_node->event_start, "custom", "n/j/Y");
       
$end = format_date($event_node->event_end, "custom", "n/j/Y");
       
$option = $event_node->title . " $start - $end";
       
$options[$val] = $option;
      } else {
// deal with deleted events
       
$options[$val] = "non-event id: $val";
      }
    } else if (
$val) { // deal with invalid values
     
$options[] = "invalid value: " . check_plain($val);
    }
  }
 
  return
$options;
 
}
// end function _dynamicselect_display_options($data)
?>

Of course, there are other ways to display the submissions, including in the analysis tab, as a CSV export, in a table, and last but definitely not least, the submission can be sent as an email. Each of these has to be handled in a similar fashion to the function above. I will not include each of those functions here, but I will include them in the attachment below...

...if I ever finish writing them :)

Download

Below you will find a ZIP archive of the component I have written. The archive includes the original directory structure in case you are confused about where to put this.

Also, the code in the archive may be different than the code shown above. When in doubt, follow the code in the archive and assume that I had a good reason for any changes that I made.