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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_webform_help_dynamicselect</span><span style="color: #007700">(</span><span style="color: #0000BB">$section</span><span style="color: #007700">) {<br> <br> switch (</span><span style="color: #0000BB">$section</span><span style="color: #007700">) {<br> case </span><span style="color: #DD0000">'admin/settings/webform#dynamicselect_description'</span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$output </span><span style="color: #007700">= </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">"A dynamic list of upcoming events."</span><span style="color: #007700">);<br> break;<br> }<br> <br> return </span><span style="color: #0000BB">$output</span><span style="color: #007700">;<br><br>} </span><span style="color: #FF8000">// end function _webform_help_dynamicselect($section)<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_webform_edit_dynamicselect</span><span style="color: #007700">(</span><span style="color: #0000BB">$currfield</span><span style="color: #007700">) {<br> <br> </span><span style="color: #0000BB">$edit_fields </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$edit_fields</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">"textfield"</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">"Default value"</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#default_value' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$currfield</span><span style="color: #007700">[</span><span style="color: #DD0000">'default'</span><span style="color: #007700">],<br> </span><span style="color: #DD0000">'#description' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">"The default value of the field."</span><span style="color: #007700">) .<br> </span><span style="color: #DD0000">"<br />" </span><span style="color: #007700">. </span><span style="color: #0000BB">webform_help</span><span style="color: #007700">(</span><span style="color: #DD0000">"webform/helptext#variables"</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#size' </span><span style="color: #007700">=> </span><span style="color: #0000BB">60</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#maxlength' </span><span style="color: #007700">=> </span><span style="color: #0000BB">255</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#weight' </span><span style="color: #007700">=> </span><span style="color: #0000BB">0</span><span style="color: #007700">,<br> );<br> <br> return </span><span style="color: #0000BB">$edit_fields</span><span style="color: #007700">;<br><br>} </span><span style="color: #FF8000">// end function _webform_edit_dynamicselect($currfield)<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_webform_render_dynamicselect</span><span style="color: #007700">(</span><span style="color: #0000BB">$component</span><span style="color: #007700">, </span><span style="color: #0000BB">$data </span><span style="color: #007700">= </span><span style="color: #0000BB">false</span><span style="color: #007700">) {<br> <br> </span><span style="color: #0000BB">$form_item </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'#title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">htmlspecialchars</span><span style="color: #007700">(</span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'name'</span><span style="color: #007700">], </span><span style="color: #0000BB">ENT_QUOTES</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#required' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'mandatory'</span><span style="color: #007700">],<br> </span><span style="color: #DD0000">'#weight' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'weight'</span><span style="color: #007700">],<br> </span><span style="color: #DD0000">'#description' </span><span style="color: #007700">=> </span><span style="color: #0000BB">_webform_filtervalues</span><span style="color: #007700">(</span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'extra'</span><span style="color: #007700">][</span><span style="color: #DD0000">'description'</span><span style="color: #007700">]),<br> </span><span style="color: #DD0000">'#prefix' </span><span style="color: #007700">=> </span><span style="color: #DD0000">"<div class='webform-component-" </span><span style="color: #007700">.<br> </span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'type'</span><span style="color: #007700">] . </span><span style="color: #DD0000">"' id='webform-component-" </span><span style="color: #007700">.<br> </span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'form_key'</span><span style="color: #007700">] . </span><span style="color: #DD0000">"'>"</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#suffix' </span><span style="color: #007700">=> </span><span style="color: #DD0000">"</div>"</span><span style="color: #007700">,<br> );<br> <br> </span><span style="color: #FF8000">// set the default value<br> </span><span style="color: #0000BB">$default_value </span><span style="color: #007700">= </span><span style="color: #0000BB">_webform_filtervalues</span><span style="color: #007700">(</span><span style="color: #0000BB">$component</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">]);<br> if (</span><span style="color: #0000BB">$default_value</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">[</span><span style="color: #DD0000">'#default_value'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$default_value</span><span style="color: #007700">;<br> }<br> <br> </span><span style="color: #FF8000">// set the component options<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">$data</span><span style="color: #007700">) { </span><span style="color: #FF8000">// $data is set<br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= </span><span style="color: #0000BB">_dynamicselect_display_options</span><span style="color: #007700">(</span><span style="color: #0000BB">$data</span><span style="color: #007700">);<br> } else { </span><span style="color: #FF8000">// $data is not set<br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= </span><span style="color: #0000BB">_dynamicselect_load_options</span><span style="color: #007700">();<br> }<br> </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">[</span><span style="color: #DD0000">'#options'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$options</span><span style="color: #007700">;<br><br> </span><span style="color: #FF8000">// set display as a checkbox set<br> </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">[</span><span style="color: #DD0000">'#type'</span><span style="color: #007700">] = </span><span style="color: #DD0000">"checkboxes"</span><span style="color: #007700">;<br> <br> return </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">;<br><br>} </span><span style="color: #FF8000">// end function _webform_render_dynamicselect($component)<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> $options </span><span style="color: #007700">= </span><span style="color: #0000BB">_dynamicselect_load_options</span><span style="color: #007700">();<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br>* dynamically load events that are happening in the next 13<br>* weeks excluding events that are not published or not open<br>* for registration<br>*/<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_dynamicselect_load_options</span><span style="color: #007700">() {<br> <br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$options</span><span style="color: #007700">[-</span><span style="color: #0000BB">1</span><span style="color: #007700">] = </span><span style="color: #DD0000">"Other"</span><span style="color: #007700">; <br> <br> </span><span style="color: #0000BB">$one_quarter </span><span style="color: #007700">= </span><span style="color: #0000BB">7 </span><span style="color: #007700">* </span><span style="color: #0000BB">13 </span><span style="color: #007700">* </span><span style="color: #0000BB">24 </span><span style="color: #007700">* </span><span style="color: #0000BB">60 </span><span style="color: #007700">* </span><span style="color: #0000BB">60</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$query </span><span style="color: #007700">= </span><span style="color: #DD0000">"SELECT<br> n.nid,<br> n.title,<br> e.event_start,<br> e.event_end<br>FROM {node} n<br>JOIN {event} e ON n.nid = e.nid<br>JOIN {content_type_event} c ON n.vid = c.vid<br>WHERE n.status = 1<br>AND e.event_start > UNIX_TIMESTAMP()<br>AND e.event_start < UNIX_TIMESTAMP() + </span><span style="color: #0000BB">$one_quarter</span><span style="color: #DD0000"><br>AND c.field_registration_value = "</span><span style="color: #0000BB">Open</span><span style="color: #DD0000">"<br>ORDER BY e.event_start ASC"</span><span style="color: #007700">;<br> <br> </span><span style="color: #0000BB">$results </span><span style="color: #007700">= </span><span style="color: #0000BB">db_query</span><span style="color: #007700">(</span><span style="color: #0000BB">$query</span><span style="color: #007700">);<br> <br> while (</span><span style="color: #0000BB">$result </span><span style="color: #007700">= </span><span style="color: #0000BB">db_fetch_array</span><span style="color: #007700">(</span><span style="color: #0000BB">$results</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$start </span><span style="color: #007700">= </span><span style="color: #0000BB">format_date</span><span style="color: #007700">(</span><span style="color: #0000BB">$result</span><span style="color: #007700">[</span><span style="color: #DD0000">'event_start'</span><span style="color: #007700">], </span><span style="color: #DD0000">"custom"</span><span style="color: #007700">, </span><span style="color: #DD0000">"n/j"</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$end </span><span style="color: #007700">= </span><span style="color: #0000BB">format_date</span><span style="color: #007700">(</span><span style="color: #0000BB">$result</span><span style="color: #007700">[</span><span style="color: #DD0000">'event_end'</span><span style="color: #007700">], </span><span style="color: #DD0000">"custom"</span><span style="color: #007700">, </span><span style="color: #DD0000">"n/j"</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$option </span><span style="color: #007700">= </span><span style="color: #0000BB">$result</span><span style="color: #007700">[</span><span style="color: #DD0000">'title'</span><span style="color: #007700">] . </span><span style="color: #DD0000">" </span><span style="color: #0000BB">$start</span><span style="color: #DD0000"> - </span><span style="color: #0000BB">$end</span><span style="color: #DD0000">"</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$options</span><span style="color: #007700">[</span><span style="color: #0000BB">$result</span><span style="color: #007700">[</span><span style="color: #DD0000">'nid'</span><span style="color: #007700">]] = </span><span style="color: #0000BB">$option</span><span style="color: #007700">;<br> } </span><span style="color: #FF8000">// end while<br> <br> </span><span style="color: #007700">return </span><span style="color: #0000BB">$options</span><span style="color: #007700">;<br><br>} </span><span style="color: #FF8000">// end function _dynamic_select_load_options()<br></span><span style="color: #0000BB">?></span></span>
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<br>(<br> [-1] => Other<br> [26] => Fraternity Event 12/7 - 12/9<br> ... <br>)
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_webform_submission_display_dynamicselect</span><span style="color: #007700">(</span><span style="color: #0000BB">$data</span><span style="color: #007700">, </span><span style="color: #0000BB">$component</span><span style="color: #007700">) {<br><br> </span><span style="color: #0000BB">$form_item </span><span style="color: #007700">= </span><span style="color: #0000BB">_webform_render_dynamicselect</span><span style="color: #007700">(</span><span style="color: #0000BB">$component</span><span style="color: #007700">, </span><span style="color: #0000BB">$data</span><span style="color: #007700">);<br><br> </span><span style="color: #FF8000">// set the selected values as checked, i.e. default<br> </span><span style="color: #007700">foreach ((array)</span><span style="color: #0000BB">$data</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">] as </span><span style="color: #0000BB">$value</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$value</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">[</span><span style="color: #DD0000">'#default_value'</span><span style="color: #007700">][] = </span><span style="color: #0000BB">$value</span><span style="color: #007700">;<br> }<br> }<br><br> </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">[</span><span style="color: #DD0000">'#attributes'</span><span style="color: #007700">] = array(</span><span style="color: #DD0000">"disabled" </span><span style="color: #007700">=> </span><span style="color: #DD0000">"disabled"</span><span style="color: #007700">);<br><br> return </span><span style="color: #0000BB">$form_item</span><span style="color: #007700">;<br><br>} </span><span style="color: #FF8000">// function _webform_submission_display_dynamicselect()<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #007700">if (</span><span style="color: #0000BB">$data</span><span style="color: #007700">) { </span><span style="color: #FF8000">// $data is set<br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= </span><span style="color: #0000BB">_dynamicselect_display_options</span><span style="color: #007700">(</span><span style="color: #0000BB">$data</span><span style="color: #007700">);<br> } else { </span><span style="color: #FF8000">// $data is not set<br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= </span><span style="color: #0000BB">_dynamicselect_load_options</span><span style="color: #007700">();<br> }<br></span><span style="color: #0000BB">?></span></span>
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.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_dynamicselect_display_options</span><span style="color: #007700">(</span><span style="color: #0000BB">$data</span><span style="color: #007700">) {<br> <br> </span><span style="color: #0000BB">$options </span><span style="color: #007700">= array();<br> <br> foreach (</span><span style="color: #0000BB">$data</span><span style="color: #007700">[</span><span style="color: #DD0000">'value'</span><span style="color: #007700">] as </span><span style="color: #0000BB">$key </span><span style="color: #007700">=> </span><span style="color: #0000BB">$val</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$val </span><span style="color: #007700">== -</span><span style="color: #0000BB">1</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$options</span><span style="color: #007700">[-</span><span style="color: #0000BB">1</span><span style="color: #007700">] = </span><span style="color: #DD0000">"Other"</span><span style="color: #007700">; <br> } else if (</span><span style="color: #0000BB">$val </span><span style="color: #007700">&& </span><span style="color: #0000BB">ctype_digit</span><span style="color: #007700">((string)</span><span style="color: #0000BB">$val</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$event_node </span><span style="color: #007700">= </span><span style="color: #0000BB">node_load</span><span style="color: #007700">(</span><span style="color: #0000BB">$val</span><span style="color: #007700">);<br> if (</span><span style="color: #0000BB">$event_node</span><span style="color: #007700">-></span><span style="color: #0000BB">type </span><span style="color: #007700">== </span><span style="color: #DD0000">"event"</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$start </span><span style="color: #007700">= </span><span style="color: #0000BB">format_date</span><span style="color: #007700">(</span><span style="color: #0000BB">$event_node</span><span style="color: #007700">-></span><span style="color: #0000BB">event_start</span><span style="color: #007700">, </span><span style="color: #DD0000">"custom"</span><span style="color: #007700">, </span><span style="color: #DD0000">"n/j/Y"</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$end </span><span style="color: #007700">= </span><span style="color: #0000BB">format_date</span><span style="color: #007700">(</span><span style="color: #0000BB">$event_node</span><span style="color: #007700">-></span><span style="color: #0000BB">event_end</span><span style="color: #007700">, </span><span style="color: #DD0000">"custom"</span><span style="color: #007700">, </span><span style="color: #DD0000">"n/j/Y"</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$option </span><span style="color: #007700">= </span><span style="color: #0000BB">$event_node</span><span style="color: #007700">-></span><span style="color: #0000BB">title </span><span style="color: #007700">. </span><span style="color: #DD0000">" </span><span style="color: #0000BB">$start</span><span style="color: #DD0000"> - </span><span style="color: #0000BB">$end</span><span style="color: #DD0000">"</span><span style="color: #007700">;<br> </span><span style="color: #0000BB">$options</span><span style="color: #007700">[</span><span style="color: #0000BB">$val</span><span style="color: #007700">] = </span><span style="color: #0000BB">$option</span><span style="color: #007700">;<br> } else { </span><span style="color: #FF8000">// deal with deleted events<br> </span><span style="color: #0000BB">$options</span><span style="color: #007700">[</span><span style="color: #0000BB">$val</span><span style="color: #007700">] = </span><span style="color: #DD0000">"non-event id: </span><span style="color: #0000BB">$val</span><span style="color: #DD0000">"</span><span style="color: #007700">;<br> }<br> } else if (</span><span style="color: #0000BB">$val</span><span style="color: #007700">) { </span><span style="color: #FF8000">// deal with invalid values<br> </span><span style="color: #0000BB">$options</span><span style="color: #007700">[] = </span><span style="color: #DD0000">"invalid value: " </span><span style="color: #007700">. </span><span style="color: #0000BB">check_plain</span><span style="color: #007700">(</span><span style="color: #0000BB">$val</span><span style="color: #007700">);<br> }<br> }<br> <br> return </span><span style="color: #0000BB">$options</span><span style="color: #007700">;<br> <br>} </span><span style="color: #FF8000">// end function _dynamicselect_display_options($data)<br></span><span style="color: #0000BB">?></span></span>
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.
Files: Drupal Webform Dynamic Checkboxes CodeTags: