After a lenghty discussion with eaton on IRC:

We need to refactor how drupal_render handles theming. The problem is that
a) We mix logic and presentation a lot with the #type property, as it is the main logical property during form processing, but also controls which theme function is called in drupal_render()
b) The distinction between #theme and #type is everything but obvious (the short form is, theme($element['#theme']) is called to render all the element's children into a single string, while theme($elements['#type']) is called to wrap this "children string" into some more markup (e.g. form tags for the form #type).

[frando_] I have another, simpler idea: 1) #type seperated from everything theming. 2) #theme as major, required property. Has to render all children either directly or by using drupal_render_children, which would simply concatenate them. 3) #wrapper_theme as additional, optional property. Gets called after #theme and has the rendered string that #theme returned (+ the whole element) as argument. If #wrapper_theme is not set, drupal_render just returns the string returned by #theme.
[frando_] This will, I think, cover all situations mentioned here and doesn't need a flag
[eaton] frando_: I do like the idea of #wrapper theme being an alternate callback for specific things that always need to be wrapped in something 'different'
[frando_] Forms would have #theme and #theme_wrapper set, if I want to custom theme a form I can override theme and leave #theme_wrapper alone.
[..]
[eaton] yes, I really REALLY like this.
[eaton] oh man, theme_form_element could be a theme_wrapper function!
[eaton] that would eliminate HUGE amounts of un-alterable code in our forms
[eaton] because right now individual form elements get their own #type based theme function, and then they get wrapped in theme_form_element(), which adds all the crazy divs and such.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Frando’s picture

In proper words, the result of the discussion was that

  • 1) We should completely seperate the #type property from presentation. A type is defined in hook_elements to supply elements with default values and it is the main distinguisher during form processing, but should not at the same time be the name of the element's main theme function.
  • 2) We need the possibility for an element to have two theme functions assigned, one that renders the children and puts them into a single string, and one that wraps something around that string. Classical example is a form with custom theming: The custom theming function renders the children and puts them into a string, while the wrapper wraps the form tags around it.

The proposed properties explained:

#theme and #theme_wrapper. #theme is the major property, while #theme_wrapper is completely optional. The theme function in #theme is responsible for rendering all the element's children and putting them into a single string. If #theme_wrapper is set on an element, it will be called later than #theme with the string that #theme returned as an argument (plus the element itself). The string returned by #theme_wrapper is then returned by drupal_render().

So basically #theme is still nearly the same, while #type in the context of drupal_render is replaced by the #theme_wrapper property.

In hook_elements, types will usually set defaults for one or both of these properties.

