Last updated August 6, 2012. Created by webkenny on January 24, 2007.
Edited by lar11, SLIU, mr.baileys, BioALIEN. Log in to edit this page.
Introduction
It's an often overlooked fact that as well as being able to override the standard theme_ functions, forms generated by the new FormAPI in Drupal 5.x (and higher) can be themed in a similar manner. This tutorial will work through the process of theming a form.
Sample form and data
The original question was "how to embed checkboxes within a table". To demonstrate this we need some data for the table we are going to create. So we begin this tutorial by getting some data we want to put in a table along with the checkboxes. So, lets start with this:
<?php
$r = db_rewrite_sql(db_query("SELECT nid, title, created FROM {node} LIMIT 20"));
while ($row = db_fetch_object($r)) {
$rows[] = $row;
}
?>Note, we put a SQL LIMIT here so that we don't end up with too many rows in our sample table, we'll remove that later when discussing pagination. Also, obviously this isn't the best way to get nodes in Drupal. It's only here to provide us with some data for our form/table.
From the supplied data we'll create a table with a checkbox in the left most column (similar to the "mass operations" for comments found in Drupal Core).
The first thing to do is generate the $form array in preparation for the call to drupal_get_form. The following code does that:
<?php
$r = db_rewrite_sql(db_query('SELECT nid, title, created FROM {node} LIMIT 20'));
while ($row = db_fetch_object($r)) {
$rows[] = $row;
}
foreach ($rows as $v) {
$nids[$v->nid] = '';
$form['title'][$v->nid] = array('#type' => 'markup', '#value' => check_plain($v->title));
$form['created'][$v->nid] = array('#type' => 'markup', '#value' => date("d/m/Y H:i", $v->created));
}
$form['nids'] = array('#type' => 'checkboxes', '#options' => $nids);
?>This may look a little unusual. Most $form arrays are static and developers are used to seeing long lists of $form declarations. Well, in this case you can see the form is dynamic. Within the loop, we create the nested array $form['title'][$v->nid] and $form['created'][$v->nid] of type markup. This used as these are display fields within the form. $nids[$v->nid] creates an array suitable to pass to the options of the checkboxes form element type.
Lastly, every table needs a header. However, we need to pass the header array as part of the form since there's no other way to do it. So, we'll add a header array and enter it as type markup and lastly we'll make a call to drupal_get_form() to get the form itself.
<?php
$r = db_rewrite_sql(db_query('SELECT nid, title, created FROM {node} LIMIT 20'));
while ($row = db_fetch_object($r)) {
$rows[] = $row;
}
foreach ($rows as $v) {
$nids[$v->nid] = '';
$form['title'][$v->nid] = array('#type' => 'markup', '#value' => check_plain($v->title));
$form['created'][$v->nid] = array('#type' => 'markup', '#value' => date("d/m/Y H:i", $v->created));
}
$form['nids'] = array('#type' => 'checkboxes', '#options' => $nids);
$form['submit'] = array('#type' => 'submit', '#value' => 'Update');
$form['header'] = array(
'#type' => 'value',
'#value' => array(
array('data' => 'nid'),
array('data' => t('Title')),
array('data' => t('Created')),
)
);
return drupal_get_form('my_test_form', $form); // 4.7
?>Now, if we did nothing more the resulting form on the browser would look quite odd indeed (try it on a test site and see). So, what we want to do is place the form inside a table and to do this we use a theme override function.
The name of the this function is derived for the $form_id parameter to drupal_get_form() above. So, in our case, the function name is theme_my_test_form($form). And here it is:
<?php
function theme_my_test_form($form) {
foreach (element_children($form['title']) as $key) {
$row = array();
$row['data'][0] = form_render($form['nids'][$key]);
$row['data'][1] = form_render($form['title'][$key]);
$row['data'][2] = form_render($form['created'][$key]);
$rows[] = $row;
}
$output = form_render(theme('table', $form['header']['#value'], $rows));
$output.= form_render($form); // Process any other fields and display them
return $output;
}
?>Drupal 5 users should use drupal_render() rather than form_render().
So, what's going on here. The theme function gets the $form array passed in. Now, the array is full of various things, submit, form_token, even our table header is in there. So, to extract the rows we are interested in the function element_children() is used to loop over one of the child arrays (I choose $form['title']).
As you can see, there are three <td> </td> the first holds the checkbox, the following two contain the two markup fields. Now, running this you see that each database row has been rendered into an html table row with a checkbox in the left column.
Coming next I'll take this further and describe how to define the table to have sortable headers and how to paginate large datasets.
Thanks go to a79v for updates and corrections which have been incorporated into the main article.
Comments
theming subforms
It took me a while, but I eventually found how to use this method for subforms - eg when appending some custom elements added to the node_edit form via form_alter.
The normal drupal_get_form() render process only uses the topmost theme function, so any custom subforms to not automatically trip a call to theme_my_subform().
However this can be invoked again by adding the #theme parameter to your form explicitly.
As seen in the core upload.module, the $form['files'] subform is defined with the function _upload_form() and added to the node edit form.
The instruction
$form['files']['#theme'] = 'upload_form_current';inside this subform means that come render time, drupal_get_for will pass rendering control back to theme_upload_form_current() - which proceeds to sort the form elements into a table.
Further discussion at http://api.drupal.org/api/head/file/developer/topics/forms_api.html
... which is hard to find as it never comes up in Drupal searches, even when I knew what I was looking for :)
.dan.
How to troubleshoot Drupal | http://www.coders.co.nz/
.dan. is the New Zealand Drupal Developer working on Government Web Standards
Updated link forms API and form theming link
http://api.drupal.org/api/file/developer/topics/forms_api.html
benjamin, Agaric Design Collective
benjamin, Agaric
A better way...
I couldn't get the above snippet to work in 5.7...having the second-to-last line wrapped in drupal_render() screwed everything up. Here's a cleaned up, full blown example that works for me (hope this saves sometime some time, i wish I had figured this out two hours ago):
function administer_agents($og_id) {
return drupal_get_form('adminster_agents_overview');
}
function adminster_agents_overview() {
$r = db_query('SELECT nid, title, created FROM {node} LIMIT 20');
while ($row = db_fetch_object($r)) {
$rows[] = $row;
}
foreach ($rows as $v) {
$nids[$v->nid] = '';
$form['title'][$v->nid] = array('#type' => 'markup', '#value' => $v->title);
$form['created'][$v->nid] = array('#type' => 'markup', '#value' => date("d/m/Y H:i", $v->created));
}
$form['nids'] = array('#type' => 'checkboxes', '#options' => $nids);
$form['submit'] = array('#type' => 'submit', '#value' => 'Update');
$form['header'] = array(
'#type' => 'value',
'#value' => array(
array('data' => 'nid'),
array('data' => t('Title')),
array('data' => t('Created')),
)
);
return $form;
}
function theme_adminster_agents_overview($form) {
foreach (element_children($form['title']) as $key) {
$row = array();
$row[] = drupal_render($form['nids'][$key]);
$row[] = drupal_render($form['title'][$key]);
$row[] = drupal_render($form['created'][$key]);
$rows[] = $row;
}
$output = theme('table', $form['header']['#value'], $rows);
$output .= drupal_render($form); // Process any other fields and display them
return $output;
}
Benjamin Lowenstein
benl.com
Same script but with ordered
Same script but with ordered by columns activated:
function administer_agents($og_id) {
return drupal_get_form('adminster_agents_overview');
}
function adminster_agents_overview() {
$form['header'] = array(
'#type' => 'value',
'#value' => array(
array('data' => 'nid'),
array('data' => t('Title'), 'sort' => 'asc', 'field' => 'title'),
array('data' => t('Created'), 'field' => 'created'),
)
);
$r = db_query('SELECT nid, title, created FROM {node} LIMIT 20'. tablesort_sql($form['header']['#value']));
while ($row = db_fetch_object($r)) {
$rows[] = $row;
}
foreach ($rows as $v) {
$nids[$v->nid] = '';
$form['title'][$v->nid] = array('#type' => 'markup', '#value' => $v->title);
$form['created'][$v->nid] = array('#type' => 'markup', '#value' => date("d/m/Y H:i", $v->created));
}
$form['nids'] = array('#type' => 'checkboxes', '#options' => $nids);
$form['submit'] = array('#type' => 'submit', '#value' => 'Update');
return $form;
}
function theme_adminster_agents_overview($form) {
foreach (element_children($form['title']) as $key) {
$row = array();
$row[] = drupal_render($form['nids'][$key]);
$row[] = drupal_render($form['title'][$key]);
$row[] = drupal_render($form['created'][$key]);
$rows[] = $row;
}
$output = theme('table', $form['header']['#value'], $rows);
$output .= drupal_render($form); // Process any other fields and display them
return $output;
}
Ninja form theming, using template files (tpl.php)
Below is an super powerful way in 5.7 to give you total control of theming any node form (e.g. a cck-based content type called news_article). It uses template files, so your designers will be less confused as well. This example uses the zen theme by the way...
1. put this in template.php (uncomment the print statement to see the template filename required)
<?phpfunction zen_node_form($form) //zen, or phptemplate, or yourthemename {
//print('/YOUR_(SUB_THEME_NAME_IF_ZEN)_THEME_NAME_HERE/'.$form['type']['#value'].'_form.tpl.php');die();
if(file_exists(path_to_theme().'/YOUR_(SUB_THEME_NAME_IF_ZEN)_THEME_NAME_HERE/'.$form['type']['#value'].'_form.tpl.php')) {
return _phptemplate_callback($form['type']['#value'].'_form', array('user' => $user, 'form' => $form));
}
}
?>
2. create a template file in your (sub if zen) template directory as nodetype_form.tpl.php (my live site uses workorder_form.tpl.php). uncomment the line below to see all the variables you have access to!
<?php //print('<pre>'); print_r($form); print('</pre>'); ?><table border=1>
<th>My Header</th>
<tr>
<td><?php print drupal_render($form['title']); ?></td>
</tr>
</table>
<?php print drupal_render($form); ?>
3. if you want to modify the $form variable (to add new form elements, like text fields for example) before that template file loads, put this in a (hopefully your own custom) module or in template.php if you're lazy / testing...
<?phpfunction ANY_MODULE_NAME_form_alter($form_id, &$form) {
$form['something'] = array(
'#title'=>t('My new something'),
'#type'=>'textfield',
'#description' => t('Please enter your new something.')
);
}
?>
4. Troubleshooting: I just had a problem of input widths being default to 60, which was too wide for my application. Here's how I fixed it. I went into my workorder_form.tpl.php, uncommented the <?print(''); print_r($form); print(''); ?>
and realized I needed to adjust the size attribute like so:
$width=20;
$form['field_cm_wo_contact_name']['0']['value']['#size']=$width;
5. More Troubleshooting: If you need to add fields to your form that are NOT CCK (I'm using charLeft.js -- which *cannot* understand field_something[0]['value'] since it stops at the first bracket -- to show number of remaining characters for a textfield), use form_alter to do something like:
<?phpfunction commercial_workorders_form_alter($form_id, &$form) {
$form['TextField']=array(
'#title'=>t('Please explain the problem. NOTE: Only the first 50 characters will be recorded'),
'#name'=>'TextField',
'#type'=>'textfield',
'#attributes'=>array(
'onBlur'=>'InputLengthCheck()',
'onKeyUp'=>'InputLengthCheck()')
);
}
//note that in this format, all the submit handlers for CCK and your custom ones fire--thanks to Crell for pointing that out to me
$form['#submit']['commercial_workorders_new']=array('');
?>
then create a commercial_workorders_new to send that value to the correct CCK field like this:
<?php//note that you have access to the entire $form
function commercial_workorders_new(&$node, &$form_values, &$form) {
$form['field_cm_wo_problem'][0]['value']=$form_values['TextField'];
}
?>
the only problem (someone know how to fix?) is that the $form values aren't written to the database like mentioned in that last line, though I worked around it by using nodeapi and change the $node->field_cm_wo_problem[0]['value']=$node->TextField in the 'submit' $op. Works but ugly...
Is there a chance to output
Is there a chance to output the title of the cck field and output the textfield of the cck field separately.
Right now when I use:
<?phpprint drupal_render($form['field_name']);
?>
I get the title and textfield of the cck field together. I need to make a design where I have to separate the title and the textfield. So need to have a code where I can just ouput textfield of the cck field without the title. Is there any chance?
For example something like:
<?phpprint drupal_render($form['field_name']['0']['value']['#title']);
?>
(This does not work yet)
This is really urgent, is there anbody who has an idea how to solve this problem?
Thank you!!
Additional parameters
Great tutorial, but how can I add additional parameters to theme_my_test_form()? I would like to have the user id within the theme function to create some custom ouput. The user id is successfully passed to my_test_form() but then?
Ah, and I don't want to use
global $useras the user id comes from the requested url.You can pass your own stuff
You can pass your own stuff through to the theme in the form values array. you can set a form element #type=hidden or #type=value and pick it up later.
... or just add anything you want directly to the $form or form element as long as you label it with a '#' first. Not best practice, but 99% harmless.
.dan. is the New Zealand Drupal Developer working on Government Web Standards
Elements showing up twice: pass $form by reference!
This one got me for a few hours. My theme_form function uses drupal_render, and calls a few other functions to theme some form elements depending on user input. It then calls drupal_render($form) to add all the hidden form bits, like so:
<?php
theme_gradprofile_edit_form($form) {
//theme common form elements
//...
//theme form for specific type of grad student
if ($form['grad_info']['student_type']['#value'] == 'PhD') {
$output .= _theme_phd_edit_form($form);
}
else if ($form['grad_info']['student_type']['#value'] == 'MS') {
$output .= _theme_ms_edit_form($form);
}
$output .= '<br /><br />' . drupal_render($form);
return $output;
}
?>
Doing this caused any elements rendered by my sub-form theming functions to show up twice. I finally figured out that the form must be passed by reference into the _theme functions. Apparently drupal_render modifies the element to show that it has already been rendered. When I was passing a copy of the form, it was not updated to reflect that the elements have already been rendered.
I just updated my _theme_whatever_edit_form function definition to pass the form by reference by adding an ampersand:
<?phpfunction _theme_whatever_edit_form (&$form) {
//some stuff
}
?>
Hope this saves someone some time.
Add an id to a radio element in Drupal 5
After hunting all over the place, it turns out to be easy. This is for Drupal 5 using the phptemplate engine.
To auto add ids to radio elements, copy the following function out of includes/forms.inc into your template.php file in your theme directory and rename it as phptemplate_radio. There may be better ways, but this should get you started.
Original version from forms.inc:
<?php
function theme_radio($element) {
_form_set_class($element, array('form-radio'));
$output = '<input type="radio" ';
$output .= 'name="' . $element['#name'] .'" ';
$output .= 'value="'. $element['#return_value'] .'" ';
$output .= (check_plain($element['#value']) == $element['#return_value']) ? ' checked="checked" ' : ' ';
$output .= drupal_attributes($element['#attributes']) .' />';
if (!is_null($element['#title'])) {
$output = '<label class="option">'. $output .' '. $element['#title'] .'</label>';
}
unset($element['#title']);
return theme('form_element', $element, $output);
}
?>
If you look at this for a while it turns out to be very straight forward, but if you're new to form theming in Drupal there are virtually no worked examples of how to do this...
Here is the updated version where you can dump in your id, this should be put in your template.php file. ( DO NOT modify the original in forms.inc ) NOTE: you'll want to check the radio to make sure you are setting the right id to the right field.
<?php
function phptemplate_radio($element) {
_form_set_class($element, array('form-radio'));
$output = '<input type="radio" ';
$output .= 'name="' . $element['#name'] .'" ';
// start change
$output .= 'id="' . $your_id_here .'" ';
// end change
$output .= 'value="'. $element['#return_value'] .'" ';
$output .= (check_plain($element['#value']) == $element['#return_value']) ? ' checked="checked" ' : ' ';
$output .= drupal_attributes($element['#attributes']) .' />';
if (!is_null($element['#title'])) {
$output = '<label class="option">'. $output .' '. $element['#title'] .'</label>';
}
unset($element['#title']);
return theme('form_element', $element, $output);
}
?>
Hope this helps anybody!