Right now we have two broad types of theme hooks used by drupal_render(), those used for #theme and those used in #theme_wrappers arrays.
The former I think most people are comfortable with:
- Define a #theme hook for your render element, or have one set as a default by your #type
- Set # prefixed variables in your render element
- The theme hook maps to a function or Twig template that is responsible for rendering any child elements and variables of this element to return rendered markup
The latter is less common, but while not rare I think it's less understood by the community:
- Define an array of #theme_wrappers hooks, or have one set as a default by your #type
- Set #prefixed variables in your render element
- The theme hooks in the array are called sequentially after the main render phase and *require that any child elements have already been rendered into #children* by either #theme or a recursive drupal_render() call so they can "wrap" #children in more markup
So, the intention is that a single element can have both #theme and #theme_wrappers set, the #theme hook is responsible for rendering element children and the #theme_wrappers hook is responsible for wrapping the rendered element children in extra markup.
One problem is that #theme_wrappers and #theme share a common pool of # prefixed attributes meaning that in practise we often have to awkwardly nest elements so that #theme_wrappers sits in a parent element and #theme sits in a child element. At this point it would be simpler to do away with #theme_wrappers altogether and simply use nested #theme hooks or #type 'html_tag'.
AFAICS, the only way to ensure that #theme_wrappers are useful as a generic way to consistently wrap *any* #theme hook in core is to ensure that they have unique variable names that won't conflict with the #theme hooks they wrap.
#theme hooks can have duplicate variable names as you can only have one per element, but #theme_wrappers can be on the same element as #theme and can be on the same element as other #theme_wrappers.
Let's look at a simple example, you want to wrap an image with an id attribute in a div with a class. To do this we want to merge the following two elements:
#theme 'image':
array(
'#theme' => 'image',
'#attributes' => array('id' => 'foo'),
'#uri' => 'bar',
);
#type 'container':
array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => 'baz'),
);
Because both render elements use #attributes and we don't want duplicate markup, the only way to safely merge them is to nest them:
array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => 'baz'),
'image' => array(
'#theme' => 'image',
'#attributes' => array('id' => 'foo'),
'#uri' => 'bar',
),
);
At which point you might as well throw away #theme_wrappers and just define a #theme => 'container' element which would work 100% of the time and simplify the API at the same time.
Proposed solution
We don't want to break the API at this point, so we can't modify the variable names for existing #theme_wrappers and that would end up being a rather fragile approach in the long run anyway as we'll just end up with a gigantic list of awkwardly named render array attributes that are inconsistent and impossible to memorise, see #2024743: Document "reserved variables" for renderables to avoid conflicts with drupal_render(), the FAPI, and _theme() for the tip of what that iceberg could look like.
What we can do is provide an alternate syntax for defining #theme_wrappers that makes attributes specific to each theme hook explicitly associated with that item in the #theme_wrappers array.
For example the last render element above could (should?) look like:
array(
'#theme' => 'image',
'#theme_wrappers' => array(
'container' => array(
'#attributes' => array('class' => 'baz'),
),
),
'#attributes' => array('id' => 'foo'),
'#uri' => 'bar',
);
This new syntax is entirely optional so there is no API breakage and the following will still work:
array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => 'foo'),
);
The guiding principle here is that each #theme_wrappers hook will use all the render elements "base" attributes as it always has, except in the case where an override is available for a specific attribute/hook combination and then only that attribute will be "overwritten" for that one hook when theme() is called internally.
Alternate proposed solution
Remove #theme_wrappers and use nested #theme hooks for everything, it would mean more typing and more DOOM in our arrays but less bugs and conflicts overall.
Comment | File | Size | Author |
---|---|---|---|
#31 | drupal-theme-hooks-used-2042285-31.patch | 6.31 KB | markhalliwell |
#31 | interdiff.txt | 1.46 KB | markhalliwell |
#26 | 2042285-22-26-interdiff.txt | 1.4 KB | thedavidmeister |
#26 | 2042285-26.patch | 5.77 KB | thedavidmeister |
#22 | 2042285-22.patch | 5.76 KB | thedavidmeister |
Comments
Comment #1
jhodgdonDavid: You make an excellent point, that the standard properties like #attributes etc. should probably be specific to either #theme or #theme_wrapper.
But I looked through core, and currently it looks like theme_wrappers is being used a LOT in core as it is now (more than 110 lines have theme_wrapper in them) so I wonder if this is a real problem most of the time? It would require a lot of work to make this change, since all of these theme hooks would probably need to have their hook_theme(), theme templates, preprocess functions, and calls change. Right?
Comment #2
thedavidmeister CreditAttribution: thedavidmeister commentedI was going to have a look to see if something could be done inside drupal_render() so that we don't have to do much work as theme() wouldn't know the difference.
could be like:
because theme() doesn't modify anything in $elements by reference, there's probably no harm in sending it a fake array.
Comment #3
jhodgdonBut all of the existing twig templates for the theme wrappers would have to be updated to use the new variables too, right? I just don't think this is all that likely to happen at this stage in D8, and I'm not sure it's such a huge problem in the first place, since there are all kinds of places in core using theme_wrappers now without (as far as I can tell) any problems.
I guess the question is whether the use case in the issue proposal is a common need and whether it should be taken care of with theme wrappers, or whether it's an edge case that you have a viable work-around for... it seems like Core doesn't need this to change, so if lots of contrib does, it would still be valuable to make the change; if not, then leaving things as they are seems like the better course of action.
Comment #4
thedavidmeister CreditAttribution: thedavidmeister commentedHave a look at the awkward patches being posted/committed under #1595614: [meta] Remove all the theme functions and templates in core that simply output a link. Replace with #type 'link' render arrays that either nest #theme inside #theme_wrappers elements or even early-render elements directly into #children!
Also, as I mentioned in #2031301: Replace theme_more_link() and replace with #type 'more_link', the current setup precludes defining #type 'more_link' (or similar) to setup default render elements for commonly re-used link markup throughout core.
No, what I put up in #2 means that the theme functions would see exactly what they would have seen before.
For example if #2 was implemented, if you had
Then theme('image') would "see" these variables:
and theme('container') would "see" these variables:
Because #container_attributes would be temporarily copied over the top of #attributes before theme('container') is called from within drupal_render(). This would happen without needing to define 'container_attributes' in hook_theme() for 'container' because it is detected dynamically based on a naming convention.
AFAICS this wouldn't impact the operation of anything past the point theme() is called, unless I'm missing something obvious.
Comment #5
thedavidmeister CreditAttribution: thedavidmeister commentedSomething like this.
Comment #7
jhodgdonWell, if you can get the tests to pass... :) Obviously, some core code is depending on the current behavior, and you'll need to do more than just this change.
Comment #8
thedavidmeister CreditAttribution: thedavidmeister commentedthat would be 'form' overriding 'id' when 'form_id' is set >.<
The most obvious solution would just be to change the proposed convention to something that is more obscure.
Comment #9
thedavidmeister CreditAttribution: thedavidmeister commentedpatch just updates the convention to something safer:
Would be the new format.
Comment #10
jenlamptonWhat about just calling the variables'#wrapper_attributes'
or whatever, and passing them up to the theme wrapper? Its not like a theme function can have more than one theme wrapper and this would more easily allow a change to the wrapper.Edit: Oh. multiple wrappers. Lame. Nevermind.
Also, I really want to see theme wrappers die in D9. Having different ways to do the same thing is really frustrating. I propose we create a follow-up for D9 that proposes the alternate solution from this issue.
Comment #11
thedavidmeister CreditAttribution: thedavidmeister commented#10, yeah, multiple theme wrappers per element are a thing.
At first I just wanted to kill theme wrappers too, but then I realised that if we keep them we can do cute things like this inside hook_element_info():
The problem is that #theme_wrappers are useful for setting up default structures in #type that move beyond a single #theme or #pre_render callback. You can't put nested children in a #type default because there's no standard way to name child elements.
Comment #12
jenlamptonHere's a sample of the code for forum module's more help link:
The previous patch doesn't work with the code above. Maybe I am missing something, but it's looking for '#attributes' on the parent, and after the foreach what we have instead is '#wrapper_container_wrapper_container_attributes'.
I think we need to subtract the additional pieces, instead of add. Taking another stab at it.
Comment #13
thedavidmeister CreditAttribution: thedavidmeister commentedI didn't think of the use-case in #12. nice work :)
Could we get an extra test in with that example?
Comment #14
thedavidmeister CreditAttribution: thedavidmeister commentedUpdated test as per #13
Comment #15
jhodgdonThis new behavior will need to be documented somewhere, and I don't see any API docs changes in this patch. I am not sure where to document it, but probably in drupal_render() docs?
Comment #16
thedavidmeister CreditAttribution: thedavidmeister commentedi'll have a look at the docs. Don't forget to set to needs work if we need doc updates :)
Comment #17
thedavidmeister CreditAttribution: thedavidmeister commentedhere we go, added docs.
Comment #18
Fabianx CreditAttribution: Fabianx commentedI don't know how much of drillability will be happening, but I encourage us to at least think about how theme_wrappers or this extended theme_wrappers would work with it.
My biggest problem with theme_wrappers is that they work on "raw HTML", while all they do in essence is setting / or extending a prefix and a suffix.
Though I could of course do something like:
So in general maybe even old style theme_wrappers would be compatible with drillability, where the theme wrappers would be available within for example:
{{ title }}
vs.
just thinking out loud. On the other hand, the h2 might as well have been the wrapper here.
But the concept of containers is especially important for our dreammarkup for node (#2004252: node.html.twig template) and we identified it as a key concept.
Maybe we can come up with something even better than theme_wrappers as part of this?
Thanks!
Comment #19
thedavidmeister CreditAttribution: thedavidmeister commentedIf #2044105: Introduce #alters for \Drupal\Core\Render\Renderer::render() happens then I'm sure a lot of the single tag wrappers could be turned into #render callbacks (as #render is modelled off #theme_wrappers anyway) which would make them simpler and faster.
It's a little dangerous to be thinking of #theme_wrappers 'container' as the only thing we have to be considering in the discussion about "wrappers". For example, from memory I believe the overlay module uses #theme_wrappers array('overlay', 'html') to wrap #theme 'page', which is quite a bit more complex than a single h2 or div. I don't know to what extent we'd have to re-write the overlay module to convert this pattern into nested #theme hooks rather than wrappers, it's something we'd have to investigate.
For now, just assume that simply swapping #theme_wrappers out for a bunch of "one tag" 'container' theme functions is not possible and the full conversion of #theme_wrappers in core to #theme would be "non-trivial".
Correct me if I'm wrong, but given where we are in the D8 release cycle I think we actually aren't allowed to remove #theme_wrappers until D9 at the earliest, as that would represent massive API breakage.
We should decide a desired course of action or we'll lose our window to make this immediately actionable (if there even still is one :P):
A. Do nothing now, push this issue to D9. We have to be content with early-rendering arrays into #children where wrappers and hooks conflict.
B. Commit the extended form of #theme_wrappers in #17 and forge forward with this idea, exploring it as a new paradigm for avoiding early-render. We'd also want to then incorporate this pattern into #2044105: Introduce #alters for \Drupal\Core\Render\Renderer::render()
C. Commit the extended form of #theme_wrappers in #17 as a stop-gap for D8, allowing us to clean out early-render but file a followup D9 ticket to remove #theme_wrappers. We'd then want to disallow multiple #render callbacks (it's currently an array) and allow only one #render per-element like how #theme works #2044105: Introduce #alters for \Drupal\Core\Render\Renderer::render(), otherwise we'll just repeat all the same mistakes with render callbacks in the future.
D. ???
I don't have a strong preference for B or C but I'm not a huge fan of A because I find early-rendering anything into #children just to support trivially simple configurations like a link in a div kind of depressing >.<
The problem with the drillability discussions in the issue queue so far (that I've seen) is that they are very "early days", high level, very theme()-centric and focus on what we want but not so much on actionable issues that can be followed up or referenced in discussions like this one. There's no instructions or consensus as to what needs to be preserved/exposed/removed/extended at #2008450: Provide for a drillable variable structure in Twig templates to "allow drillability" in any of #cache, #printed, #access, #pre_render, #theme_wrappers, #markup, #post_render.
AFAIK, all we have so far to guide our progress towards drillability is:
- There's a single "proof of concept" patch that attempts to handle #theme correctly but the consequences of defining any of the other standard attributes in drupal_render() on an element are ignored.
- We have a broad consensus that to make something drillable we have to split rendering of elements and *parts* of elements into at least two phases "prepare" and "make string", with the implication that all the steps to "prepare" can be identified reliably and accessed publically and "make string" can either be inferred once the variable is prepared or explicit instructions as to how an element should be "made into a string" can be found on the element somewhere.
This says nothing about #theme_wrappers. It's not even clear that themers "drilling" *want* theme wrappers to be rendered around the "parts" of the element that they're drilling into. If we think back to Jacine's blog post in 2012 about 'theme components' referenced in #1804488: [meta] Introduce a Theme Component Library, one of the key concepts outlined there is that with a two-tier structure for a theme system (she calls it "container" and "item", but it's roughly translatable to "#theme" and "#theme_wrappers" IIRC) at least one serious themer specifically doesn't/didn't want the wrapper to be rendered for elements when being drilled (items), only when the element is being rendered in its entirety.
I did actually bring up the issue of #theme_wrappers with the current proposals for drillability at https://drupal.org/node/2008450#comment-7602499 but haven't heard anything back yet :( It's very unclear to me what the intention is for #theme_wrappers and drilling and I think any further discussion on the topic should live in that issue.
Sooo, in the interests of not de-railing this issue and keeping drillability discussion somewhere with high visibility, can we agree that this issue can propose removing #theme_wrappers only if we can't resolve the issue of conflicting variable names with other #theme_wrappers hooks or #theme hooks and #2008450: Provide for a drillable variable structure in Twig templates can propose remove #theme_wrappers only if they're deemed fundamentally outside what can be supported at the same time as Twig drillability (we can also decide if that would actually be a good enough reason to remove #theme_wrappers over there).
Comment #20
Fabianx CreditAttribution: Fabianx commented#19: Thanks for the insightful reply.
I have taken a pre-liminary decision on that and that is to not support #theme_wrappers for drillability ( I really just wanted to quickly think about it, not de-rail).
I think if you go to the lengths of implementing essentially a "sub-template" then #theme_wrappers and changing structure is rather a problem, than a solution, so #theme_wrappers or #render (multiple) is in that respect something I even _want_:
Consider:
I would need to drill down into:
which is like WTF.
Now something else "wraps" this and I end up with:
which is even more unreliable.
In that respect I am _for_ #theme_wrappers.
Drillable theme_wrappers can be supported like:
which would just be a mix of drupal_render(), pre-paring $variables, internal output and then wrapping the output with the #theme_wrappers!
However not terribly difficult:
I'll continue further discussions on the drillability issue.
Long story short:
Nothing to be done here for drillability, but #theme_wrappers is a good concept in itself and should not be removed!
Any new #theme_wrappers could be:
a) left as is (works fine with the little work-around as seen above)
b) split into adding to #prefix and #suffix instead and maybe using an API for that (maybe too much for this issue)
Despite all this what I would like to "needs work" here is the same as for #render proposal:
I don't like the long variable names and I propose to extend #theme_wrappers in the following way instead:
That would be pretty much the same syntax, but would just extend #theme_wrappers in a way to allow to override any attributes needed.
And if we wanted we could add some special attributes as well just on the #theme_wrappers added like for example:
#inherit => FALSE, // Only use defined vars
#inherit => array('var1', 'var2'); // Only inherit specified vars
Comment #21
Fabianx CreditAttribution: Fabianx commentedCNW per #20.
Comment #22
thedavidmeister CreditAttribution: thedavidmeister commentedHere's a patch that implements the alternate syntax in #20 sans-#inherit, which I personally believe is overkill at this point and is really blurring the lines between #theme, #theme_wrappers and child elements (not in a good way).
One bonus of the syntax in #20 is that it is compatible with multiple instances of the same #theme_wrappers hook, eg:
I added a test for that, although I don't know how much mileage this use-case will get in the wild it is still a nice clean-up.
I also fleshed out the docs a bit as the new syntax is more confusing even if it is more concise.
Comment #22.0
thedavidmeister CreditAttribution: thedavidmeister commentedUpdated issue summary.
Comment #22.1
thedavidmeister CreditAttribution: thedavidmeister commentedupdated issue summary to match latest patch
Comment #23
thedavidmeister CreditAttribution: thedavidmeister commentedComment #24
Fabianx CreditAttribution: Fabianx commentedI reviewed this, it passes tests, it unblocks cleanup to not drupal_render things early and makes containers much more useful at the same time. (for #type link for example)
It is BC compatible!
Clear yes! => RTBC
Comment #25
jhodgdonThere are some blank lines in the documentation that probably need to be eliminated, and some bad indentation:
That blank line needs to go away (it will make a paragraph break where none is wanted) and the indentation is incorrect after the blank line.
I very much like the documentation though: it's clear. Always a plus.
Comment #26
thedavidmeister CreditAttribution: thedavidmeister commented/sigh. I don't know why I always seem to get the docs wrong somehow :/
Comment #28
thedavidmeister CreditAttribution: thedavidmeister commented#26: 2042285-26.patch queued for re-testing.
Comment #29
Fabianx CreditAttribution: Fabianx commentedFeedback addressed, back to RTBC!
Comment #30
jhodgdonThere still shouldn't be a blank line before the next list item in the documentation:
Comment #31
markhalliwellFixed indention and extra line doc issues. Looks good to me now.
Comment #32
Fabianx CreditAttribution: Fabianx commentedBack to RTBC (once green)
Comment #33
jenlamptonTagging, for more eyes :)
Comment #34
xjm#31: drupal-theme-hooks-used-2042285-31.patch queued for re-testing.
Comment #36
thedavidmeister CreditAttribution: thedavidmeister commented#31: drupal-theme-hooks-used-2042285-31.patch queued for re-testing.
Comment #37
Fabianx CreditAttribution: Fabianx commentedBack to RTBC (after testbot fluke)
Comment #38
alexpottThis is only an API addition and not a change. I've tested the view_ui which makes extensive use of #theme_wrappers and it looks good. I've also confirmed with @thedavidmeister that backwards compatibility is being tested. Updated the title to reflect the actual nature of the change.
Committed dc5e334 and pushed to 8.x. Thanks!
Comment #39
catchComment #40
jenlamptonI just wrote my first Change Notice :) [#2066209]
edit: https://drupal.org/node/2066209
Does someone want to review it for me and let me know if I got it right?
Comment #41
thedavidmeister CreditAttribution: thedavidmeister commentedlooks good to me :) although, maybe we should mention the new syntax is optional and you can just use strings as values for #theme_wrappers if you don't need the extra attributes flexibility.
I actually wrote a blog post on this as well, but it didn't come through Planet Drupal for some reason :(
Comment #42
markhalliwellLooks good! Short, sweet and to the point. I was able to make sense of what was changed. Thanks @jenlampton :)
Comment #43
thedavidmeister CreditAttribution: thedavidmeister commentedopened #2068217: array('hook') syntax for suggestions is broken for #theme_wrappers
Comment #45
xjmUntagging. Please remove the tag when the change notification task is completed.
Comment #45.0
xjmUpdated issue summary.
Comment #46
sun