Note that this is basically what we do now, but properly separated from form logic and with much better names and less confusion. It will also introduce new theming possibilities as it decouples form rendering from form processing quite a bit more (by making it possible to override #theme_wrapper, which wasn't really possible up to now, as that would have meant changing the #type which would fail form processing).

We can introduce a function drupal_render_children() that can be called by the #theme of an element to simply drupal_render all children and concatenate them into a single string.

We can also make #theme_wrapper an array, so for form elements, theme_form_element would simply be a key in there, making it finally simple to disable or replace theme_form_element for a single form element.

We also don't have to introduce a new flag to do what we want to do in the page rendering patch (which we would have to with the current system).

Frando’s picture

Status: Active » Needs work
FileSize
7.44 KB

So here's a very first stab! Still very rough.
Lots of forms are still broken, but some already work, e.g. the module form or the login form. I did not yet convert any actual elements. But maybe this makes clearer what's the idea behind this.
Dunno if I'll have much time to work on this in the next weeks, though.

Frando’s picture

Status: Needs work » Needs review
FileSize
22.23 KB

Now this one is much better already.

* theme_form_element is now just another entry in the #theme_wrapper array of form elements, which means it's finally simple and easy to disable or change theme_form_element for specific elements via hook_form_alter
* all core elements now have the #theme_wrapper property!

Also, the diffstat says 118 insertions(+), 142 deletions(-), and many of the additions are just comments. So we'll get a much cleaner drupal_render with less code, even!

I clicked around through lots of core forms, and they all seem to work. Haven't checked thoroughly so far, though.
I'm setting to needs review anyway to get review on the concept. If there are still broken forms (which is very likely), they will (hopefully) be easy to fix (mostly replacing a drupal_render by drupal_render_children or similar).

Status: Needs review » Needs work

The last submitted patch failed testing.

catch’s picture

Since drupal_render() took up a lot of time in profiling, I'll try to run some as soon as tests pass to see if we get any different either way. Also, this would be a good place to write some direct unit tests for drupal_render().

Damien Tournoud’s picture

Issue tags: +Needs tests
eaton’s picture

Since drupal_render() took up a lot of time in profiling, I'll try to run some as soon as tests pass to see if we get any different either way. Also, this would be a good place to write some direct unit tests for drupal_render().

For the record, this refactoring also makes drupal_render a lot simpler, in terms of the rules that govern its internal behavior. Tests will be much easier with that in mind.

Frando’s picture

Issue tags: +Rendering
moshe weitzman’s picture

Looks like a great improveemnt to me. I agree with moving _element_info() and removing underscore. Also, I would abbreviate "If set to TRUE" to just "If TRUE" - occurs a few times.

Frando’s picture

Status: Needs work » Needs review
FileSize
34.4 KB

New patch attached! I caught several more forms that do a drupal_render($form) in their theme function and made them all use drupal_render_children instead. With the patch, drupal_render($element) in the $element's #theme function now causes an endless loop. It didn't previously because of some quite weird flag-setting inside of drupal_render. IMO, with the patch it's much much cleaner now - to render the not-yet-printed children of a form in its theme function, simply use drupal_render_children(). This also makes the theme functions more readable, as drupal_render_children just makes more sense.

So a lot more tests should pass now, hopefully, maybe even all.

Also incorporated moshes suggestions in #9 and made the input format fieldset reappear with the patch.

So here's a quick summary of how things are now in drupal_render():

  • #type has nothing to do anymore with theming. #type simply is responsible for loading the right set of default values (including theming related properties).
  • #theme: If this property is set to a theme function, it is called by drupal_render with the element as an argument. If the element is allowed to have children, the theme function is then responsible to render the children, either manually using drupal_render on an individual child, or by calling drupal_render_children, which concatenates all not-yet-rendered children into a string.
  • If #theme is not set, the children are concatenated into a single string using drupal_render_children.
  • For elements that are not allowed to have children, like textfields, #theme is the perfect place to create the element's markup.
  • Now, in addition to #theme there is #theme_wrapper. #theme_wrapper is an array of theme functions. They are called after the children have been rendered, and can therefore wrap more markup around the string returned by #theme (or drupal_render_children). This is used e.g. by the #types form and fieldset to wrap their tags around the rendered children. It is also used for nearly all form elements to wrap the markup of theme_form_element around them. The latter allows finally to override this behaviour in an easy and clean way for individual elements.

Status: Needs review » Needs work

The last submitted patch failed testing.

starbow’s picture

Very interesting.
Do the #theme_wrapper functions have to be theme functions? Could this be used to, say, wrap the html of a page into a JSON wrapper (he asks, trying not to get too excited)?

Frando’s picture

Well, with the implementation in the patch, #theme_wrapper functions are called with theme(). But still, yes, I think in conjunction with the page rendering patch (http://drupal.org/node/351235), this could be used to wrap elements into json. You could then for example implement hook_page_alter, iterate over the contents of the content region, and add appropriate #theme_wrapper functions which then are going to add the required json stuff. And allowing other caller functions in place of theme() would be quite an easy follow up patch I'd guess, e.g. a #theme_wrapper_callback that defaults to 'theme' and then #theme_wrapper would actually be #theme_wrapper_callback_arguments, or something like that. Needs some more thought, of course, and I haven't really spend time figuring this out yet, but this patch, together with the hook_page_alter patch, is definitely a door-opener for a lot more flexibility when it comes to rendering stuff into different output formats IMO.

I don't really believe the bot, btw - installation went smooth for me locally. There are still some test failures, but a *lot* less then previously. Setting to CNR to give the testbot another chance.

Frando’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch failed testing.

Frando’s picture

Status: Needs work » Needs review
FileSize
36.02 KB

* Rerolled for recent changes in HEAD
* Fixed the remaining TODOs I left in the patch
* We used to alter elements in theme functions before passing them to theme_form_element for radios and checkboxes. I moved those checks in a pre_render function to make them compatible with theme_form_element as #theme_wrapper entries.
* Fixed many failures and updated the remaining element definitions.

Now, this one should have 100% passes!

Frando’s picture

Don't really believe the testbot. The fail is in "File naming". Passes for me locally, and really has nothing to do with drupal_render(). Requesting retest.

catch’s picture

One of the servers doesn't like that test, not an issue with this patch.

catch’s picture

FileSize
127.12 KB
123.21 KB

Hmm, initial profiling doesn't look too good - here's time spent in drupal_render and drupal_render_children. Did two requests each and they were about the same each time.

Frando’s picture

Hm. So I just did a round of profiling. /node with 90 nodes, no caching
HEAD:
drupal_render 198 calls, 84 161 incl (6.7%), 28 698 (2.3%) exl
Patch:
drupal_render 198 calls, 104 124 incl (9.17%), 18 997 excl (1.67%)
drupal_render_children 50 calls, 21 406 incl (1.89 %), 5 327 excl (0.47 %)

So no. of calls is identical, little bit more time spend. I fear we're talking about microoptimizations here.. if anybody sees a part in the patch that could have an actual negative influence on drupal_render, please enlighten me (I couldn't really find something but I'll look into it more thorougly tomorrow),

catch’s picture

FileSize
39.51 KB

Well some things are definitely being called more times. I get 20 more calls to array_filter(), which takes the calls to element_child() from 200+ to 680. So it looks like we're doing some processing twice that could possibly be saved in $elements and re-used.

On a minor point I get call time pass by reference notices in the call to drupal_render_children(&$elements) - that's deprecated now. Uploaded a patch with only that one change.

Should say this looks so, so much nicer, but this function is in our top ten slowest functions in HEAD already (both for self and incl. time) so I just want to make sure we catch any gotchas where possible while it's still being reviewed.

Frando’s picture

So here we go, optimizing starts to be fun!

Main point were time is saved now is element_children. Also, we now pass the keys of the element's children to drupal_render_children, which saves another element_children() run.

Profiling screenshots attached. Profiling was done for node/xx/edit.

So now the patch is even faster for drupal_render than HEAD on the node edit page (by quite a lot), while on /node with 90 nodes it's the same time.

catch’s picture

Nice work with the optimisation, not done my own cachegrinds yet to compare but seems like we've got most possible places now.

A few code style things:

There's some trailing whitespace introduced by the patch.

I keep getting told off for putting "don't" and "they've" and other abbreviations in code comments, should probably "do not" and "they have" for clarity.

  +    if (sizeof($cache)) {

This should probably be !empty - also we normally use count() rather than sizeof() where it's necessary but it doesn't seem like it is here. Need to sit down with it a bit more, but all the changes - both inside drupal_render() and where it's been applied elsewhere in core look a lot cleaner to me.

Frando’s picture

Fixed the issues catch raised.
* No more trailing whitespace
* sizeof() changed to !empty() (I didn't introduce that in this patch, but !empty() is still what we use normally), also moved a call do element_basic_defaults so that it doesn't get called each time
* Improved documentation and moved the important parts into the phpdoc block, where we had barely any interesting documentation at all before.

Frando’s picture

FileSize
38.2 KB

Reroll for the hook_page_alter patch (yay!) and the element sorting fix!

Status: Needs review » Needs work

The last submitted patch failed testing.

Frando’s picture

Assigned: Unassigned » Frando
Status: Needs work » Needs review
FileSize
38.76 KB

Fixed the new type #list and also a double label on the body field due to text format processing. Should pass all tests again now.

Status: Needs review » Needs work

The last submitted patch failed testing.

RobLoach’s picture

Subscribing.

Owen Barton’s picture

Subscribing

Frando’s picture

Status: Needs work » Needs review
FileSize
38.79 KB

The tableselect element patch broke this. Reroll attached, should still pass all tests. I think this one is ready to go.

somes’s picture

subs

eaton’s picture

Status: Needs review » Needs work

I really love this refactoring: it's exactly where we should be going with some of our internal worker functions, cleaning out historical cruft and clearing the way for more optimizations.

I think allowing multiple simultaneous #theme_wrapper functions is a mistake, though: right now, we already support theme($foo...) taking an array of theme functions, and the expected behavior is to use the first one with a matching implementation. If someone really needs to wrap an element in multiple wrappers simultaneously, I think nesting an element one-off is a better solution... Other than that, though, I really like it.

chx’s picture

This is good indeed with the minor caveat Eaton mentioned. I would also like to hear more about $force. Or remove it.

babbage’s picture

Subscribing.

Frando’s picture

Status: Needs work » Needs review
FileSize
38.66 KB

I guess I agree with eaton about #theme_wrapper. I first thought it would be needed as an array for form_element theming, but is not. So in the attached patch, #theme_wrapper is simply a string, which adds more simplifications and makes things consistent with #heme.
Also synced with HEAD and improved documentation here and there.

The $force parameter allows to print elements a second time on the same page, which wouldn't be possible otherwise without recursing over all children, as #printed would be set to TRUE not only for the element but also for all its children.

Status: Needs review » Needs work

The last submitted patch failed testing.

Frando’s picture

Status: Needs work » Needs review
FileSize
38.71 KB

This one should be better.

Dries’s picture

I need to look into this more, but at first glance, I think it is a little weird that we have both #theme and #theme_wrapper. Can't we just have #theme and fix up #type differently? I'd prefer a more intuitive API, to be honest.

Frando’s picture

Well, I think we definitely need to have the ability to have two theme functions per element, one for printing out the children and one for adding something around that.

We always had this, and we need this. What now is theme($element['#theme_wrapper']) used to be theme($element['#type']), which was just plain wrong (as we were overloading the type property, making it much less flexible). This patch just cleans up existing functionality.

We need two properties, for example for custom themed forms:
By default, the #type form has just #theme_wrapper set, #theme is empty. #theme_wrapper adds the form markup around the rendered child elements of the form. Now we can set #theme on an individual form (e.g. via hook_form_alter or in the form definition), where we control how and where the individual children are printed (we use this on many forms in core). But because of #theme_wrapper, we don't have to deal with the form markup, this just gets wrapped around the result of #theme. Same goes for fieldsets, or for individual form items, which are first themed and then the form_element markup is wrapped around the form element (which finally makes theme_form_element overridable on a per-element-basis. This used to be a pain for themers.).

So the patch doesn't really introduce new functionality, it just cleans up existing stuff.

RobLoach’s picture

#prefix and #suffix?

Frando’s picture

#prefix and #suffix are used for different purposes (adding markup before or after an element). #prefix and #suffix should not have to interfere with the generation of the element itself. It would make things horribly complicated if #prefix and #suffix had to be used to theme the actual element.

Note, again, that #theme_wrapper is not actually new. Removing it would be a regression to Drupal 5 and 6.

eaton’s picture

I need to look into this more, but at first glance, I think it is a little weird that we have both #theme and #theme_wrapper. Can't we just have #theme and fix up #type differently? I'd prefer a more intuitive API, to be honest.

We need a way to render the internal markup of an element (usually its child elements) separately from its 'implicit' markup.

Why? Many of our types -- forms and fieldsets are two easy examples -- require that themers be able to override the rendering of the children by setting #theme, while a separate standard theme function be used to render the 'wrapper' around the element, like the FORM tags and its attributes.

We currently handle this using a lot of tangly logic, with two completely separate code paths based on whether #theme is set or not, with bonus magic that changes an item's #type behind its back and calls drupal_render a second time, recursively, to get the wrapper stuff working. That makes our drupal_render code a lot more complicated, makes it impossible to override the wrapper for something like a fieldset without overriding ALL fieldsets drupal-wide, and also makes #type a nasty magic name that collides very easily with existing theme functions. (#type => image would not work, for example, because it would collide with the existing theme('image') implementation.)

With this patch, we render child elements using #theme, then -- if it is present -- use #theme_wrapper to handle the themed enclosing markup. Fieldsets, forms, and other "wrapped" elements can supply default wrapper theme functions, be overridden globally or on an element-by-element basis (for example, to make the element labels on a form display differently than normal), and the internal #theme function for child elements still works as it does today.

Regarding #prefix and #suffix, in addition to the issues Frando noted, they're not themable: the #theme_wrapper method still gives people the ability to override functions like theme_form_element().

starbow’s picture

I want to play. How about #theme (for self) and #theme_children (for children)?
Separating these two make a lot of sense to me...of course what I most want to do is theme the children as xhtml, then theme the element itself as JSON and send it off to a ajax call.

Frando’s picture

Well, #theme / #theme_wrapper or #theme_children / #theme is basically bikeshedding IMO, except that for elements that are never allowed to have children (e.g. buttons or textfields), #theme and not #theme_wrapper is used to print the element, so renaming #theme to #theme_children might be more confusing here. So I vote for sticking with #theme and #theme_wrapper.

eaton’s picture

Also, using #theme_wrapper means that #theme preserves its current meaning. If we went to #theme and #theme_wrapper, the meaning would be inverted -- #theme would not be able to touch any of the data it currently works with.

babbage’s picture

Issue tags: +Bikeshed

Tag. :)

Dries’s picture

After looking into the patch some more, I agree that the patch improves the current situation, but I wouldn't exactly call it themer-friendly yet. I'm happy to commit this patch, if we try to make the PHPdoc a bit more themer-friendly.

In the PHPdoc, it would be useful to add another paragraph of help text after A wrapper theme function always has to include the element's #children property in its output, as this contains the rendered children.

I think it should give an explanation, or an example, of a type that require that themers be able to override the rendering of the children by setting #theme, while a separate standard theme function be used to render the 'wrapper' around the element. I don't think we've quite nailed that explanation yet, and I'd like to see us take another crack at it such that a themer can grok this. The explanation should show that we need both, and why.

Thanks!

starbow’s picture

Wow, there is a Bikeshed tag! I don't know why, but that makes me very happy.

Frando’s picture

Issue tags: -Bikeshed
FileSize
39.11 KB

The attached patch adds another paragraph of documentation to the PHPDOC of drupal_render, giving an example on the difference between #theme and #theme_wrapper. Also removed the bikeshed tag, this issue is not at all about bikeshedding but about significant simplifications to a very important Drupal core function that is getting used more and more (and luckily so) ;)

chx’s picture

Frando, why would I render the same array twice on a page?? If, say, you print a form twice then it will be a different array where at least #id properties differ. If I want the very same for some reason, then I simply duplicate the output of drupal_render.

Frando’s picture

FileSize
38.68 KB

chx, I thought about $force some more and agree that it's not needed currently. In an earlier stage of the patch I thought it might be useful for drupal_render_children, but is not and might not even work properly in conjunction with #theme functions.

So, $force is removed from the patch now.

eaton’s picture

Status: Needs review » Reviewed & tested by the community

This is awesome, frando, and a big, big win for both internal simplification and flexibility in overriding FAPI's dense nest of divs and classes. I'm going to be bold and set this as ready; the only question IMO is Dries' feelings on the PHPDoc.

chx’s picture

I am content as well now that $force is gone.

babbage’s picture

Frando: I added the Bikeshed tag not in relation to the original or subsequent patches (which look like a good improvement) but the developing argument about how the css tags should be named—which really had nothing to do with the patch, but potentially threatened to delay its progress. And it was a bit of a wind-up regarding that conversation too, admittedly. ;)

I fully agree with the point where you removed it.
:)

