Problem/Motivation
Every region template (eg. core/modules/system/templates/region.html.twig) uses {{ content }} to render the blocks in that region. But unlike node templates (eg. core/modules/node/templates/node.html.twig), where you can do things like {{ content|without('field_example') }}, the content of a region cannot be manipulated. This is because the content variable of a region is not an array of renderable elements, but just a string of rendered HTML.
Sometimes you want to be able to have access to the separate blocks, for instance to do something like this:
<div class="grid-container">
{{ content|without('special-block') }}
</div>
{{ content.special-block }}
Proposed resolution
The reason why blocks arrive in the template as an already rendered blob of HTML, can be found in the comments on line 439 and 440 of core/lib/Drupal/Core/Render/Renderer.php:
// If #theme is not implemented or #render_children is set and the element
// has an empty #children attribute, render the children now. [...]
This also points us in the direction of a solution: Build the render array of a region with a #theme property instead of a #theme_wrapper. As far as I can see, this results in the exact same output as the current situation, requiring no changes to twig templates, while adding additional flexibility for those who need it.
Remaining tasks
I did not find any downsides yet, but we have to think of reasons to not do this.
No reasons have been brought forward.
Are there performance implications?
None, except that a little more function nesting (still well within reasonable limits) might theoretically slow down xDebug.
Could caching be a problem when region output is made more dynamic? And if so, is that a problem for Drupal core to solve, or just a case of "with great power..."?
Manual testing has shown that caching works as expected, also when making use of the new capabilities we are adding. Also, all automated tests pass.
Decide what to do about regions that contain blocks, but eventually render to an empty string. Currently they skip rendering the div.region wrapper, but after the patch the wrapper is rendered. See comment #43.
This is not a problem if we target the 10.x-alpha release, see comment #47.
User interface changes
none
API changes
none (see also comment #34)
Data model changes
none
Release notes snippet
Do we need one?
| Comment | File | Size | Author |
|---|---|---|---|
| #79 | render-region-blocks-later-3178202_79.patch | 23.95 KB | gcb |
| #65 | core-render-blocks-later-3178202-65.patch | 22.26 KB | watergate |
| #64 | UmamiPages.zip | 39.73 KB | tyler36 |
Issue fork drupal-3178202
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
marcvangendPatch attached. I was surprised how little code I needed to change to make this work :-)
Comment #4
marcvangendI'll look into that test failure tomorrow. At first sight I don't understand what the batch test has to do with this patch, but I guess there's some kind of logic to it.
Comment #5
joycelam commentedi've applied and tested the patch from #2 on one of our websites and it works. Thanks!
Comment #6
marcvangendThanks for testing, Joyce.
Meanwhile I did find a somewhat related issue: #2714509: Remove usages of #theme_wrappers
Comment #7
marcvangendThis patch should fix the failing test. The previous patch could accidentally mess up render elements when a render array returned by a controller has
'#type' => 'page'(as \Drupal\system\Controller\BatchController::batchPage does).Comment #8
sutharsan commentedThe variable name
$blockconfused me. I suggest to change it to$key, asElement::children()returns 'The array keys of the element's children'.Comment #9
marcvangendThanks Sutharsan, fair point. I used $block because the keys are usually (but not even necessarily) block identifiers. Indeed $key is more correct. New patch attached.
Comment #10
marcvangendOops, made a silly mistake when I did the fix in #7.
Comment #11
BarisW commentedThis could use another review
Comment #12
marcvangendCorrecting another silly mistake.
Comment #14
marcvangendThis should fix the test failures. (Again, BatchController::batchPage does things differently than normal pages. Yay for automated tests.)
Comment #16
karlsheaThis patch is extremely helpful, thank you!
Comment #17
marcvangendThanks Karl :-)
If you find any problems caused by this patch, please post them here. Likewise, if you do not encounter issues, that's valuable feedback too!
Comment #18
karlsheaI did not find any issues at all. I needed to output blocks individually in a footer template, and the patch didn't seem to affect any of the existing output.
Comment #19
casey commentedNice one! I've been wondering the same.
#60552: Add region.tpl.php for all regions in themes is where it was introduced; https://git.drupalcode.org/project/drupal/-/blame/7.x/modules/system/sys...
The usage of #theme instead of #theme_wrapper doesn't seem to be discussed. The main reason for the region wrapper to be introduced was to have a wrapper element with an ID.
#60552-5: Add region.tpl.php for all regions in themes
It was also considered important to be able to not show the region wrapper if the region is empty.
Both is possible when using a #theme and more.
One issue I can think of is that it will increase the function nesting level, which might be an issue when using xdebug; but it wouldn't be much, I think it would increase the nesting level with 1 or 2.
Comment #20
marcvangendThanks for the historical research, Casey. I think the advantage of having a region template still applies today, and indeed this proposal does not change that. In fact the patch doesn't change anything about the way the region wrapper is rendered, it's all about the rendering of the region content (blocks).
I also found that the idea to early-render elements that do not have a #theme property, goes way back to at least 2009. This is before the release of Drupal 7: Before the introduction of render arrays and the paradigm that we add flexibility by rendering them at the last possible moment.
Regarding the function nesting level and xDebug, I put a breakpoint in a block Twig template to test this. Drupal seems to add 9 function calls to the stack between the rendering of the region template and the rendering of the block template. The total depth of the call stack was 60 at that point, so that's still well within the default value of 256.
All this leads me to believe that it was never a deliberate choice to pass pre-rendered HTML to the region template, but rather the unintentional effect of evolving code, never updated to reflect the new approach. I guess it's finally time to fix that.
KarlShea has already reported that this change works without any issues and my organization is running this patch in multiple production environments. I don't want to set my own patch to RTBC, but if someone else would, we might attract the attention of core maintainers.
Comment #21
karlsheaI'm also now using this on several sites and the patch lgtm.
Comment #22
quietone commentedAdding test with 9.3.x
Comment #23
alexpottThis can be written like:
For less local variable and a single assignment.
Comment #24
alexpottPinged this issue to @catch (as someone who groks the cache system and know the drupal tea-leaves) and @lauriii (as frontend framework manager) for more thoughts. I think this is pretty interesting and it's great to see that it possible.
It would be good to add some form of test coverage of the new capabilities this gives us.
Comment #25
marcvangendThanks for the review, Alex.
I wasn't quite sure if it is expected and acceptable to both overwrite and wrap
$page[$region]in a single statement, but I made that change now. The new patch has also been checked/rerolled against 9.3.x (not that it made any difference).Good point about tests too. I'm setting this to needs review now, so the new patch can be tested. After that it can go back to needs work for the tests.
Comment #26
marcvangendOops, missed a comma.
Comment #27
marcvangendNew patch with tests, otherwise unchanged.
Comment #28
marcvangendYet another patch... Only changes a single comment that I copy-pasted and forgot to edit.
Comment #30
marcvangendTo be sure, I did some manual testing regarding cacheability. As far as I can see, everything looks good.
During these manual tests I was not logged in. The page_cache module was not installed because it does not support cache contexts for anonymous users, but dynamic_page_cache was enabled.
To test the cacheability of blocks, I took the following steps:
date_default_timezone_get().The above works as it should, both with and without the patch.
To test the cacheability of regions, I took the following steps:
$variables['#cache']['contexts'].The above works as it should when the patch is applied. This could not work without the patch, because you would not have a way to manipulate the region content and wrap a single block in a div.
As you can see, I had to add the 'timezone' cache context in the region preprocessor to make it work. However that is not a shortcoming of this change; as always it is the developer's responsibility to add cacheability metadata when the rendered output is made dependent on a variable such as the timezone.
Comment #31
neclimdulPoked at this in blackfire and there is definitely some shifting around of function calls.
First test was with all render and page caching disabled(null backends). Seems to be mostly the same call graphs. Fewer calls on one branch, more similar calls on another branch. The most noticeable change I think seems to be more calls to BubbleableMetadata:merge. The cost of which seems pretty minimal though I don't immediately understand the reason for them.
I ran 3 more comparisons, adding a layer of caching each time with the render cache, dynamic page cache, and full page caches. The call graphs seem identical which is what I would have expected.
Comment #32
andypostComment #33
marcvangend@andypost I'm sorry, I don't see how those issues are related. Can you elaborate?
Please note that this issue doesn't change anything about what is cached and how it is cached. It merely delays the point at which region content (usually blocks) is rendered. The only new way this relates to the caching system, is the situation where the new possibilities are used to A) vary the output of the region or B) change the output using some value from the database. Case A would need a cache context added to the region, while case B would require a cache tag. That said, adding cache contexts and/or cache tags to a region is already supported in Drupal core. My manual testing in #30 just shows that the patch doesn't break it and that it plays nice with the new possibilities.
Comment #34
marcvangendAs @lendude asked me in Slack:
In templates, I consider the variable names to be an API (as developer you can assume they don't change) and I expect the output of those variables to be stable. If such a variable is documented to support drilling down (eg.
{{ content.field_foo }}in a node template) I consider that part of the API too.As you can see in template_preprocess_region, we don't change the names of the available variables, only what's inside
$variables['content']. Also, the output of{{ content }}is identical after the change. The change does not effect any other variables that may have been added to the template. In other words: we are not breaking any API.The whole point of this issue is that currently content is a pre-rendered string of HTML. I can only imagine the patch breaking someone's preprocessor if they rely on str_replace, regex etc. to manipulate the HTML. But that's not something Drupal core can support.
Comment #35
marcvangendWhen I wrote "documented to support drilling down" in #34 I realized that we should probably document this in region(-*).html.twig files. The new patch is identical to #28, except that it changes this:
...to this, similar to what is found in node templates:
...in the two dozen places where it occurs.
Comment #37
marcvangendI never knew we also test file hashes of Classy templates. Fixed the test to match the documentation change introduced in #35.
Comment #39
marcvangendTest failure in #37 was intentional.
Comment #40
marcvangendComment #41
sutharsan commentedRemoving the "Needs tests" tag since a test has been added.
Comment #42
kristen polThanks for the patch and reviews.
I have tested the patch in #37 as follows:
The only differences were:
<div class="region region-tabs">...</div>was addedExample snippet:
I've attached the full file. The extra
divshows up in 77 places. I don't see any mention of this getting added in the comments above but maybe I missed it.Comment #43
marcvangendThank you Kristen for your review. The extra div turns out to be a logical effect of the change, but you are right this has not explicitly been mentioned before and I was not aware of this. This only happens in case the content of a region renders to an empty string.
To clarify (because I initially misread your comment): in the case of the Umami demo it is only a single div, the same 'tabs' region, which appears on 77 different URLs.
Analysis
The Umami demo site places one block in the tabs region: block ID 'umami_local_tasks', an instance of the LocalTasksBlock block plugin. For anonymous users there are no local tasks, so the block renders to an empty string. Note that this block is rendered with a #lazy_builder callback, as is the case with most blocks (except instances of MainContentBlockPluginInterface or TitleBlockPluginInterface, see \Drupal\block\BlockViewBuilder::viewMultiple).
Current Drupal core
Currently, the region content is flattened to HTML rather early (the lazy builder isn't as lazy as it could be!). The region.html.twig template then skips rendering the div.region wrapper if that string of HTML is empty (
{% if content %}).After the patch
The essence of this patch is that the region content (ie. blocks) is not rendered before the region template is being processed. Therefore, in the new situation the
{% if content %}check in region.html.twig returns TRUE if there is a block to be rendered, regardless of whether that block actually returns HTML or an empty string. This causes a div.region wrapper to be rendered while it used to be skipped.Possible solution
If this empty div is a problem, it is possible to avoid by changing
{% if content %}to{% if content|render|striptags|trim is not empty %}. The Umami theme itself uses this pattern a lot in its page.html.twig, and Olivero does it too. However this method has downsides, see #953034: [meta] Themes improperly check renderable arrays when determining visibility. We could add this to the core region.html.twig templates by default, but I don't think we should be introducing sub-optimal code all over the place. That said, it might make sense to do it in the stable/stable9 themes, since unchanging output is what they promise.Comment #44
marcvangendDiscussed with @lauriii on Slack. His thoughts:
Back to needs work.
Comment #45
neclimdulDoesn't look like I included this in my previous comment but I'd considered the "change" in behavior for content.
I actually think the current approach cleverly deals with this because in my experience, its impossible to actually do anything in the region template largely because of this issue. And believe me I've seen people try... Because of that I've only seen usable templates that limit themselves to general wrapping logic and
{{ content }}Alternatively, if I understand the suggested change correctly, we're going to have a fully rendered variable and a second render array variable. Then the "correct" way to render a region is to ignore the rendered version and double render all blocks by using the render array.
From a performance standpoint this seems really bad but I'll check the patch when its up.
Comment #46
marcvangendRe. #45: Thanks for bringing up performance. Yes, you understand the suggestion correctly. There would be two variables in a region template: the old pre-rendered
$contentand the new render array variable (let's call it$elementsfor now). Indeed this approach comes with a performance hit because of double rendering. We should test how noticeable this is.Actually, the more I think about the $content + $elements approach, the more downsides I see. It feels like we're not solving the problem, but we're trading it for another.
The thing is: at some point in the future we would have to get rid of the deprecated $content variable in favor of $elements. So what happens then? Do we simply remove $content, breaking all templates that still use it? Do we rename $elements to $content, breaking all templates that meanwhile switched to $elements? It seems to me that more sites will be affected at that point, and a removed template variable will probably cause more problems than an extra empty div would.
Of course one could argue that breaking things is more acceptable at the release of a new major version. If that is the consensus, let's just get this ready for Drupal 10, which should be less than 6 months away.
Comment #47
marcvangendAll right, let's make a decision before it's too late. The current situation:
My conclusion: Let's target 10.x, preferably alpha, and get this in core.
Comment #48
marcvangendComment #49
effulgentsia commentedOverall, I like a lot what this issue is attempting to do. It's definitely weird that 'content' is a render array in most templates, but a string in region.html.twig.
However, with respect to BC:
There's a potential BC break here that I don't think was mentioned in earlier comments. Within hook_preprocess_region(), $variables['elements'] is currently a render array whose direct children are the render arrays of blocks, but with this patch, it is now a render array with a 'region_content' child that wraps those blocks. This will break uikit, for example.
Our BC policy does allow us to break BC of render arrays, but despite that policy we should think through the implications of that.
I don't know yet what my recommendation is for this. I'll try to comment in the next couple days about that if this hasn't already been committed by then. Just wanted to point out the break itself in the meantime.
Comment #50
marcvangendRe #49: Thanks for your input @effulgentsia.
I agree, it would have been nicer if we didn't have to change the structure of the render array. The wrapping
region_contentwas introduced in patch #7 to fix the test failure in #2. The wrapper is not even needed for "normal" pages, only for a very specific case (quoting comment #7):So keeping the render array structure unchanged, as patch #2 did, is definitely an option if we find another way to solve the
'#type' => 'page'bug.Comment #51
marcvangendTrying out an alternative patch in an attempt to address #49. Tests pass locally so I'm curious if testbot agrees.
[edit] Please ignore this patch. It was a renewed attempt at the approach previously used in #2. Although the tests came back green, things actually break. [/edit]
Comment #52
alexpott@marcvangend we definitely need a change record here.
Comment #53
alexpottComment #54
marcvangendThanks for looking into it, Alex. I agree, especially for cases such as uikit, mentioned in #49, a change record is needed. (Not sure if the issue status should be "needs work" then.)
Anyway, I had a closer look at the uikit code to understand why it needs access to
$variables['elements']. I found thatuikit_preprocess_regionaims to place different blocks in three distinct divs within the navbar region template. So that is exactly what this issue enables you to do (albeit without preprocessors and in a way that is consistent with other templates). With this patch applied, the uikit preprocessor could have less code and be more efficient, because currently it needs to load and render blocks that have already been loaded and rendered.So while I understand that this change may require maintainers to adapt their code to the new render array, it may actually be a change for the better.
Comment #55
marcvangendHere's a new patch, addressing the BC concerns raised in #49. I managed to "find another way to solve the
'#type' => 'page'bug" as suggested in #50.First let me explain where this
'#type' => 'page'bug comes from. Most pages are built using the block system. This happens in\Drupal\block\Plugin\DisplayVariant\BlockPageVariant::buildand it results in a nicely structured $page array, where every region element is an array of block render arrays, keyed by block ID ($page[region_name][block_id] = [block_render_array]). However, in some rare cases, controllers take full control by returning a render array that already has'#type' => 'page'. One example is\Drupal\system\Controller\BatchController::batchPage. Batch controller does not follow the $page[region_name][block_id] structure, but instead places a render array with'#type' => 'page_title'directly in$page['header']. As a result, we cannot safely set$page['header']['#theme'] = 'region'because it would break the page title render array.This new patch detects the edge cases (like the batch page) by checking if the $page[region_name] element is a render array with a #theme or #type property. If so, it is wrapped in an array with key "region_content". In all other cases the structure is not altered: blocks remain the direct children of $variables['elements'].
So, does this mean that with this new patch $variables['elements'] is 100% identical? No. After all, the point of this patch is to render the blocks as late as possible. A block with a lazy builder will not yet be built when it passes through the region preprocessor (see image).
Comment #57
marcvangendI can't reproduce the test failure locally. Rerunning the test.
[update] It seems that the test fail in #56 is unrelated: #3210432: [random test failure] LayoutBuilderQuickEditTest::testQuickEditIgnoresDuplicateFields() Also, it passed on the second try.
Comment #58
catchI need to catch up with the latest changes here, but it would be good to summarise if/how it affects #2828136: A single uncacheable block can make the entire website uncacheable.
Comment #59
borisson_#58: I have no idea how this affects #2828136: A single uncacheable block can make the entire website uncacheable. It seems like probably does not make a difference, since nothing changes in the cacheability propagation of the blocks themselves? I'm not sure how to answer this question @catch.
I think this is biggest nitpick I've ever given on a patch review, so I'm sorry but I think the block group is not right here, while this does use blocks, I think it tests the render or theme systems more specifically. Looks like @group Theme and @group Render both also exist already.
I think render makes more sense here.
Feel free to disagree though
I also created a CR.
Comment #60
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #61
idebr commentedReroll for 10.1.x
#59 still needs to be addressed.
Comment #62
borisson_Needs work for the nitpick in #59
Comment #64
tyler36 commentedExactly what I was was looking for!
Confirmed working on Drupal
10.1.3on demo site.I'm attaching the results of a test output similar in scope to #43.
The zipfile contains HTML output of pages with Twig and cache debugging on so you can see the template, caching and rendering times.
## Test
1. Installed Drupal 10.1.3 using Umami profile
2. Cleared cache
3. Used wget to get static version of site (see UmamiPages.zip|pre-index.html)
4. Cleared cache
5. Applied patch #61
6. Cleared cache
7. Used wget to get static version of site (see UmamiPages.zip|post-index.html)
8. Used wget to get static version of site (see UmamiPages.zip|post2-index.html)
You can see the cache hashes and render time change between 3 and 7.
Steps 7 & 8 produced identical pages (no cache clear between).
Comment #65
watergate commentedThanks a lot for the patch(es); this functionality was/is missing. I can confirm that everything works perfectly on Drupal 10.1.6.
I agree with the nitpick in #3178202-59: Render blocks later, so they can be placed individually in a region template and think that
@group Rendermakes the most sense (and is in line with the rest of the test classes in the namespace). I've updated the patch with the small change :)Comment #66
mollux commentedpatch in #65 works as expected, I'm able to prevent certain blocks in a region to be rendered
Comment #67
giuseppe87 commentedDoes the #65 requires some manual action?
If I install it, every
region.html.twigin the site has thecontentvariable valued as an emtpy string, killing pretty much the render of the site.The site I've tried is multilanguage and with a good deal of patches\contrib modules, I don't know if something of that is liable.
I'll try as soon as possible on a clean install to see if I can replicate.
Comment #68
giuseppe87 commentedComment #69
watergate commentedAfter applying the patch, everything should work as before. Using
{{ content }}should still produce the same output as before. The extra functionality of the patch is that you can output specific blocks like{{ content.page_title }}and{{ content|without('page_title') }}.Please let us know if you can reproduce the problem using a clean install.
Comment #70
giuseppe87 commentedI've tried with a D10 clean install and the patch doesn't cause the same problem.
I've identify that the bug I'm facing appears when using the Bootstrap theme.
Being this a core issue, I guess the theme needs to solve the incompatibility, after this patch is merged.
Comment #71
borisson_I'll set this back to rtbc, after #68/#70 tested manually that with just core, this patches fixes the issue.
Comment #72
catchChanging render arrays in a minor release is OK, but that's quite major breakage of the bootstrap theme - I think we need to figure out why it's not rendering anything after the change, and either look at whether there's a way to keep it working, or open an issue against bootstrap with how it should change (which will need to support versions of core before and after this change). That information should also go into the change record in case other themes have the same problem.
Moving back to needs review for that.
Comment #73
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue.
While you are making the above changes, we recommend that you convert this patch to a merge request. Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)
Comment #74
luismagr commentedHi all,
I'm running 10.2.5 and I can confirm it works as expected. I'm building a new theme started with the starterkit and this is working perfectly fine.
I'm not putting this issue back to 'Reviewed & tested by the community' because I can't confirm if it's working or not in a boostrap theme.
Thanks all for your work on this issue
Comment #75
tisteegz commentedPatch #65 is working fine with Drupal 10.5.2 using a custom theme based on UIkit.
Comment #79
gcbHere's a patch version of the latest MR.