Problem

  • A '#type' => 'container' returned in the top-level array of a menu page callback is ignored.

Goal

  • Make the page rendering system respect everything that is in the top-level renderable array returned from a page callback.

Details

  • A page callback invoking drupal_get_form() returns a renderable array having '#type' => 'form' on the top-level, which happens to be respected (by coincidence).
  • A page callback returning a '#type' => 'container' on the top-level ignores the container.

Cause

  • #type 'container' defines #theme_wrappers, but the page callback result is ultimately rendered as a block, and a block already defines a dedicated #theme_wrappers function.
  • The page rendering process does not merge the #theme_wrappers of the page callback and the (system-main) block.

------------- ORIGINAL SUMMARY -------------

Problem/Motivation

In drupal_render(), the result of element_info($elements['#type']) is added to the element, and not merged recursively. In the case of the original post below, #theme_wrappers is set to array('block') by _block_get_renderable_region()/_block_get_renderable_array() (d8/d7), and if '#type' => 'container', then #theme_wrappers won't have container like it should.

Proposed resolution

I attempted to use drupal_array_merge_deep(), but in this case all of the #process was duplicated, and large parts of the markup were then duplicated (admin_menu, colorbox). My two current ideas are a way to reduce duplicates in render array values, or a way to reduce duplicates in drupal_array_merge_deep(), neither of which seem like good or easy solutions.

Remaining tasks

Find a real fix

User interface changes

-

API changes

-

Original report by tim.plunkett

When used in the top level of a render array that represents the main content, '#type' => 'container' has no effect.


function container_fail_menu() {
  $items = array();
  $items['container_fail'] = array(
    'title' => 'Container Fail',
    'page callback' => 'container_fail_callback',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

function container_fail_callback() {
  return array(
    '#type' => 'container',
    '#attributes' => array(
      'id' => 'container-id',
      'class' => array(
        'container-class1',
        'container-class2',
      ),
    ),
    'child-element' => array(
      '#type' => 'container',
      '#attributes' => array(
        'class' => array(
          'nested-container',
        ),
      ),
      'child-element-markup' => array(
        '#markup' => t('This prints fine.'),
      ),
    ),
  );
}

Expected output:

<div id="block-system-main" class="block block-system"><div class="content">
<div id="container-id" class="container-class1 container-class2"><div class="nested-container">This prints fine.</div></div>
</div></div>

Actual output:

<div id="block-system-main" class="block block-system"><div class="content">
<div class="nested-container">This prints fine.</div>
</div></div>

Comments

tim.plunkett’s picture

Status: Active » Needs review
StatusFileSize
new614 bytes

Wrapping both cases in an array makes more sense to me anyway.

tim.plunkett’s picture

If a render array has '#block' => $some_block and '#theme_wrappers' = array('block'), '#type' => 'container' doesn't work. Maybe that's the real bug here.

sun’s picture

Why does #type 'form' work though?

tim.plunkett’s picture

Title: Using '#type' => 'container' should be valid when used in drupal_set_page_content() » In drupal_render(), element_info is not merged recursively, preventing #type from serving its purpose
Status: Needs review » Active

In the middle of drupal_render():
http://drupalcode.org/project/drupal.git/blob/refs/heads/8.x:/includes/c...

if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
  $elements += element_info($elements['#type']);
}

In the case of the system_main block, '#theme_wrappers' => array('block'). When '#type' => 'container', the above code attempts to add container as a theme_wrapper, but the elements aren't merged recursively, so it isn't added.

I tried using drupal_array_merge_deep(), but it doesn't reduce duplicates, causing all of the #process callbacks to be duplicated, which led to sheer craziness.

Should there be checks added to prevent duplicate values of these FAPI/render keys from existing? Or modify drupal_array_merge_deep to remove duplicates? I see no easy generic solution at the moment.

tim.plunkett’s picture

Issue summary: View changes

Updated issue summary.

tim.plunkett’s picture

Issue summary: View changes

Updated issue summary.

sun’s picture

Title: In drupal_render(), element_info is not merged recursively, preventing #type from serving its purpose » drupal_render_page() completely ignores what a page callback returns
Priority: Normal » Major
Issue tags: -Needs backport to D7

Clarified issue summary. Better title. This cannot be backported.

tim.plunkett’s picture

In response to #3, this doesn't happen with '#type' => 'form' because by the time _block_get_renderable_array loads the block, #theme_wrappers already contains form, whereas with container, #theme_wrappers is added by drupal_render as mentioned above.

Also, I'm testing this on D7, I need to double check all of this on D8 and I will report back.

sun’s picture

Title: drupal_render_page() completely ignores what a page callback returns » Page rendering completely ignores what a page callback returns as top-level element

We have a helper function for the "outside scenario": http://api.drupal.org/api/function/element_info_property/7 (see callers)

I guess I'd be fine with unconditionally adding 'main' as an additional layer (+ tests) as a stop-gap solution. Not backportable. Essentially, however, this merely defers/delays the debate and solution we need to have about merging "default" element properties. But anyway, guess I'd +1 the stop-gap fix by now.

The answer of "hey hipster, you should have known that #type defines #foo and #bar - not in the element info, you know, but somewhere else in the page rendering process - so you need to also apply *those* defaults in your page callback!" is an unacceptable answer.

I'd rather like to see this fixed now, instead of being forgot and still not resolved for D8. Since even the stop-gap fix is not backportable.

So let's make sure that we fix what we can fix easily in order to prevent annoyance in the long run. *Then* attempt to fix it properly. Should be a matter of minutes - especially since the current patch passed already.

In essence: is the block containing output the same element as the block?
or is the output a sub-element of the block?

Right now core tries to do the former, mixing the output into the block.

well 'main page content' being a required block is what really messes with this

Still a block. Of course, if you disable block.module, we run into a larger debate ;) Then, we speak about "containers holding the output of atomic pieces on a page".

