Using Theme Override Functions For Forms
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 an 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 use 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 out 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

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/
Updated link forms API and form theming link
http://api.drupal.org/api/file/developer/topics/forms_api.html
benjamin, Agaric Design Collective
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;
}