Resolving conflicts with #theme and #theme_wrappers in Drupal 8
Resolving conflicts with #theme and #theme_wrappers in Drupal 8Sunday, 11th Aug 2013
As part of the work in Drupal 8 trying to refine the way that we use the theme and rendering system we discovered that there was a bit of a conflict between #theme
and #theme_wrappers
in drupal_render()
.
We resolved it by adding a new, alternate syntax for #theme_wrappers
that I hope you'll like, but before I show you that I'll explain what's going on here.
theme() is deprecated, stop using it!
Ok, so there's a good chance that you don't realise this, but theme()
is actually deprecated. We're still working to remove it from Core, but drupal_render() works in D7 so you should get into the habit of avoiding direct calls to theme()
sooner rather than later. If you've been using theme()
everywhere but haven't looked into using render arrays, now is the time to learn!
For a direct conversion from a theme()
call to a renderable array, it's very simple.
To convert this:
$output = theme('foo', array('var' => 'bar'));
We do this:
$foo = array(
'#theme' => 'foo',
'#var' => 'bar',
);
$output = drupal_render($foo);
Easy, right?
An important part of this conversion is splitting out building a representation of our final markup as an array and creating a string into two steps. This is not optional, you'll get fatal errors if you try this:
// Do not try this!
$output = drupal_render(array('#theme' => 'foo', '#var' => 'bar'));
When we do that, we no longer need to "flatten" all our data into strings as soon as we start working with it; we have a nice little packet of data we can pass around and manipulate within the system and only render it at the last minute.
If you've worked with the Form API before, this should all seem very familiar as drupal_render()
is the same function that powers the rendering of form elements under the hood.
Nesting renderable data
So, one other thing we gain by creating structured data is the ability to have "child elements". (Unfortunately?) child elements aren't supported for all #theme
hooks, so you have to do some research before you use them but where they are supported they look like this:
$foo = array(
'#theme' => 'foo',
'#someattribute' => 'bar',
'some child' => array(
'#theme' => 'baz',
),
);
Any key in a render element that doesn't start with a # will be treated as a "child element", which is itself a renderable array.
This is great for building structure "on the fly" but what if you want to create a #type
(a re-usable set of defaults to base render elements off) with nested structure? The problem is, while the allowable names of #-prefixed "attributes" of an element are defined by the #type
/#theme
, the child element could be named anything. We don't really want to define element types that rely on arbitrary, yet predefined names for child elements.
We have a tool to help with this, in the form of an array of #theme_wrappers
hooks. Theme hooks that are intended for use with #theme_wrappers
expect that the element has already been rendered into a special attribute called #children
- again, you have to do some research to figure out which hooks I'm talking about as it's not obvious just by looking at them.
'container' is one example of a theme wrapper, it "wraps" a HTML div
around whatever is rendered by the element, using the #attributes
render element attribute to add HTML attributes to the div
. For example:
$foo = array(
'#markup' => 'foo',
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => 'bar'),
);
$output = drupal_render($foo);
Renders: <div class="bar">foo</div>
This is quite a bit neater than using #prefix
/#suffix
to force wrapper divs into place throughout your render arrays. I used #markup
in the example, but #theme_wrappers
used like this is compatible with every #theme
and #type
as well, except for one thing...
Render element attribute conflicts
There's a reason that I chose #markup
in the previous example - #markup
doesn't use any render element attributes so there would be no chance of attributes conflicting with a #theme_wrappers
hook.
Let's look at our previous example again if I want to use #type 'link'
with a HTML ID of bar instead:
$foo = array(
'#type' => 'link',
'#attributes' => array('id' => 'bar'),
'#title' => t('My link'),
'#href' => 'http://drupal.org',
'#theme_wrappers' => array('container'), // Um... where do I set the #attributes for my container?
);
$output = drupal_render($foo);
Renders: <div id="bar"><a href="http://drupal.org" id="bar">My link</a></div>
Yeah... this doesn't work. As #attributes
is not a render attribute name unique to 'container', it is used by both the a
tag and the div
which is clearly not what we wanted.
We can go back to nesting our render elements to avoid this, but as I mentioned above, this causes problems for creating re-usable #type
definitions for elements wanting to leverage #theme_wrappers
.
The "advanced" #theme_wrappers syntax
We built a new syntax into Drupal 8 that allows developers to explicitly set where their render attributes will be interpreted. We actually tried a few different ideas, kudos to @Fabianx for proposing what ended up being adopted.
If any of the values in the #theme_wrappers
is an array itself, each of the #-prefixed attributes in this array will override the same attribute if it is set in the render element only for that theme hook. In this case the key for the array will be used as the wrapper theme hook.
Here's an example that shows how to make a link with a class of foo and a wrapper div
with an ID of bar using the new syntax:
$foo = array(
'#type' => 'link',
'#attributes' => array('class' => 'foo'),
'#title' => t('My link'),
'#href' => 'http://drupal.org',
'#theme_wrappers' => array(
'container' => array(
'#attributes' => array('id' => 'bar'),
),
),
);
$output = drupal_render($foo);
Renders: <div id="bar"><a href="http://drupal.org" class="foo">My link</a></div>
You can mix the "normal" and "advanced" syntax in the same #theme_wrappers array, and you only have to override the exact attributes you need to (no need to declare everything twice if there's no conflict) so this should hopefully be very intuitive to use in practise.
So, that's it. Not as impressive and sweeping as other changes in Drupal 8 but I hope that someone finds it useful someday after Drupal 8 launches :)
Syndicate: planet drupal