Displaying some form elements outside of a form (ie by passing them through drupal_render()) is impossible, since form_builder sets certain defaults that are required by theming functions. Most notably, most elements do not display at all if #title_display is not set explicitly, and several elements will not display without explicitly setting #parents.

As an example, the following code:

  $items['test'] = array(
    '#type' => 'textfield',
    '#title' => 'My Field',
    '#value' => 'My Value',
    '#disabled' => true,
  );
  
  return $items;

shows a blank page. When #title_display is added like so:

function form_test_test_page() {
  $items = array();
  
  $items['test'] = array(
    '#type' => 'textfield',
    '#title' => 'My Field',
    '#title_display' => 'before',
    '#value' => 'My Value',
    '#disabled' => true,
  );
  
  return $items;
}

the field displays, but with the following notices/warnings:

    *  Notice: Undefined index: #required in _form_set_class()  (line 3064 of /var/www/html/mak413/includes/form.inc).
    * Notice: Undefined index: #parents in form_get_error() (line 1160 of /var/www/html/mak413/includes/form.inc).
    * Notice: Undefined index: #parents in form_get_error() (line 1164 of /var/www/html/mak413/includes/form.inc).
    * Warning: implode(): Invalid arguments passed in form_get_error() (line 1164 of /var/www/html/mak413/includes/form.inc).
    * Notice: Undefined index: #name in theme_textfield() (line 2756 of /var/www/html/mak413/includes/form.inc).
    * Notice: Undefined index: #id in theme_textfield() (line 2756 of /var/www/html/mak413/includes/form.inc).
    * Notice: Undefined index: #title_display in theme_form_element() (line 2943 of /var/www/html/mak413/includes/form.inc).

The attached path simply copies the defaults for all form elements from form_builder to drupal_render. This fixes the #title_display issue, but leaves the notices and warnings. Hopefully someone can help out with that, as my core-fu is weak. Thanks!

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

effulgentsia’s picture

Subscribing.

agentrickard’s picture

Priority: Critical » Normal
Status: Active » Needs work

First, I'm not sure why this should be allowed. form_builder() has legitimate reasons for being a necessary function.

Second, if it should be allowed, the patch should be applied to http://api.drupal.org/api/function/system_element_info/7, where the default items are added to rendered forms. These are not 'common elements,' they are form specific elements.

Personally, I don't see this as 'critical' as the request is essentially: "I cannot bypass the FormsAPI and arbitrarily render form elements." You couldn't do this in prior versions of Drupal, either.

agentrickard’s picture

Considering that, I don't think this is a bug, either. It's a feature for developers. But I'll leave that status alone.

Damien Tournoud’s picture

Well, the default values for form elements (as defined is system_element_info()) should be the only thing required for rendering. This is a bug. Not critical, but still a bug.

makononov’s picture

Status: Needs work » Active

