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.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jhodgdon’s picture

David: 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?

thedavidmeister’s picture

I 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.

  if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
    foreach ($elements['#theme_wrappers'] as $theme_wrapper) {
      $elements['#children'] = theme($theme_wrapper, $elements);
    }
  }

could be like:

  if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
    foreach ($elements['#theme_wrappers'] as $theme_wrapper) {
      $overrides = array();
      foreach ($elements as $key => $value) {
        if($key[0] === '#' && isset($elements['#' . $theme_wrapper . '_' . substr($key, 1)])) {
          $overrides[$key] = $elements['#' . $theme_wrapper . '_' . substr($key, 1)];
        }
        else {
          $overrides[$key] = $value;
        }
      }
      $elements['#children'] = theme($theme_wrapper, $overrides);
    }
  }

because theme() doesn't modify anything in $elements by reference, there's probably no harm in sending it a fake array.

jhodgdon’s picture

But 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.

thedavidmeister’s picture

it seems like Core doesn't need this to change,

Have 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.

But all of the existing twig templates for the theme wrappers would have to be updated to use the new variables too, right?

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

array(
  '#theme' => 'image',
  '#theme_wrappers' => array('container'),
  '#container_attributes' => array('class' => 'baz'),
  '#attributes' => array('id' => 'foo'),
  '#uri' => 'bar',
);

Then theme('image') would "see" these variables:

array(
  'attributes' => array('id' => 'foo'),
  'uri' => 'bar',
);

and theme('container') would "see" these variables:

array(
  'attributes' => array('class' => 'baz'),
);

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.

thedavidmeister’s picture

Status: Active » Needs review
FileSize
2.95 KB

Something like this.

Status: Needs review » Needs work

The last submitted patch, 2042285-5.patch, failed testing.

jhodgdon’s picture

Well, 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.

thedavidmeister’s picture

that 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.

thedavidmeister’s picture

Status: Needs work » Needs review
FileSize
2.99 KB
2.09 KB

patch just updates the convention to something safer:

array(
  '#theme' => 'image',
  '#theme_wrappers' => array('container'),
  '#wrapper_container_attributes' => array('class' => 'baz'),
  '#attributes' => array('id' => 'foo'),
  '#uri' => 'bar',
);

Would be the new format.

jenlampton’s picture

Issue tags: +theme system cleanup

What 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.

thedavidmeister’s picture

#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():

$types['more_link'] = $types['link'];
$types['more_link'] += array(
  '#title' => t('More'),
  '#attributes' => array(
    '#title' => t('Show more content'),
  ),
  '#theme_wrappers' => array('container'),
  '#wrapper_container_attributes' => array(
    'class' => array('more-link'),
  )
);

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.

jenlampton’s picture

FileSize
1.25 KB
3.02 KB