Dries’s picture

Status: Reviewed & tested by the community » Needs work

Thanks for working on the documentation some more. I have committed this to CVS HEAD. Please mark this 'fixed' once you updated the upgrade instructions. Thanks.

Dries’s picture

Thanks for working on the documentation some more. I have committed this to CVS HEAD. Please mark this 'fixed' once you updated the upgrade instructions. Thanks.

Frando’s picture

Status: Needs work » Fixed

added

drupal_render() theming properties changed

(issue) The value of the #type property of elements declared in hook_elements is no longer treated as a theming function. Instead, there are now two explicit properties that control HTML generation: #theme and #theme_wrapper. #theme is mostly the same as in Drupal 6, it can be used to render the element's children. #theme_wrapper is called afterwards and can be used to wrap markup around the rendered children, similar to the way the #type property used to be used.

If you implement hook_elements, you usually will have to set at least one of these properties for your elements, likely to the same value as #type.
In Drupal 6, in the #theme theme function of an element, it was common to call drupal_render() on the element itself to render all remaining children. This now causes an endless loop, use drupal_render_children() instead.

Also see the API docs for drupal_render()

to the upgrade docs.

Status: Fixed » Closed (fixed)

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

David_Rothstein’s picture

It seems that this patch broke the theming for radios and checkboxes -- see #433992: Radios missing class form-radios, theme_radios() never called