Dropping the priority down to normal for those reasons. This *did* work in prior versions of Drupal (I haven't had a chance to sift through the Drupal 6 code yet to see what changed). If this is desired behavior, then it should be clarified that the only thing you can return in a render array for a page callback are markup type elements.

Personally, I can think of several reasons why you would want to use form elements outside of a form. I would agree however, that the attached patch probably isn't the "right" solution.

makononov’s picture

Status: Active » Needs work
agentrickard’s picture

Right, but it's a simple change. Just move the += code into the proper function and you're done.

effulgentsia’s picture

Component: forms system » base system
Issue tags: -FAPI +Needs tests

I would argue that this issue is part of the "render system" component, which doesn't exist as a component choice yet, so moving to "base system".

We should start with a patch that adds a test that fails: a page callback that returns a render array containing an element of each type, and see what happens. We'll then have to decide which types we don't wish to support outside of the FAPI pipeline (for example, "checkboxes" has logic in #process that we probably don't want to move to #pre_render), remove those from the tests, and add documentation somewhere explaining those types are for FAPI-use only. For the types that make sense to support outside of FAPI, we'll need to make those tests pass.

@makononov: are you up for writing the test?

sun’s picture

The only #types that are valid to use outside of Form API are

- fieldset
- vertical_tabs

Perhaps more. But anything that can receive user input needs to go through FAPI. Also allowing to render form elements receiving input without Form API would just introduce a pile of security issues.

makononov’s picture

@sun the two element types that alerted me to this issue were of #type item and fieldset. Items would not render at all due to #title_display not being set, and fieldsets would complain about the lack of a #parent array.

@effulgentsia I've never written a test before, but that sounds like a good idea and I'm willing to give it a try. I'll put some time into it tomorrow morning.

@agentrickard I'll move it into system_element_info() and roll a new patch, but the #parent issue is going to be slightly more complicated. Both drupal_render() and system_element_info() have no idea about hierarchy and so would be unable to set the #parent array. Any idea how to approach it?

makononov’s picture

Assigned: Unassigned » makononov
moshe weitzman’s picture

I disagree with #9 by sun. Please make a case why #title_display is a form specific property and not a render property. I don't think we can. So, it should be declared in system_element_info(), not form_builder().

IMO if people want to bypass FAPI, they are on their own. We don't need a framework that plays nanny for you and prohibits reusing form elements as developer sees fit.

moshe weitzman’s picture

For example, assume I am building a form which submits to a 3rd party site. Its silly IMO to prohibit dev from using our render and alter system while completing this task.

sun’s picture

ok, let's simply see how far we can go with moving stuff off to hook_element_info()

makononov’s picture

The attached patch adds additional defaults to system_element_info() which solves several of the problems. Many form elements still throw warnings and notices due to the lack of a #parents array, and fieldsets are still almost completely broken.

EDIT: Ignore this -- didn't test it completely, sorry.

makononov’s picture

Update patch -- #name and #id can't be initialized inside of system_element_info, as it messes with actual form functionality.

makononov’s picture

Attached is a test that checks drupal_render()'s ability to render form elements outside of FAPI.
Before the patch from post #16: 7 failures, 59 exceptions.
After the patch: 2 failures, 49 exceptions

makononov’s picture

Status: Needs work » Needs review
FileSize
14.06 KB

The attached patch adds some additional defaults into system_element_info() and some empty checking in the form.inc theme functions which now allows form elements to render outside of the forms API. It also includes a test which verifies that the rendering takes place properly. Please note that the test still throws several notices and warnings due to the lack of a #parents array.

Status: Needs review » Needs work

The last submitted patch, render_form_elements_outside_of_forms.patch, failed testing.

makononov’s picture

Status: Needs work » Needs review
FileSize
14.04 KB

Missed an option when generating the patch.

Status: Needs review » Needs work
Issue tags: -Needs tests

The last submitted patch, render_form_elements_outside_of_forms.patch, failed testing.

makononov’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests
makononov’s picture

Status: Needs review » Needs work

The last submitted patch, render_form_elements_outside_of_forms.patch, failed testing.

makononov’s picture

This patch now passes all tests except for the issues with the lack of a #parents array. Can anyone more familiar with the render system explain to me a good place to get this array filled with defaults (or an alternative approach)?

makononov’s picture

Assigned: makononov » Unassigned
sun’s picture

Note that most form.inc changes in this patch are already covered in #690980: Disabled form elements not properly rendered, which is hopefully committed ASAP.

sun’s picture

Title: Unable to render form elements that have not been passed through form_builder(). » Form elements cannot be rendered without form_builder()
Component: base system » theme system
Assigned: Unassigned » sun
Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
20.5 KB

Revamped this patch. Looks like it's time to vastly decrease the file size of form.inc ;)

Let's only handle the general issue of rendering form elements here. Separate from the specific collapsible fieldsets + vertical tabs cases, as those are much more complex and should be resolved in #857124: Collapsible fieldsets and vertical tabs do not work without form_builder() (Form API). I.e., here, we only prepare theme_fieldset() to work without form_builder(), nothing else.

Status: Needs review » Needs work

The last submitted patch, drupal.render-form-elements.27.patch, failed testing.

sun’s picture

Status: Needs work » Needs review
FileSize
22.63 KB

form_get_error(). What kind of an odd function is that. Sheeeesh.

sun’s picture

This is quite possibly one of the most awesome Form API theme function clean-up patches thus far.

yched’s picture

Heh, nice promotion work :-)

+++ modules/field_ui/field_ui.admin.inc	1 Sep 2010 17:16:52 -0000
@@ -1480,13 +1480,16 @@ function field_ui_field_edit_form($form,
-  $form['instance'] = array(
+  $form['instance'] = array_merge(element_info('fieldset'), array(

why on this specific fieldset amongst all core fieldsets ?

Powered by Dreditor.

sun’s picture

That's due to a yet not well known problem that will bite all of us very badly in D7:

That particular fieldset manually sets a #pre_render, i.e.

  $form['myfieldset'] = array(
    '#type' => 'fieldset',
    '#pre_render' => array('yay_prerender_yay'),
  );

form_builder(), however, does:

  // Use element defaults.
  if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = element_info($element['#type']))) {
    // Overlay $info onto $element, retaining preexisting keys in $element.
    $element += $info;      // <<------------------------------------
    $element['#defaults_loaded'] = TRUE;
  }

So unless you know that system_element_info() defines:

  $types['fieldset'] = array(
    '#process' => array('form_process_fieldset', 'ajax_process_form'),
    '#pre_render' => array('form_pre_render_fieldset'),
    '#theme_wrappers' => array('fieldset'),
  );

You are overriding (skipping) form_pre_render_fieldset() entirely, which is NOT the intention in 99.99% of all cases.

yched’s picture

Hm, bad DX mojo indeed.

We didn't have that in D6 ?

(as a side note, I don't even agree with that specific #pre_render callback in field_ui - field_ui_field_edit_instance_pre_render

sun’s picture

We had the problem in D6 already, but it wasn't as evident as it is in D7, because not many modules were actively making use of #process, #pre_render, #after_build, and whatnot.

TBH, when I saw that #pre_render in Field UI, my reaction was: Oh dear lord, this module needs some love. ;) But frankly, I'd prefer to defer that to #616240: Make Field UI screens extensible from contrib - part II or a new issue.

yched’s picture

Field UI does needs some love.
And that part won't be adressed by #616240: Make Field UI screens extensible from contrib - part II. Anyway, different issue indeed.

Can we add a comment for this array_merge(element_info('fieldset'), ...) ?

sun’s picture

Sure thing :)

sun’s picture

Component: theme system » forms system

Any other feedback?

sun’s picture

sun’s picture

Any feedback?

moshe weitzman’s picture

Status: Needs review » Needs work

Looks like another nice cleanup. I just found a couple comment snafus.

+++ includes/common.inc	3 Sep 2010 00:53:16 -0000
@@ -5582,6 +5582,30 @@ function element_get_visible_children(ar
+ *   except for the leading '#', then an attribute name value is sufficientn and

typo: sufficientn

+++ includes/form.inc	3 Sep 2010 00:53:16 -0000
@@ -1369,17 +1369,20 @@ function form_get_errors() {
+ * Form errors higher up in the form structure override more deeper errors and

remove 'more'. is redundant

+++ includes/form.inc	3 Sep 2010 00:53:16 -0000
@@ -3338,10 +3319,16 @@ function theme_file($variables) {
+  // This function is invoked as theme wrapper, but themed form elements may
+  // not necessarily went through form_builder().

busted 'went'

+++ includes/form.inc	3 Sep 2010 00:53:16 -0000
@@ -3486,10 +3473,12 @@ function _form_set_class(&$element, $cla
+  // This function is invoked from theme functions, but themed form elements may
+  // not necessarily went through form_builder().

busted 'went'

Powered by Dreditor.

sun’s picture

Status: Needs work » Needs review
FileSize
23.11 KB

Thanks for reviewing, moshe! Fixed all of those.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

Good to go.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Alright. Committed to CVS HEAD. Thanks.

sun’s picture

We are trying to solve the default properties problem outlined in #33 over in #914792: Custom element properties entirely override default element info properties -- and we would love some developer feedback on two different resolution attempts, so please review them and share your thoughts over there!

catch’s picture

Alan D.’s picture

I think this has been my first WTF with Drupal 7. I have a simple use case to append a fieldset with a large help section to the content. This is what is being return from the page callback: (Running Drupal dev 30 Sept, 2010)

<?php
  return array(
    'table' => array(
       '#markup' => 'The main content',
    ),
    'help' => array(
      '#type' => 'fieldset',
      '#title' => t('Help'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      'subcontent' => array(
        '#markup' => 'This is the help',
      ),
    ),
  );
?>

The code generates a fieldset just fine, but it lacks all of the functionality. Pasting the code into an existing form, no issues at all. This lead me here.

The separation of the rendering and processing is a bit alien to me. When skim reading over the changes introduced in Drupal 7, I assumed that all content, either $forms or $output would have been run through the exact same rendering & processing functions, with the exception of the $form element that would have had additional processing to append the wrapping form, cached values, etc. Now it seems like a hybrid mix. Anyway that's my 5 min rant.

The workaround is to do the additional step when generating the content. This is not the best solution as you invoke the Forms API completely, but it works (full version from the module this time):

<?php
function my_page_callback() {
  ...
  return array(
    'table' => array(
       '#markup' => theme('table', array('header' => $header, 'rows' => $rows)) . $help . theme('item_list', array('items' => $help_items)),
    ),
    'help' => drupal_get_form('_name_format_help_form'),
  );
}

/**
 * A workaround to display a collapable fieldset inside a standard page.
 */
function _name_format_help_form() {
  $form['help'] = array(
    '#type' => 'fieldset',
    '#title' => t('Format string help'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    'format_parameters' => array(
      '#markup' => theme('name_format_parameter_help'),
    ),
  );
  return $form;
}
?>
moshe weitzman’s picture

+++ modules/simpletest/tests/common.test	15 Sep 2010 02:50:08 -0000
@@ -1470,6 +1470,132 @@ class DrupalRenderUnitTestCase extends D
+    $this->assertRenderedElement($element, '//fieldset/legend[contains(., :title)]', array(

this line actually tests that fieldsets work outside of forms. so please help isolate if the test is wrong or otherwise.

Powered by Dreditor.

Alan D.’s picture

Status: Fixed » Needs review
FileSize
1.61 KB

The system goes through and process's the fieldset through drupal_render(). Very cool except that half of the functionality is in the FAPI #process callback: as per the dump after += element_info() shows below. Thus the eariler comment about not liking the split between the two processing systems if both are meant to do the same thing!

#process (Array, 2 elements)
            0 (String, 21 characters ) form_process_fieldset
            1 (String, 17 characters ) ajax_process_form
#pre_render (Array, 1 element)
            0 (String, 24 characters ) form_pre_render_fieldset

The function form_process_fieldset() adds the magic for the fieldset, but this is never called. Moving the code from #process to #pre_render works on this singular use-case, but I'm not sure if it would break things in other places.

Totally untested code as I do not have a working HEAD version nor SimpleTest set up, and I had to step through with manual debugging on another Drupal 7 setup. :(

Alan D.’s picture

At least this lead to a cleaner workaround:

<?php
  $form = array(
    '#type' => 'fieldset',
    '#title' => t('Format string help'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#parents' => array(),
    'format_parameters' => array(
      '#markup' => theme('name_format_parameter_help'),
    ),
  );

  // Add the collapsible magic from the FAPI system.
  $form_state = array();
  $form = form_process_fieldset($form, $form_state);
  
  return array(
    'table' => array(
       '#markup' => theme('table', array('header' => $header, 'rows' => $rows)) . $help . theme('item_list', array('items' => $help_items)),
    ),
    'help' => $form,
  );  
?>
Alan D.’s picture

FileSize
1.55 KB

Window line endings, this time with *NIX

sun’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.