Question remains the same though - is it valid/legit to merge the result of the handler responsible for the container /into/ the definition of the page element that makes up the container?

No, hence trying to move it into a sub element of system_main.

Forget about #type container, this affects everything.

well right, but when does it mainfest? why DOES type form work?

Yeah, since #type 'form' also uses #theme_wrappers, that's a detail I'd still like to see clarified in the issue.

tim.plunkett’s picture

Status: Active » Needs review
StatusFileSize
new654 bytes

When block.module is disabled, none of this a problem, because drupal_set_page_content($page); is called at the top of drupal_render_page(), and the render array is used properly.
When block.module is enabled, the callback result should be the child of system_main.

There remains the issue of fixing the element_info merging.

Setting back to needs review for this non-ternary reroll of #1.

sun’s picture

Issue tags: +Needs backport to D7

So, #type 'form' works, because drupal_prepare_form() does

  $form += element_info('form');

upfront!

Likewise, that page callback returning a container would work equally, if the full element info would be pre-populated upfront.

Meanwhile, I'd agree on adding the 'main' layer for D8 there.

But given this analysis, for D7, I wonder whether we can't squeeze a

if (isset($page_callback_result['#type']) {
  $info = element_info($page_callback_result['#type']);
  if ($info) {
    $page_callback_result += $info;
  }
}

into a function somewhere?

Status: Needs review » Needs work

The last submitted patch, drupal-1300290-8.patch, failed testing.

tim.plunkett’s picture

Title: Page rendering completely ignores what a page callback returns as top-level element » In drupal_render(), element_info is not merged recursively, preventing #type from serving its purpose
Status: Needs work » Active

Uh, this is completely unreproducible in D8. _block_get_renderable_block() fixes this entire problem by doing $element['content'] = $block->content;, effectively the same as nesting everything into 'main' like my patch does.

This was fixed in D8 by #918808: Standardize block cache as a drupal_render() #cache, which was only a week or so ago.

Since my patch isn't backportable to D7, I think the focus should shift back to the problems with element_info. Restoring the title (which can still be cleaned up).

sun’s picture

That "fix" seems to have been completely unintentional.

1) I'd still like to see a unit test, which ensures that this works; regardless of version.

2) In light of #9, I wouldn't lose hope just yet. A fix along those lines would look "backportable" to me (along with the test).

sun’s picture

Status: Active » Needs review
StatusFileSize
new2.26 KB

Starting point.

Status: Needs review » Needs work

The last submitted patch, drupal8.render-top-level-test.13.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new2.35 KB

Rerolled, will still fail.

Status: Needs review » Needs work

The last submitted patch, drupal-1300290-15.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new2.32 KB

Rerolled.

sun’s picture

Hm. I wonder whether #1473548: CSS classes in $page['#attributes']['class'] are not applied to HTML BODY is related to this or not. (definitely different element/scenario, but seemingly similar situation in that the problem affects the "top-level" render element only)

Status: Needs review » Needs work

The last submitted patch, drupal-1300290-17.patch, failed testing.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett
Priority: Major » Normal

