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>
| Comment | File | Size | Author |
|---|---|---|---|
| #30 | 1300290-element-30.patch | 3.86 KB | tim.plunkett |
| #30 | 1300290-element-30-interdiff.txt | 1.86 KB | tim.plunkett |
| #28 | 1300290-element-28.patch | 3.82 KB | tim.plunkett |
| #28 | 1300290-element-28-interdiff.txt | 3.53 KB | tim.plunkett |
| #26 | 1300290-form-26.patch | 3.82 KB | tim.plunkett |
Comments
Comment #1
tim.plunkettWrapping both cases in an array makes more sense to me anyway.
Comment #2
tim.plunkettIf a render array has
'#block' => $some_blockand'#theme_wrappers' = array('block'),'#type' => 'container'doesn't work. Maybe that's the real bug here.Comment #3
sunWhy does #type 'form' work though?
Comment #4
tim.plunkettIn the middle of drupal_render():
http://drupalcode.org/project/drupal.git/blob/refs/heads/8.x:/includes/c...
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#processcallbacks 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.
Comment #4.0
tim.plunkettUpdated issue summary.
Comment #4.1
tim.plunkettUpdated issue summary.
Comment #5
sunClarified issue summary. Better title. This cannot be backported.
Comment #6
tim.plunkettIn 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.
Comment #7
sunWe 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.
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.
Yeah, since #type 'form' also uses #theme_wrappers, that's a detail I'd still like to see clarified in the issue.
Comment #8
tim.plunkettWhen block.module is disabled, none of this a problem, because
drupal_set_page_content($page);is called at the top ofdrupal_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.
Comment #9
sunSo, #type 'form' works, because drupal_prepare_form() does
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
into a function somewhere?
Comment #11
tim.plunkettUh, 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).
Comment #12
sunThat "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).
Comment #13
sunStarting point.
Comment #15
tim.plunkettRerolled, will still fail.
Comment #17
tim.plunkettRerolled.
Comment #18
sunHm. 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)
Comment #20
tim.plunkettI'm going to work on this, but I no longer believe it to be major
Comment #21
tim.plunkettI actually don't trust the approach of this test anymore. @sun, any help?
Comment #21.0
tim.plunkettUpdated issue summary.
Comment #22
dawehnerComment #26
tim.plunkettComment #28
tim.plunkettGot the order of mergeDeep backwards, switching the order.
Comment #30
tim.plunkettTurns out the integer array keys matter
Comment #35
tim.plunkettComment #36
aleevasLooks like this one no need reroll, because it's applied to branch 8.7.x
Comment #37
markhalliwellI 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:
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.
---
Possible dup? #2070131: Form & Render Element default properties not properly merged when property is an array of callbacks
Comment #42
johnalbinI'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.
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…
'#type' => 'form'defaults from\Drupal::service('element_info')->getInfo(). These defaults can be altered by a module or theme withhook_element_info_alter.hook_form_alterorhook_form_BASE_FORM_ID_alterorhook_form_FORM_ID_alter.hook_form_alterorhook_form_BASE_FORM_ID_alterorhook_form_FORM_ID_alter. I mention this separately because a module's implementation always comes before a theme's implementation.#processfunctions#after_buildfunctions#pre_renderfunctions on the form. The list of these functions can be altered by any previous form alteration#theme_wrapperswhere the form is pre-rendered, a form can contain a#themehook 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.#typedefaults from\Drupal::service('element_info')->getInfo(). These defaults can be altered by a module or theme.#pre_renderfunctions on the elementshook_theme_alterOh, 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