Here's a sample of the code for forum module's more help link:

      $more_help_link = array(
        '#type' => 'link',
        '#href' => 'admin/help/forum',
        '#title' => t('More help'),
        '#theme_wrappers' => array('container'),
        '#wrapper_container_attributes' => array(
          'class' => array('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.

thedavidmeister’s picture

Status: Needs review » Needs work

I didn't think of the use-case in #12. nice work :)

Could we get an extra test in with that example?

thedavidmeister’s picture

Status: Needs work » Needs review
FileSize
3.61 KB
3.48 KB

Updated test as per #13

jhodgdon’s picture

This 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?

thedavidmeister’s picture

Status: Needs review » Needs work

i'll have a look at the docs. Don't forget to set to needs work if we need doc updates :)

thedavidmeister’s picture

Status: Needs work » Needs review
FileSize
1.12 KB
4.65 KB

here we go, added docs.

Fabianx’s picture

I 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:

$output = array(
  '#theme_wrappers' => 'abc',
  '#children' => 'MAGIC_STRING-uniq'
);

$x = explode('MAGIC_STRING-uniq', $output);

$render['#prefix'] .= $x[0];
$render['#suffix'] .= $x[1];

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.

{{ title.prefix }}
  {% if not page %}
    <h2{{ title.attributes }}><a href="{{ url }}" rel="bookmark">{{ title.label }}</a></h2>
  {% endif %}
  {{ title.suffix }}

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!

thedavidmeister’s picture

If #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).

Fabianx’s picture

#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:

$wrapped_link = array(
  '#theme' => 'container',
  child => array(
    '#type' => 'link',
    '#path' => 'foo',
  ),
);

I would need to drill down into:

link.child.attributes.href

which is like WTF.

Now something else "wraps" this and I end up with:

link.container1.child.attributes.href

which is even more unreliable.

In that respect I am _for_ #theme_wrappers.

Drillable theme_wrappers can be supported like:

{% drupal_render %}
<a href="{{ link.attributes.href }}">More Content!</a>
{% end drupal_render %}

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:

$render_array['#children'] = 'render-[unique_id]';
unset($render_array('#theme'));
$output = explode('render-[unique_id]', drupal_render($render_array));

echo $output[0];

$variables = get_variables_from_render_array($render_array);
$variables['parent'] = $context;
$context = $variables;

echo '<a href="';
echo '' . /* ... */;
echo '>More Content!</a>';

echo $output[1];

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:

array(
  #theme_wrappers => array(
    'container' => array(
      '#attributes' => array(
        'class' => 'content',
      ),
    ),
    'another_wrapper',
    'and_another_one'
  ),
  '#attributes' => array(
    'id' => 'x',
  ),
);

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

Fabianx’s picture

Status: Needs review » Needs work

CNW per #20.

thedavidmeister’s picture

Status: Needs work » Needs review
FileSize
5.76 KB

Here'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:

array(
  '#attributes' => array('class' => 'foo'),
  '#theme_wrappers' => array(
    'container' => array(
      '#attributes' => array('class' => 'bar'),
    ),
  'container',
  ),
);

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.

thedavidmeister’s picture

Issue summary: View changes

Updated issue summary.

thedavidmeister’s picture

Issue summary: View changes

updated issue summary to match latest patch

thedavidmeister’s picture

Title: theme hooks used as #theme_wrappers should have unique variable names or be converted to normal #theme hooks » theme hooks used as #theme_wrappers should be able to be provided with a different set of variables to #theme to avoid conflicts
Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

I 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

jhodgdon’s picture

Status: Reviewed & tested by the community » Needs work

There are some blank lines in the documentation that probably need to be eliminated, and some bad indentation:

+ *       '#theme' => 'image',
+ *       '#attributes' => array('class' => 'foo'),
+ *       '#theme_wrappers' => array('container'),
+*      );
+*      @endcode
+*
+*      and we need to pass the class 'bar' as an attribute for 'container', we
+*      can rewrite our element thus:
+*      @code
+*      array(

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.

thedavidmeister’s picture

Status: Needs work » Needs review
FileSize
5.77 KB
1.4 KB

/sigh. I don't know why I always seem to get the docs wrong somehow :/

Status: Needs review » Needs work
Issue tags: -theme system cleanup

The last submitted patch, 2042285-26.patch, failed testing.

thedavidmeister’s picture

Status: Needs work » Needs review
Issue tags: +theme system cleanup

#26: 2042285-26.patch queued for re-testing.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Feedback addressed, back to RTBC!

jhodgdon’s picture

Status: Reviewed & tested by the community » Needs work

There still shouldn't be a blank line before the next list item in the documentation:

+ *      @endcode
+ *
  *   - If this element has an array of #post_render functions defined, they are
markhalliwell’s picture

Status: Needs work » Needs review
FileSize
1.46 KB
6.31 KB

Fixed indention and extra line doc issues. Looks good to me now.

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC (once green)

jenlampton’s picture

Title: theme hooks used as #theme_wrappers should be able to be provided with a different set of variables to #theme to avoid conflicts » theme hooks used as #theme_wrappers need a different set of variables for #theme, to avoid conflicts
Issue tags: +Twig

Tagging, for more eyes :)

xjm’s picture

Status: Reviewed & tested by the community » Needs work

The last submitted patch, drupal-theme-hooks-used-2042285-31.patch, failed testing.

thedavidmeister’s picture

Status: Needs work » Needs review
Issue tags: +Twig, +theme system cleanup
Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC (after testbot fluke)

alexpott’s picture

Title: theme hooks used as #theme_wrappers need a different set of variables for #theme, to avoid conflicts » [Change notice] #theme_wrappers should be able to have a unique set of variables per wrapper hook
Priority: Normal » Critical
Status: Reviewed & tested by the community » Active
Issue tags: +Needs change record

This 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!

catch’s picture

Category: bug » task
jenlampton’s picture

Status: Active » Needs review

I 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?

thedavidmeister’s picture

Status: Needs review » Needs work

looks 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 :(

markhalliwell’s picture

Title: [Change notice] #theme_wrappers should be able to have a unique set of variables per wrapper hook » #theme_wrappers should be able to have a unique set of variables per wrapper hook
Priority: Critical » Normal
Status: Needs work » Fixed

Looks good! Short, sweet and to the point. I was able to make sense of what was changed. Thanks @jenlampton :)

thedavidmeister’s picture

Status: Fixed » Closed (fixed)

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

xjm’s picture

Issue tags: -Needs change record

Untagging. Please remove the tag when the change notification task is completed.

xjm’s picture

Issue summary: View changes

Updated issue summary.

sun’s picture