I'm going to work on this, but I no longer believe it to be major

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

I actually don't trust the approach of this test anymore. @sun, any help?

tim.plunkett’s picture

Issue summary: View changes

Updated issue summary.

dawehner’s picture

Component: base system » render system

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

tim.plunkett’s picture

Version: 8.3.x-dev » 8.4.x-dev
Status: Needs work » Needs review
Issue tags: +Needs tests
StatusFileSize
new3.82 KB

Status: Needs review » Needs work

The last submitted patch, 26: 1300290-form-26.patch, failed testing. View results

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new3.53 KB
new3.82 KB

Got the order of mergeDeep backwards, switching the order.

Status: Needs review » Needs work

The last submitted patch, 28: 1300290-element-28.patch, failed testing. View results

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new1.86 KB
new3.86 KB

Turns out the integer array keys matter

Status: Needs review » Needs work

The last submitted patch, 30: 1300290-element-30.patch, failed testing. View results

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

tim.plunkett’s picture

Issue tags: +Needs reroll
aleevas’s picture

Issue tags: -Needs reroll

Looks like this one no need reroll, because it's applied to branch 8.7.x

markhalliwell’s picture

I think this should either mark this as "works as designed" or bumped to 9.x.

Recursively merging element info now would, technically, be a massive breaking change and would likely introduce duplication callback hell for contrib/custom code.

To give you an example, it's quite common to see the following due to the current limitation:

  $elementInfo = \Drupal::service('element_info');
  $build['container'] = [
    '#type' => 'container',
    '#pre_render' => array_merge($elementInfo->getInfoProperty('container', '#pre_render', []), [
      'my_custom_pre_render_callback_function',
    ]),
    '#process' => array_merge($elementInfo->getInfoProperty('container', '#process', []), [
      'my_custom_process_callback_function',
    ]),
  ];

This is really just another by-product of using render arrays; how they were originally created.

I've run into this on occasion, but easily fixed this "flaw" by simply placing everything in a wrapper element instead of the top level render array.

$build['wrapper'] = ['#type' => 'container'];

---

Possible dup? #2070131: Form & Render Element default properties not properly merged when property is an array of callbacks

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

johnalbin’s picture

Status: Needs work » Closed (works as designed)

I'm going to agree with Mark here. And dawehner in #2894952: Render element preRenders don't get added if a module form_alters the $element.

This is tricky. The current behaviour allows to remove pre_render functions completely, merging it in would no longer allow that.

It's a deep flaw in Form API. There are tons of places in Form API where you try to make a specific change and it causes a cascade of problems later in the rendering process.

For example, if you use form_alter to make a type-specific change to an element, a later alteration can change the #type and now your change makes no sense.

From my quick count, there are at least 4 places where you can modify a form and late or early changes can muck up the rendering. Let's see…

  1. The merging of '#type' => 'form' defaults from \Drupal::service('element_info')->getInfo(). These defaults can be altered by a module or theme with hook_element_info_alter.
  2. A module's hook_form_alter or hook_form_BASE_FORM_ID_alter or hook_form_FORM_ID_alter.
  3. And then a theme's hook_form_alter or hook_form_BASE_FORM_ID_alter or hook_form_FORM_ID_alter. I mention this separately because a module's implementation always comes before a theme's implementation.
  4. The #process functions
  5. The #after_build functions
  6. The #pre_render functions on the form. The list of these functions can be altered by any previous form alteration
  7. The preprocess functions on a form-id-specific theme hook. While the "form" hook is always a #theme_wrappers where the form is pre-rendered, a form can contain a #theme hook based on the form ID or type of form (e.g. confirm_form)and, after a specific template suggestion is picked, the preprocess functions can alter variables and since the form is an un-rendered variable, the form can be once again altered.
  8. For each element in the form, the merging of #type defaults from \Drupal::service('element_info')->getInfo(). These defaults can be altered by a module or theme.
  9. The #pre_render functions on the elements
  10. The preprocess functions for the elements' theme hooks. You can alter the list of preprocess functions in hook_theme_alter

Oh, wait. That's 10 places.

So, merging the element info defaults is only a tiny part of a larger problem. And fixing this tiny problem is way too late; many developers know about this problem and new devs will instantly see the breakage when trying a simple form_alter.

IMO, the only possible fix to this is to completely replace Render/Form API with a next-generation API.

In the meantime, might I suggest documenting all the ways a form can be altered? #2909574: Document how to alter forms when hook_form_alter won't work