The object: to use the forms API to render tabular data

This can probably be done by extending the form builder functions and inserting table structure where currently we have fieldset structure.

Table layouts are not all bad. Sometimes, when dealing with actual data, a table is the right thing. Could phpadmin work without tables? Anyway, my data structure needs tables. And form elements inside them.

An example form/table structure

The form table I want (built as HTML string): "; $table = "
"; $output .= $table; $output .= " ... which could trivially be built like this:
".htmlspecialchars($table)."

Attempting the same in forms API however...

Well, First I'll establish the heirachy, a bit like
  form:
    fieldset:
      field: 'Dewey'
    fieldset:
      field: 'Huey'
      field: 'Louie'
And once that's taking shape, I need to fill in the Forms API verbose definitions.

Basic, Structured Form

It could end up looking like: "; $dewey_field = array( '#type' => 'textfield', '#default_value' => 'Dewey', ); $huey_field = $dewey_field; $huey_field['#default_value'] = 'Huey'; $louie_field = $dewey_field; $louie_field['#default_value'] = 'Louie'; $group = array(); $group['row1'] = array( '#type' => 'fieldset', '#title' => t('row 1'), ); $group['row1']['huey'] = $huey_field; $group['row2'] = array( '#type' => 'fieldset', '#title' => t('row 2'), ); $group['row2']['dewey'] = $dewey_field; $group['row2']['louie'] = $louie_field; $group['submit_button'] = array( '#type' => 'button', '#value' => 'Submit', ); $form = array( 'group' => $group ); $output .= "
".print_r($form,1)."
"; // save a copy before processing $form2 = $form; $got_form = drupal_get_form('form_id', $form); //print('form is initialized'); //print("
got form: ".htmlspecialchars(print_r($form,1))."
"); $output .= "

This form definition now gets run through the wringer, and after drupal_get_form() is done with it, it renders like:

"; $output .= "

The Form, rendered normally

"; $output .= $got_form ; $output .= "

Now, turn the form into a table

Formgroups from my trivial example become trs, fields become tds. This is simply convenient, in practice the actual nesting may have to be thought out... but it's no harder than the old theme_form() function.

I define a few new rendering functions theme_tr(), theme_td() for the formbuilder to find. Publish these with a hook_elements() implimentation to declare their existance and how I expect them to behave.

Add some logic to allow lazy definitions ( any child of a TABLE is expected to be a TR, any child of a TR must be a TD ) ... using a '#process' parameter. This process is a shortcut, but it assumes that anonymous arrays should implicitly have an appropriate '#type' set, and that elements that don't fit the rules of table structure will be shuffled down until they find a td to fall into.

I'll modify the form structure I began with to get:

  table:
    tr:
      td:
        field: 'Dewey'
    tr:
      td:
        field: 'Huey'
      td:
        field: 'Louie'

"; $form2['group']['#type'] = 'formtable'; $form2['group']['#attributes'] = array('border'=>'2'); // turn formgroups into trs, // the items within them will get wrapped in tds $form2['group']['row1']['#type'] = 'tr'; $form2['group']['row2']['#type'] = 'tr'; $output .= "It now looks like:
".htmlspecialchars(print_r($form2,1))."
"; $output .= "

Result! A formtable

"; // re-initialize $got_form = drupal_get_form('form_2', $form2); //$output .= "
".htmlspecialchars(print_r($form2,1))."
"; $output .= " Setting drupal_get_form() on this new structure now gives me:
".htmlspecialchars(print_r($got_form,1))."

The Form as a table

$got_form

And with some work, the rest of the forms arsenal can be squeezed into this page-building! Dunno how far I can drag this towards the table-builder with sorting columns and all. I don't think I need that yet..

Note how the button element (that was simply defined as a top-level member of the form) has ended up in its own cell. This could be constructed neater if you took the time, but I'm letting default behaviour format it for now.

I have some tweaking to do to enforce XHTML compliance (TBODY, maybe TH and that crap) but it looks trivial. TD attributes like colspan can be added in the same way as the old theme_table() did and the new forms API does.

"; return $output; } /** * Build an HTML tag. * Very generic. * Uses #type, #attributes, #children and #value */ function theme_element($element){ return '<'.$element['#type'] . drupal_attributes($element['#attributes']) .' >' . $element['#children'] . $element['#value'] . '\n"; } function theme_formtable($element){ return '' . $element['#children'] . $element['#value'] . "\n"; } function theme_tr($element){ return theme_element($element); } function theme_td($element){ return theme_element($element); } function theme_th($element){ return theme_element($element); } /** * return custom fields for my new element definitions */ function formtable_elements(){ $type['td'] = array('#process' => array('ensure_text' => array()), '#tree' => TRUE ); $type['th'] = array('#process' => array('ensure_text' => array()), '#tree' => TRUE ); $type['tr'] = array('#process' => array('ensure_table_cells' => array()), '#tree' => TRUE ); $type['formtable'] = array('#process' => array('ensure_table_rows' => array()), '#tree' => TRUE ); return $type; } /** * Given a TR element, the only valid children are TDs * If the child is NOT a TD, wrap it up as if it were one */ function ensure_table_cells($element){ // should I use element_children($element) ? foreach($element as $key=>$cell){ if($key[0] == '#'){continue;} if($cell['#type'] != 'td'){ if(is_string($cell)){ $element[$key] = array('#type' => 'td', '#value' => $cell ); } else { $element[$key] = array('#type' => 'td', $cell ); } } } return $element; } /** * cast random plaintext values encountered in the structure * into text elements */ function ensure_text($element){ foreach($element as $key=>$cell){ if($key[0] == '#'){continue;} if(is_string($cell)){ $element[$key] = array('#value' => $cell ); debug("plaintext '$cell' found in the middle of a forms API structure. Casting it into a text node. This should be avoided",E_USER_WARNING ); } } return $element; } function ensure_table_rows($element){ // should I use element_children($element) ? foreach($element as $key=>$cell){ if(! is_array($cell)){continue;} if($key[0] == '#'){continue;} if(! in_array($cell['#type'] , array('caption','thead','tbody','tfoot','tr') ) ){ $element[$key] = array('#type' => 'tr', $cell ); } } return $element; }