The solution I came up with there was to make #theme_wrapper an array, but it sounds like that idea was discussed and rejected above... so maybe someone who is really up to speed on drupal_render() and friends can hop over to the other issue and take a look?

Gábor Hojtsy’s picture

I suspect this also broke the page construction rework with do at: http://drupal.org/node/428744#comment-1583306 Whether this patch or that patch is at fault is a good question to me.

chx’s picture

Priority: Normal » Critical
Status: Closed (fixed) » Needs work

The upgrade documentation and the doxygen needs work and lots. Consider this:

function theme_node_filter_form($variables) {
  $form = $variables['form'];
  $output = '';

  $output .= '<div id="node-admin-filter">';
  $output .= drupal_render($form['filters']);
  $output .= '</div>';
  $output .= drupal_render_children($form);
  return $output;
}

Uhm why there is a d_r and why there is a d_r_c? When i want to use which?

moshe weitzman’s picture

Title: Refactor drupal_render theming » Refactor drupal_render theming - docs
Priority: Critical » Normal
effulgentsia’s picture

For D8, looks like drupal_render_children() is being merged into drupal_render(). See #1920886: drupal_render() should check if it's rendering a 'render element' and if so call drupal_render_children() (inline) instead for details.

  • Dries committed 29c8e40 on 8.3.x
    - Patch #355236 by Frando: refactor drupal_render() theming.
    
    

  • Dries committed 29c8e40 on 8.3.x
    - Patch #355236 by Frando: refactor drupal_render() theming.
    
    

  • Dries committed 29c8e40 on 8.4.x
    - Patch #355236 by Frando: refactor drupal_render() theming.
    
    

  • Dries committed 29c8e40 on 8.4.x
    - Patch #355236 by Frando: refactor drupal_render() theming.
    
    

  • Dries committed 29c8e40 on 9.1.x
    - Patch #355236 by Frando: refactor drupal_render() theming.