Strange drupal_render Problem...

ExTexan - July 1, 2009 - 00:52

I'm developing a module in 6.x that reads a db table and presents the data in a table view. I have one function to create the form and another to theme it. The theme form is the one calling drupal_render for all form elements.

I have three buttons - one to "Show" the page again after choosing an option from a selection box (it sets "rebuild" in validation) - another button to update values from several selection boxes back to the db table (submit button) - a third button to "Add" records (redirects to another form).

The "Update" and "Add" buttons are at the bottom (below the "table") while the "Show" is at the top. I decided to duplicate the "Update" and "Add" buttons above and below the table view for convenience (when there are many records, user won't have to scroll all the way to the bottom just to click the button).

But, if there are "No records", I don't want to duplicate the buttons - one set at the bottom is fine. So I took out the drupal_render($form) at the end of my theming function and am calling drupal_render for ALL my form elements - making the duplicate buttons at the top conditional if there are no records.

But without the drupal_render($form) at the end (after all other elements have been rendered), my buttons don't work. They all simply reload the form. I know "rebuild" is only being set on the "Show" form, so that's not the problem. I suspect that drupal_render($form) is initializing some other required vars that are not getting set when I specifically call drupal_render on each form element. Can someone shed some light on this problem?

My work-around was to unset the $form['top-buttons'] "tree" and then leave the drupal_render($form) at the end of the theming function. But even though that works fine, I'd still like to know what's going on behind the scenes in drupal_render, just so I'll understand it better. Besides, I'm not sure unsetting form vars is the "drupal way". ;-)

Thanks in advance.

you cannot use that, use

horst_wessel - July 1, 2009 - 03:30

you cannot use that, use drupal_get_form ('your_form_id', <additional parameter>) instead it's because drupal will emits certain token for every form (for authorization policy), and drupal_get_form is required to create that token

cheers...

cheers

More Info...

ExTexan - July 1, 2009 - 07:36

horst_wessel, perhaps I jumped too deep into the problem in my description. First, let me say that what I'm doing was "borrowed" from the Profile module. I guess I shud include code samples at this point.

Here's the pertinent section of my_mod.module file:

$items['admin/litdep/fields'] = array(
    'title' => 'My Title',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('litdep_fields_list_form'),
    'access arguments' => array('field definitions'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'litdep.pages.inc',
);

And here's an abbreviated version of the form function...

function litdep_fields_list_form(&$form_state, $tid = NULL, $uid = NULL) {

  ...init stuff omitted...

  $form = array();

  $form['selection'] = array(
      '#type' => 'fieldset',
      '#description' => t('Select table to view field definitions.'),
  );

  if ($is_admin) {
      $form['selection']['select-user'] = array(
          '#type' => 'textfield',
          '#title' => t('User'),
          '#default_value' => $uid,
      );
  } else {
      $form['selection']['select-user'] = array(
          '#type' => 'value',
          '#value' => $uid,
      );
  }

  $form['selection']['select-table'] = array(
      '#type' => 'select',
      '#title' => 'Table',
      '#default_value' => $tid,
      '#options' => $tables,
      '#multiple' => FALSE,
  );

  $form['selection']['show'] = array(      //In validation, $form_state['rebuild'] is set to true if this button was clicked
      '#type' => 'button',
      '#value' => 'Show',
  );

  // Get the fields for the specified Table Id, (and user)
  ... query the table ...

  while ($row = db_fetch_object($result)) {
      // For each row of the db table, the "row" of the <table> is a $form entry indexed by $rowCount
      // For example, one field is set up like this:
      $form[$rowCount]['Desc'] = array('#value' => check_plain($row->fDesc));
      ... set $form entries for other fields ...
  }

  if ($prevCategory != NULL) {      //The code dealing with Category was omitted above, but NULL means there were no recs to show
      $form['buttons-top']['update-top'] = array(
          '#type' => 'submit',
          '#value' => 'Update Category Weights',
      );
      $form['buttons-btm']['update-btm'] = array(
          '#type' => 'submit',
          '#value' => 'Update Category Weights',
      );
  }

  $form['buttons-top']['add-field-top'] = array(
      '#type' => 'button',
      '#value' => 'Add Field',
  );

  $form['buttons-btm']['add-field-btm'] = array(
      '#type' => 'button',
      '#value' => 'Add Field',
  );

  return $form;
}

And this is the function to theme the form..

function theme_litdep_fields_list_form($form) {

  $rows = array();

  foreach (element_children($form) as $key) {
      // Render Category row
      if (array_key_exists('Category', $form[$key])) {
          $category = &$form[$key];
          $row = array();
          $row[] = array('data' => drupal_render($category['Weight' . $key]), 'class' => 'category weight');
          $row[] = array('data' => drupal_render($category['Category']), 'colspan' => 5, 'class' => 'category');
          $rows[] = array('data' => $row, 'class' => 'row');
      }

      // Render Field row
      if (array_key_exists('Desc', $form[$key])) {
          $field = &$form[$key];
          $row = array();
          $row[] = drupal_render($field['Desc']);
          ... render other fields (columns) ...
          $rows[] = array('data' => $row, 'class' => 'row');
      }
  }

  if (empty($rows)) {
    $rows[] = array(array('data' => t('No fields defined for the table.'), 'colspan' => 6));
  }

  $header = array(
      t('Description'),
      ... other column headings ...
  );

  $output = drupal_render($form['selection']);
  $output .= drupal_render($form['buttons-top']); // Show buttons at top and bottom for convenience
  $output .= theme('table', $header, $rows, array('id' => 'fields-list'));
  $output .= drupal_render($form['buttons-btm']);
//  $output .= drupal_render($form);      // I *think* I've rendered all my desired info above, so I *shudn't* need to call this
// But without it, ALL buttons on the form simply reload the form instead of doing what they are supposed to do.

  return $output;
}

I know drupal creates a lot of extra entries in the $form and $form_state arrays in order to make it all work, but I can't see why, if I've rendered all my desired fields, I should have to call drupal_render($form) just to make my buttons work the way I want them to. Can you explain this anomaly? Thanks in advance for your help.

sorry to reply late, since i

horst_wessel - July 1, 2009 - 11:15

sorry to reply late, since i work on my project all day long...

basically i never used hook_theme nor drupal_render for rendering a form since in drupal_get_form($form_id) function drupal_render is already used, if you want to place two button, just place a two button instead :D

you cannot use the theme () function like theme('table', $header, $rows, array('id' => 'fields-list')); you must first declare a hook_theme... in this case table_theme.

the last flaw in my opinion is you cannot declare page_arguments like 'page arguments' => array('litdep_fields_list_form'), that is the argument to pass to the page, here's my most used module template

//implementation of hook_perm
function mymodule_perm() {
return array ('access mymodule');
}

//implementation of hook_menu
function mymodule_menu () {
$items['mymodule/%/%'] = array(
'title' => 'lalala',
'page arguments' => array(1,2), //the arguments for the page e.g mymodule/arg1/arg2 if mymodule/pop/cap.... it pases to mymodule_page (pop,cap)
'page callback' => 'mymodule_page', //your page function
'access arguments' => array('access my module'),
'type' => MENU_CALLBACK,
);
return $items;
}
function mymodule_page ($arg1,$arg2) {
if (is_numeric($type) || !is_numeric($nid)) {
// We will just show a standard "access denied" page in this case.
return drupal_access_denied();
}
                 $content .= drupal_get_form ('myform');
                $content .= theme('mymodule', $header, $rows, array('id' => 'fields-list'));
return $content
}
function myform () {
               //form attributes goes here
               return $form;
}

function mymodule_theme () {

}

hope that will help
you can learn from here though http://api.drupal.org/api/file/developer/topics/forms_api.html/6
cheers

cheers

Can We Not Rely on Core Modules?

ExTexan - July 2, 2009 - 06:28

horst_wessel,

Thanks for the very detailed reply. My original question has been answered, which is that, as I suspected, extra array elements are being created during the call to drupal_render($form).

I didn't really understand what you meant when you said, "if you want to place two button, just place a two button instead ". Can you elaborate?

However, some of your comments on the way I was doing my form surprised me, as this "method" was copied from the Profile module - which is a Drupal Core module. I thought I would certainly be safe in using Core modules as examples on how to do things the "Drupal Way".

More specifically, you said:

you cannot use the theme () function like theme('table', $header, $rows, array('id' => 'fields-list')); you must first declare a hook_theme... in this case table_theme.

This is directly from the Profile module:

function theme_profile_admin_overview($form) {

  ...code to init $rows array and other stuff...

  $header = array(t('Title'), t('Name'), t('Type'));
  if (isset($form['submit'])) {
    $header[] = t('Category');
    $header[] = t('Weight');
  }
  $header[] = array('data' => t('Operations'), 'colspan' => 2);

  $output = theme('table', $header, $rows, array('id' => 'profile-fields'));    <== Is this what you said I shouldn't do?
  $output .= drupal_render($form);

  return $output;
}

Your next commet was...

the last flaw in my opinion is you cannot declare page_arguments like 'page arguments' => array('litdep_fields_list_form'), that is the argument to pass to the page, here's my most used module template

And, again, this is from the Profile module...

function profile_menu() {
  .
  .
  $items['admin/user/profile'] = array(
    'title' => 'Profiles',
    'description' => 'Create customizable fields for your users.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('profile_admin_overview'),
    'access arguments' => array('administer users'),
    'file' => 'profile.admin.inc',
  );
  .
  .
  return $items;
}

I understand the example in your "template" for using page arguments to pass args to the function. But with the above code, how else would "drupal_get_form" get the form id passed to it?

Besides just wanting to understand the proper way to do things, I guess I'd like to pose this question - how would I (and other noobies) know which Core modules we can use as examples and which ones are not conforming to this elusive "Drupal Way"?

And thanks again for all your help.

i'm sorry of my bad english,

horst_wessel - July 2, 2009 - 07:51

i'm sorry of my bad english, what i mean from "if you want to place two button, just place a two button instead " sentence is by putting

$form['submit-1'] = array(
  '#button_type' => 'submit',
  '#value' => t('Save'),
);

//other form elements
$form['submit-2'] = array(
  '#button_type' => 'submit',
  '#value' => t('Save'),
);

i know it seems stupid, but it works... since i'm not a very experienced drupal developer my self...

Have you defined 'access arguments' => array('field definitions'), on the hook_form? I have the same problem too, but after i define on hook form, i get it shown nicely :) you should take template http://api.drupal.org/api/file/developer/examples/page_example.module/6/... for a page, http://api.drupal.org/api/file/developer/topics/forms_api.html/6 for form api and http://api.drupal.org/api/file/developer/examples/node_example.module/6/... for a node. And for a newbie, i don't recommend to reverse engineering the other module... if you are truly understand the concept of hooks and page callbacks, you can do reverse engineering method.

cheers
hope that will help

cheers

You always need to call

dman - July 1, 2009 - 11:28

You always need to call drupal_render($form) at the end.
That will embed all the other (usually hidden) form fields that are required for form API to work, including step, form ID and a security token. REQUIRED.
Whenever you render form element yourself, it gets a little 'is_rendered' flag set. The final tidy-up form_render captures everything that was not marked as being done yet.
All forms made with FAPI expect, and require that the declared form elements that were there in the form definition are still present when that form is committed.

.dan.

yep, cause drupal actually

horst_wessel - July 1, 2009 - 12:20

yep, cause drupal actually register every form for security issues... so u must either use drupal_get_form('form-id') or drupal_render ($form). so that it will generate security code...

cheers

 
 

Drupal is a registered trademark of Dries Buytaert.