Hello,

One year ago, the Drupal 8 html_tag has been updated and it's now able to render nested tags, see the CR here #2887146.

I backported the functionnality to Drupal 7 so no, we can render this render array properly, including the children.

To test the functionnality, run this code before and after applying the patch in /devel/php:

$render_array = array(
  '#type' => 'html_tag',
  '#tag' => 'h1',
  '#attributes' => array('class' => 'title'),
  'children' => array(
    array(
      '#type' => 'link',
      '#title' => 'Link title',
      '#href' => '/',
      '#attributes' => array('class' => 'inner'),
    ),
    array(
      '#theme' => 'link',
      '#text' => 'Link title',
      '#path' => '/',
      '#options' => array(
        'attributes' => array(),
        'html' => FALSE,
      )
    ),
  )
);

$html = render($render_array);

debug($html);

Without the patch: <h1 class="title" />

With the patch: <h1 class="title"><a href="/" class="inner">Link title</a><a href="/">Link title</a></h1>

Another example:

$render_array = array(
  '#type' => 'html_tag',
  '#tag' => 'h1',
  '#attributes' => array('class' => 'title'),
  '#value' => 'value',
  'children' => array(
    array(
      '#type' => 'link',
      '#title' => 'Link title',
      '#href' => '/',
      '#attributes' => array('class' => 'inner'),
    ),
    array(
      '#theme' => 'link',
      '#text' => 'Link title',
      '#path' => '/',
      '#options' => array(
        'attributes' => array(),
        'html' => FALSE,
      )
    ),
  )
);

$html = render($render_array);

debug($html);

Without the patch: <h1 class="title">value</h1>

With the patch: <h1 class="title">value<a href="/" class="inner">Link title</a><a href="/">Link title</a></h1>

Change record

Proposed change record: https://www.drupal.org/node/2982025

Comments

Pol created an issue. See original summary.

pol’s picture

StatusFileSize
new2.97 KB
pol’s picture

Status: Active » Needs review
pol’s picture

Issue summary: View changes
pol’s picture

Issue summary: View changes
pol’s picture

Issue summary: View changes
andypost’s picture

Title: Let the 'html_tag' element render children if any » Let the 'html_tag' element render children if any (backport from d8)

bit cleaner title

Status: Needs review » Needs work

The last submitted patch, 2: 2981726.patch, failed testing. View results

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new2.95 KB

Updating the patch.

Status: Needs review » Needs work

The last submitted patch, 9: 2981726.patch, failed testing. View results

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new2.96 KB

Update the failing test.

pol’s picture

StatusFileSize
new2.95 KB

Update the failing test.

The last submitted patch, 11: 2981726.patch, failed testing. View results

pol’s picture

StatusFileSize
new2.96 KB

Update code and failing test.

The last submitted patch, 12: 2981726.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 14: 2981726.patch, failed testing. View results

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new2.96 KB

Fix the test.

Status: Needs review » Needs work

The last submitted patch, 17: 2981726.patch, failed testing. View results

pol’s picture

For some reasons, the tests are passing locally, but not on the Drupal CI. Investigating.

pol’s picture

I'm unable to reproduce the issue from the Drupal CI.

Here's the result locally:

$ php ./scripts/run-tests.sh --url http://d7x/ --class "RenderElementTypesTestCase" --php /usr/bin/php "RenderElementTypesTestCase"

Drupal test run
---------------

Tests to be run:
 - Render element types (RenderElementTypesTestCase)

Test run started:
 Tuesday, June 26, 2018 - 13:59

Test summary
------------

Render element types 5 passes, 0 fails, and 0 exceptions

Test run duration: 24 sec

$
pol’s picture

Status: Needs work » Needs review
StatusFileSize
new2.95 KB

Update the tests.

Status: Needs review » Needs work

The last submitted patch, 21: 2981726.patch, failed testing. View results

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new2.96 KB

Update the tests.

samuel.mortenson’s picture

Status: Needs review » Reviewed & tested by the community

This looks good to me after a short review.

pol’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new3.16 KB

Update patch codestyle.

pol’s picture

StatusFileSize
new3.53 KB

Fix test.

The last submitted patch, 25: 2981726.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 26: 2981726.patch, failed testing. View results

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new3.53 KB

Fix the test.

Status: Needs review » Needs work

The last submitted patch, 29: 2981726.patch, failed testing. View results

manuel garcia’s picture

Issue tags: +Needs change record

+1 to doing this, thanks @Pol!

I think a CR for this would be great :)

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new4.17 KB
pol’s picture

Issue summary: View changes
pol’s picture

Issue summary: View changes
pol’s picture

The last patch:

  1. Fix the issue with nested element in html_tag (the original issue)
  2. Add a test for this new feature
  3. Remove the useless space at the end of the attributes (ex: <br /> to <br/>)

Ready for review again.

manuel garcia’s picture

Issue tags: -Needs change record

Thanks for the CR, I've reworded and clarified it a bit, in the spirit of making it clearer.

ieguskiza’s picture

Assigned: Unassigned » ieguskiza
ieguskiza’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed and tested the added functionality and tests, looks ready for me.

ieguskiza’s picture

Assigned: ieguskiza » Unassigned
donquixote’s picture

As an initial reaction, the proposed change appears to be an improvement and is consistent with Drupal 8.
I personally have run into situations where I did wish that 'html_tag' in D7 would work the way that is proposed here.

BC concerns

On second thought, the proposed change does introduce BC issues in some edge cases.

Here is a line from drupal_render():

  $elements += element_info($elements['#type']);

This means the following will produce a different result with this patch than it does now:

$element = array(
  '#theme_wrappers' => array('container'),
  '#type' => 'tag_name',
  '#tag' => 'strong',
  '#value' => 'Hello',
);

Without the patch, I expect the following html:
<div><strong>Hello</strong></div>
With the patch, I expect the following html:
<div>Hello</div>

Why?
The #theme_wrappers will still be just array('container'), not array('container', 'tag_name') or array('tag_name', 'container'), due to the way drupal_render() works.

Was this a BC break in D8 too?

The D8 change record was on 29 June 2017, on 8.4.x branch.
At this time D8 itself was already considered stable, but 8.4.0 was not released yet (this was on 4 October 2017).
The D8 change is implemented somewhat differently, using a preRenderHtmlTag() method. So, I don't know if this is comparable anyway.

The BC break I am pointing out here might not be the only one. We are changing 'html_tag' to work as a theme wrapper, this might cause other side effects.

Alternatives

We could look at different implementations with #pre_render, but this could also cause edge case BC issues, for similar reasons.

A safer alternative would be to introduce a new element, or extend the 'container' element to have a dynamic '#container_tag_name' property. I am saying #container_tag_name or #container_tag, not just #tag_name or #tag, to reduce possible conflict with other elements that support a #tag property.

$render_array = array(
  '#type' => 'container',
  '#container_tag' => 'h1',
  '#attributes' => array('class' => 'title'),
  '#value' => 'value',
  'children' => array(
    array(
      '#type' => 'link',
      '#title' => 'Link title',
      '#href' => '/',
      '#attributes' => array('class' => 'inner'),
    ),
    array(
      '#theme' => 'link',
      '#text' => 'Link title',
      '#path' => '/',
      '#options' => array(
        'attributes' => array(),
        'html' => FALSE,
      )
    ),
  )
);

Contrib

There is at least one contrib implementation I know of,
'themekit_container'.
(this reminds me I need to publish a stable version of this module)

Perhaps there are others who have attempted the same.

I am NOT proposing that everyone use this or another contrib solution. Adding a dependency just for one theme hook usually is not worth it.

Conclusion

The original proposal has potential BC issues, but is more consistent with D8.
We might conclude that the BC issues are survivable or can be somehow mitigated.

The alternative has less BC issues, but is not consistent with D8.

pol’s picture

Hello @donquixote,

At first I had exactly the same idea, and I've filled in an issue regarding this as well... #2981632: Let the 'container' element use a different HTML tag than the default "div".
Then only I proposed this patch when I saw how Drupal 8 did the thing.

I've updated your example:

$element = array(
  '#theme_wrappers' => array('container'),
  '#type' => 'html_tag',
  '#tag' => 'strong',
  '#value' => 'Hello',
);

$html = render($element);
debug($html);

Without the patch:

<div><strong>Hello</strong></div>

With the patch:

<div></div>

donquixote’s picture

I've updated your example:

You are right.

donquixote’s picture

Code review for #32:

+ *
+ * @return string
+ *   The Drupal element.

Perhaps this should read "The rendered html" or something like that.

+  if (!empty($element['#children']) || NULL !== $element['#value']) {

I wonder, what should happen if $element['#children'] === '' vs !isset($element['#children'])?
I think the latter happens if 'tag_name' is used with #theme, the other if used with #theme_wrappers with no children.

We can output either this
<TAGNAME></TAGNAME> (with empty contents)
or this
<TAGNAME/>

I think if used with #theme_wrappers, we always want the former. Or do we?

For a browser they might be equivalent, but we still want a clearly defined outcome in each case.

NULL !== $element['#value']
I like the yoda condition, but probably somebody will complain about it :)

+    $output .= $element['#value_prefix'];
+    $output .= $element['#value'] . $element['#children'];
+    $output .= $element['#value_suffix'];

Perhaps the #value_prefix and #value_suffix should only wrap the #value, not the #children?
So, more like this,

    $output .= $element['#value_prefix'];
    $output .= $element['#value'];
    $output .= $element['#value_suffix'];
    $output .= $element['#children'];

For the overall structure, I would usually propose "onion style", where you first build the inner html and then wrap it into a container.
But in this case it actually seems good enough to me in your patch, I don't see how changing it would greatly improve things.

pol’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new4.4 KB

Thanks for the suggestions @donquixote.

I've updated the patch:

  1. Wrapped the #value only with #value_prefix and #value_suffix,
  2. Removed the yoda condition,
  3. Updated the way the tag is built, using the "onion" style,
  4. Added a better comment.
pol’s picture

StatusFileSize
new4.39 KB

Patch updated:

  1. Update the inline comments
pol’s picture

StatusFileSize
new1.36 KB

Here's the interdiff with #32.

_KurT_’s picture

HI, shouldn't we check if $element['#tag'] is present and is not NULL ?
I understand that it is created for this, to render what exactly you need, but currently it renders < class="title">value<a href="/" class="inner">Link title</a><a href="/">Link title</a></> if #tag is empty.

pol’s picture

Hello,

Yes we could check for the existence of that property. But then, we have to decide what to do in each case.

If the $element['#tag'] is not present ? What tag to use ?

The current implementation of theme_html_tag() does not check for that.

Because of those black spots, I didn't add the check.

donquixote’s picture

HI, shouldn't we check if $element['#tag'] is present and is not NULL ?

#tag is considered a required property for #theme = 'html_tag' or #type = 'html_tag'.
A number of theme hooks have required properties, and in most cases there are no checks in place, they are just expected to be there. Omitting e.g. $image['#path'] would lead to a PHP notice, and it would not work properly.

The current implementation of theme_html_tag() does not check for that.

Exactly.
I think we should keep it that way.

If the $element['#tag'] is not present ? What tag to use ?

I'd say this counts as incorrect use of the 'html_tag', so the some kind of error handling should apply.
In a different life, we might let theme functions throw a specific type of exception.
In this life, we just let the PHP notice happen.

pol’s picture

markhalliwell’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record
  1. +++ b/includes/theme.inc
    @@ -2274,25 +2274,44 @@ function theme_feed_icon($variables) {
    +  // Set default values to avoid warning later.
    +  $element += array(
    +    '#attributes' => array(),
    +    '#children' => '',
    +    '#value' => NULL,
    +    '#value_prefix' => '',
    +    '#value_suffix' => '',
    +  );
    ...
    +  $content = $element['#value_prefix'] .
    +    $element['#value'] .
    +    $element['#value_suffix'] .
    +    $element['#children'];
    ...
    -    if (isset($element['#value_prefix'])) {
    ...
    -    if (isset($element['#value_suffix'])) {
    

    The #value_prefix and #value_suffix properties had isset() checks around them because these properties are not defined in system_element_info().

    If we want to remove the checks, then these properties need to be added to the element info as default values (that's why element_info exists in the first place).

    I suspect that this is an attempt to deal with the fact that this is technically a theme hook, which can be invoked outside of the render system using theme() (which is not possible in 8.x and, now, considered extremely bad practice in 7.x).

    I would almost argue that this should deprecate the theme hook implementation entirely and move all this code to a #pre_render callback that populates #markup like HtmlTag::preRenderHtmlTag does (which would be a true 8.x backport BTW, unlike what the current issue title suggests).

    We can then replace the code in theme_html_tag with something that constructs a new element using the correct #type and populating the appropriate properties, spit out a deprecation notice (perhaps with the ability to silience it via a variable) and then render the new element using drupal_render().

    There wouldn't be any risk of recursion either since the html_tag element would get rid of the #theme property and simply rely on the new #pre_render callback that populates #markup.

  2. +++ b/modules/simpletest/tests/theme.test
    @@ -513,7 +513,7 @@ class RenderElementTypesTestCase extends DrupalWebTestCase {
    -        'expected' => '<meta name="description" content="Drupal test" />' . "\n",
    +        'expected' => '<meta name="description" content="Drupal test"/>' . "\n",
    

    Pretty sure this could be considered a BC break. While newer browsers don't care about this space, I don't think changing this now is worth breaking existing contrib/site tests as well.

---

Regardless of whatever is done here, this issue will likely need a CR.

markhalliwell’s picture

Title: Let the 'html_tag' element render children if any (backport from d8) » Backport D8 html_tag element changes
Related issues: +#552478: Restrict "self-closing" tags to only void elements in drupal_pre_render_html_tag

This should also take into account changing the logic of the self-closing tags to only apply to void tags, not based on content (see related 8.x issue).

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new4.28 KB

Here's a patch with a complete different approach - I'm putting this here to test it first, of course, feedback is welcome.

Status: Needs review » Needs work

The last submitted patch, 53: 2981726.patch, failed testing. View results

markhalliwell’s picture

Closer, I'm not entirely sure why only certain parts were backported. The majority of the code, documentation, and tests could more or less be a 1:1 copy and paste (adjusting for 7.x APIs/simpletest of course).

---

  1. +++ b/includes/common.inc
    @@ -5769,6 +5769,36 @@ function drupal_pre_render_conditional_comments($elements) {
    +function drupal_pre_render_htmltag_markup(array $elements) {
    

    This is a weird name, in the early days of 8.x, it was just named: drupal_pre_render_html_tag.

  2. +++ b/includes/common.inc
    @@ -5769,6 +5769,36 @@ function drupal_pre_render_conditional_comments($elements) {
    +  $void_elements = array(
    +    'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
    +    'link', 'meta', 'param', 'source', 'track', 'wbr',
    +  );
    

    This is not the same list as 8.8.x.

  3. +++ b/includes/common.inc
    @@ -5769,6 +5769,36 @@ function drupal_pre_render_conditional_comments($elements) {
    +  $elements['#prefix'] .= $open_tag;
    +  $elements['#suffix'] = $close_tag . $elements['#suffix'] ."\n";
    

    These overwrite potential #prefix and #suffix properties from previous #pre_render callbacks (i.e. conditional comments, see D8's implementation).

  4. +++ b/modules/system/system.module
    @@ -313,9 +313,14 @@ function system_element_info() {
         '#theme' => 'html_tag',
    

    This needs to be removed as it is now handled by the new #pre_render callback.

    Also, theme_html_tag needs to be changed to something like the following:

    function theme_html_tag($variables) {
      // Ensure the type is always "html_tag" and then render the element.
      $variables['element']['#type'] = 'html_tag';
    
      // @todo Trigger deprecation warning. This theme hook was removed in 8.x.
      //   This will help contrib/custom code identify where they are manually invoking
      //   theme() and using '#theme' => 'html_tag' instead of using '#type' => 'html_tag'.
      return drupal_render($variables['element']);
    }
    
  5. +++ b/modules/system/system.module
    @@ -313,9 +313,14 @@ function system_element_info() {
    +    '#prefix' => NULL,
    +    '#suffix' => NULL,
    

    Shouldn't these be #value_prefix and #value_suffix? These properties aren't handled in 8.x (because it's a proper render array and #weight can be used for child elements), but these will need to be supported in 7.x for BC reasons. I suspect they can just be prepended and appended to #value respectively. Core already handles #prefix and #suffix properties defined any any renderable element automatically.

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new2.76 KB
new8.6 KB

Thanks Mark,

I updated the patch, fixing most of your comments.

As you said, we can almost copy the doc from D8 and the tests as well, but it's not yet done.

pol’s picture

Looks like I uploaded the wrong patch, going to update that now.

pol’s picture

StatusFileSize
new2.76 KB
new5 KB

Here's the good patch with the interdiff against #53.

pol’s picture

StatusFileSize
new9.43 KB
new10.53 KB
  • Small code adjustements
  • Updated documentation
  • Updated tests - copy the tests from D8, they are 99.9% the same
pol’s picture

markhalliwell’s picture

Status: Needs review » Needs work
  1. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    + * @param array $elements
    

    Should be $element, a #pre_render callback is invoked on a single element (regardless if it has children).

  2. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    + *   - #attributes: (optional) An array of HTML attributes to apply to the
    + *     tag. The attributes are escaped, see \Drupal\Core\Template\Attribute.
    

    The reference needs to be changed to drupal_attributes().

  3. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    + *   - #value: (optional) A string containing tag content, such as inline
    + *     CSS. The value of #value will be XSS admin filtered if it is not safe.
    ...
    +    $elements['#markup'] = $elements['#value'];
    

    The #value needs to be ran through filter_xss_admin(), which also means there is a test missing.

  4. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    +function drupal_pre_render_htmltag(array $elements) {
    

    There should be an underscore (_) between html and tag.

  5. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    +  $void_elements = array(
    

    Add the corresponding 8.x comment explaining what void elements are via URLs above this please.

  6. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    +  $elements['#markup'] = $elements['#value_prefix'] . $elements['#markup'];
    +  $elements['#markup'] .= $elements['#value_suffix'];
    

    This should only work in non-void elements (else statement above).

    I would also argue that these properties should be deprecated and a notice triggered (via watchdog) when they're used informing developers to use child elements in the render array instead.

    Thus, both of these need independent isset() checks.

  7. +++ b/includes/common.inc
    @@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
    +  $elements['#prefix'] .= $open_tag;
    +  $elements['#suffix'] = $close_tag . $elements['#suffix'];
    

    This should also backport the #noscript feature as well as check for existing properties, please just use the same 8.x code:

        $prefix = isset($element['#prefix']) ? $element['#prefix'] . $open_tag : $open_tag;
        $suffix = isset($element['#suffix']) ? $close_tag . $element['#suffix'] : $close_tag;
        if (!empty($element['#noscript'])) {
          $prefix = '<noscript>' . $prefix;
          $suffix .= '</noscript>';
        }
        $element['#prefix'] = $prefix;
        $element['#suffix'] = $suffix;
    
  8. +++ b/includes/theme.inc
    @@ -2313,25 +2313,19 @@ function theme_feed_icon($variables) {
    + * @deprecated
    + *   This theme hook was removed in Drupal 8.
    +//   Use '#type' => 'html_tag' instead of '#theme' => 'html_tag'.
    

    This should indicate when this was deprecated. Also, the last line needs to start with an asterisk, not an inline comment.

    Also, at the very least, we should trigger a watchdog deprecation notice here I think?

  9. +++ b/modules/system/system.module
    @@ -312,10 +312,16 @@ function system_element_info() {
    +    '#prefix' => NULL,
    +    '#suffix' => NULL,
    

    Not necessary. This is automatically handled by drupal_render().

---

Change record should also mention the deprecation of theme_html_tag().

markhalliwell’s picture

+++ b/includes/common.inc
@@ -5769,6 +5769,60 @@ function drupal_pre_render_conditional_comments($elements) {
+  $elements['#children'] = $elements['#markup'];

+++ b/modules/simpletest/tests/theme.test
@@ -546,6 +546,201 @@ class RenderElementTypesTestCase extends DrupalWebTestCase {
+      array(
+        'name' => 'Ensure that nested render arrays render properly.',
+        'value' => array(
+          '#type' => 'html_tag',
+          '#tag' => 'p',
+          '#value' => NULL,
+          [
+            ['#markup' => '<b>value1</b>'],
+            ['#markup' => '<b>value2</b>'],
+          ],
+        ),
+        'expected' => '<p><b>value1</b><b>value2</b></p>' . "\n",
+      ),

Manually setting #children shouldn't be necessary... that is why #markup is used.

Although, that being said... I suspect this is because #markup works in quite differently in 8.x compared to its previous 7.x counterpart. (edit: yes, Renderer::doRender() checks for #markup after #pre_render callbacks are invoked and child elements have been rendered and then prepends it to the existing #children property)

We have drupal_pre_render_markup() that can be used as a callback. This essentially does the same thing, but as a separate callback which would allow other #pre_render callbacks to be injected between this process should the need arise.

I'm still a bit confused however, as manually setting #children to something other than an empty string actually sort circuits drupal_render() from rendering any children though...

So I'm not entirely sure how this test is "passing" and seems like it's a false positive (BTW, no short array syntax).

We may have to convert #value (as well as #value_prefix and #value_suffix respectively) to child render array elements for this to work properly in 7.x.

pol’s picture

StatusFileSize
new13.13 KB
new8.17 KB

Massive feedback from @Mark, thanks!!! Very much appreciated :-)

I've updated the patch, fixing most of the issue I think, I will post it here to trigger the tests.

As discussed on Slack, I added an extra pre_render callback to render the children elements.

As this patch is quite big, I've created a temporary branch on Gitlab, I think it will be easier if we follow the dev on a specific branch on Gitlab.

Interdiff #59 to #62: https://gitlab.com/drupol/drupal/compare/9af4f5892f337160660f339b30c1562...

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new21.84 KB

Status: Needs review » Needs work

The last submitted patch, 64: 2981726.patch, failed testing. View results

markhalliwell’s picture

  1. +++ b/includes/common.inc
    @@ -5769,6 +5769,123 @@ function drupal_pre_render_conditional_comments($elements) {
    +  } else {
    

    Separate line.

  2. +++ b/includes/common.inc
    @@ -5769,6 +5769,123 @@ function drupal_pre_render_conditional_comments($elements) {
    +  if (isset($element['#value_prefix'])) {
    ...
    +  if (isset($element['#value_suffix'])) {
    

    These belong in the else statement above as they should only work on non-void elements (same as #value). This also likely needs a test.

  3. +++ b/includes/common.inc
    @@ -5769,6 +5769,123 @@ function drupal_pre_render_conditional_comments($elements) {
    +    watchdog(
    +      'theme',
    +      'Use of the %property property has been deprecated in 7.68. The %type element is now capable of rendering child elements. Instead of using this property, create additional child elements and manually set the %weight property accordingly. See https://www.drupal.org/node/2981726.',
    +      array(
    +        '%property' => '#value_prefix',
    +        '%type' => 'html_tag',
    +        '%weight' => '#weight',
    +      ),
    +      WATCHDOG_WARNING
    +    );
    

    Comments are what should respect the 80 character limit rule. Code (aside from arrays) all belong on one line (this needs to be applied to the two other warnings).

  4. +++ b/includes/theme.inc
    @@ -2313,25 +2313,31 @@ function theme_feed_icon($variables) {
    + * @deprecated This theme hook was removed in Drupal 8 and since Drupal 7.68, use '#type' => 'html_tag' instead of '#theme' => 'html_tag'.
    

    Should wrap at 80 characters.

  5. +++ b/includes/theme.inc
    @@ -2313,25 +2313,31 @@ function theme_feed_icon($variables) {
    +  // Make sure this is not set.
    +  unset($variables['element']['#theme']);
    

    "Ensure #theme is not set to prevent recursion."

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new26.42 KB

Comments from #66 are fixed.

The commit related to that is here: https://gitlab.com/drupol/drupal/merge_requests/8/diffs?commit_id=63ff7b...

Important remarks

Tests were failing with the previous version of the patch because of entries in the watchdog when clearing the cache. It was quite easy to guess where it was coming from.

The issues were in drupal_pre_render_styles() and drupal_pre_render_scripts(), so I made modifications to prevent the use of #value_prefix and #value_suffix and prevent warnings in watchdog (dblog).

I also had to do two calls to render() in drupal_pre_render_html_tag() because contrib modules like devel uses the #prefix and #suffix keys as render arrays.
render() had to be used to convert the render array into a string and I added a condition and watchdog message to stay BC while warning the user of that bad practice.

Let me know if it's the proper approach.

See the commit: https://gitlab.com/drupol/drupal/merge_requests/8/diffs?commit_id=c6e016...

markhalliwell’s picture

Status: Needs review » Needs work
  1. +++ b/includes/common.inc
    @@ -3597,9 +3597,20 @@ function drupal_pre_render_styles($elements) {
    -          $element['#value'] = $group['data'];
    -          $element['#value_prefix'] = $embed_prefix;
    -          $element['#value_suffix'] = $embed_suffix;
    +          $element += array(
    +            'prefix' => array(
    

    Changing the structure of this at this point is a BC break, especially for contrib modules that might modify styles/scripts (i.e. AdvAgg).

    Instead, perhaps we should introduce a silencing property that won't trigger the deprecation warnings.

    Something like $element['#ignore_deprecation_warning'] = TRUE;?

    This allows code that must use it (for BC reasons) to safely bypass the warnings while providing the originally intended functionality.

    We can then just wrap the watchdog calls to check for this:

    if (empty($element['#ignore_deprecation_warning'])) {
      watchdog(...);
    }
    
  2. +++ b/includes/common.inc
    @@ -5769,6 +5812,122 @@ function drupal_pre_render_conditional_comments($elements) {
    +      watchdog('theme', '%element Use of the %property property has been deprecated in 7.68. The %type element is now capable of rendering child elements. Instead of using this property, create additional child elements and manually set the %weight property accordingly. See https://www.drupal.org/node/2981726.', array('%property' => '#value_prefix', '%type' => 'html_tag', '%weight' => '#weight'), WATCHDOG_WARNING);
    

    Almost, arrays that have an opening parenthesis past the 80 character limit do get a new line after them (https://www.drupal.org/docs/develop/standards/coding-standards#array).

    This should be:

    watchdog('theme', '%element Use of the %property property has been deprecated in 7.68. The %type element is now capable of rendering child elements. Instead of using this property, create additional child elements and manually set the %weight property accordingly. See https://www.drupal.org/node/2981726.', array(
      '%property' => '#value_prefix',
      '%type' => 'html_tag',
      '%weight' => '#weight',
    ), WATCHDOG_WARNING);
    
  3. +++ b/includes/common.inc
    @@ -5769,6 +5812,122 @@ function drupal_pre_render_conditional_comments($elements) {
    +  if (is_array($element['#prefix'])) {
    ...
    +  if (is_array($element['#suffix'])) {
    

    These properties have never allowed arrays which means this is really an issue with devel specifically, not core.

    Also, the current html_tag implementation does not handle these either, drupal_render() does.

    So this doesn't belong here. It is completely out of scope of this issue.

  4. +++ b/includes/theme.inc
    @@ -2313,25 +2313,32 @@ function theme_feed_icon($variables) {
    +  watchdog(
    +    'theme',
    

    This needs the correct code style still.

  5. +++ b/includes/theme.inc
    @@ -2313,25 +2313,32 @@ function theme_feed_icon($variables) {
    +  // Ensure the type is always "html_tag" and then render the element.
    

    Now that there's another line of code below this, remove "and then render the element" from the comment. Besides, drupal_render($variables['element']) is already quite self explanatory.

---

Change record also needs to explicitly mention the deprecation of properties as well as html_tag theme hook (i.e. theme_html_tag()).

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new22.67 KB
pol’s picture

Change record updated:

  • Add a section "Deprecated" with an example

Link: https://www.drupal.org/node/2982025

markhalliwell’s picture

Status: Needs review » Needs work
Issue tags: -Needs change record
+++ b/includes/common.inc
@@ -5769,6 +5772,124 @@ function drupal_pre_render_conditional_comments($elements) {
+      if ($element['#ignore_deprecation_warning'] === FALSE) {
...
+      if ($element['#ignore_deprecation_warning'] === FALSE) {

+++ b/modules/system/system.module
@@ -312,9 +312,14 @@ function system_element_info() {
+    '#ignore_deprecation_warning' => FALSE,

Using empty($element['#ignore_deprecation_warning']) as the conditional is preferred as this doesn't require the value to be eplicitly FALSE (should allow any empty value: 0, NULL, '').

Thus, defining this in the element info isn't necessary, nor do we really want to advertise it there.

This is really just an optional property intended mainly for use internally (due to BC reasons only).

pol’s picture

Status: Needs work » Needs review
StatusFileSize
new22.91 KB
markhalliwell’s picture

Status: Needs review » Reviewed & tested by the community

LGTM, let's get @Fabianx's review on this now.

fabianx’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/includes/common.inc
    @@ -3495,6 +3495,7 @@ function drupal_pre_render_styles($elements) {
    +    '#ignore_deprecation_warning' => TRUE,
    
    @@ -3502,6 +3503,7 @@ function drupal_pre_render_styles($elements) {
    +    '#ignore_deprecation_warning' => TRUE,
    
    @@ -4535,6 +4537,7 @@ function drupal_pre_render_scripts(array $elements) {
    +    '#ignore_deprecation_warning' => TRUE,
    
    @@ -5769,6 +5772,124 @@ function drupal_pre_render_conditional_comments($elements) {
    +        watchdog('theme', '%element Use of the %property property has been deprecated in 7.68. The %type element is now capable of rendering child elements. Instead of using this property, create additional child elements and manually set the %weight property accordingly. See https://www.drupal.org/node/2981726.', array(
    +          '%property' => '#value_suffix',
    +          '%type' => 'html_tag',
    +          '%weight' => '#weight',
    +        ), WATCHDOG_WARNING);
    

    There is standard way for deprecation and that is to use trigger_error() with E_DEPRECATED, which will never trigger on prod (if properly configured) and always trigger on dev.

    I am okay with a kill-switch for that, but core special casing itself but not allowing contrib the same without code changes feels weird.

    Have to think about the deprecation part of the patch.

  2. +++ b/includes/common.inc
    @@ -5769,6 +5772,124 @@ function drupal_pre_render_conditional_comments($elements) {
    +    $element['#markup'] = filter_xss_admin($element['#value']);
    

    Appreciate the thinking of security, but it's a larger behaviour change.

    It could be okay as security hardening, but I would want it in it's own issue.

  3. +++ b/includes/common.inc
    @@ -5769,6 +5772,124 @@ function drupal_pre_render_conditional_comments($elements) {
    +function drupal_pre_render_children(array $element) {
    +  $element['#children'] .= drupal_render_children($element);
    +
    +  return $element;
    +}
    

    I assume this is the same in D8 - Why is it needed / wanted?

    drupal_render() is perfectly capable of rendering children by itself. Why do we want / need to pre_render them?

  4. +++ b/includes/theme.inc
    @@ -2313,25 +2313,27 @@ function theme_feed_icon($variables) {
    + * @deprecated This theme hook was removed in Drupal 8 and since Drupal 7.68,
    + *   use '#type' => 'html_tag' instead of '#theme' => 'html_tag'.
    + *
    + * @see https://www.drupal.org/project/drupal/issues/2981726
    

    This is fine in a way, but should core not just do the same (if it's a simple switch-over) instead of doing so.

    Why can't we transparently map '#theme => ...' to '#type' => '...' and just say the behavior changed - if in essence we do that ...

  5. +++ b/includes/theme.inc
    @@ -2313,25 +2313,27 @@ function theme_feed_icon($variables) {
    +  watchdog('theme', 'Use of the %hook theme hook has been deprecated in 7.68. Instead, create a render array that explicitly sets "#type" => "html_tag". See https://www.drupal.org/node/2981726.', array(
    +    '%hook' => 'html_tag',
    +  ), WATCHDOG_WARNING);
    

    That's not acceptable unfortunately - needs to use trigger_error.

    See Drupal 8 and Symfony.

  6. +++ b/includes/theme.inc
    @@ -2313,25 +2313,27 @@ function theme_feed_icon($variables) {
    +  // Ensure the type is always "html_tag".
    +  $variables['element']['#type'] = 'html_tag';
    

    Currently this would lead to even more deprecation warnings (if you use value_prefix and value_suffix right now).

    Should we not just leave '#theme' => 'html_tag' alone and maybe put a deprecation warning - if something wants to know that it's deprecated?

    In that way we would only improve the #type => 'html_tag'.

  7. +++ b/modules/system/system.module
    @@ -312,9 +312,13 @@ function system_element_info() {
    -    '#theme' => 'html_tag',
    -    '#pre_render' => array('drupal_pre_render_conditional_comments'),
         '#attributes' => array(),
    +    '#pre_render' => array(
    +      'drupal_pre_render_conditional_comments',
    +      'drupal_pre_render_html_tag',
    +      'drupal_pre_render_markup',
    +      'drupal_pre_render_children',
    +    ),
    

    A larger problem and that also applies to unset '#theme' and set 'type' instead in the theme_html_tag function is that:

    - There is #theme_wrappers that run directly after the children are rendered.

Sorry for spoiling the party, but with my conservative core committer hat on, I have to decline this for this iteration.

We - and I know that sucks process wise - need to add a deprecation policy to Drupal 7 first and also get agreement from the Community that using E_DEPRECATED is okay and maybe rather have a global kill switch for it.

As PHP 7 uses it, I think that should be fine.

As for the approach itself - I like it much in parts, but we need to be careful to not break existing usages. The patch already takes great care of that, but I unfortunately feel that we still have problems with some edge cases.

So sadly setting to CNW (or rather: Approach needs discussion)

joseph.olstad’s picture

You guys are doing great work here.
Trying to follow along; E_DEPRECATED sounds like a good idea, as long as it's compatible with the various versions of PHP (or maybe add a conditional based on minimum php version if it helps compatibility or is needed) otherwise a change record suffice if it's only for a far out edge case.

Passing all tests is very encouraging!

fabianx’s picture

I appreciate all the hard work that did go into this, but I feel a safer way would be to just do:

  // Get the children of the element, sorted by weight.
  if (!isset($element['#value'])) {
    $element['#value'] = '';

    $children = element_children($elements, TRUE);
    foreach ($children as $key) {
      $elements['#value'] .= drupal_render($elements[$key]);
    }
  }

  if (empty($element['#value'])) {
    // Direct close tag "/ >"
  }
  else {
    // ...
  }

Even that is a BC break though in a way, because if you added some children somewhere and then converted to type html_tag then it would work (as #value is not set), but as soon as above code would run the void element would suddenly have content. It's a real edge case, but could imagine we break something somewhere.

Yes, the D8 code and backport are way nicer, but it's a hard BC break - too, because if you use "my-custom-element" and expected "<my-custom-element />" then this would suddenly break as it's not white-listed -- even if invalid.

We could introduce a new 'd8_html_tag' though OR conditionally enable the functionality for new installs with old installs having to opt-in to using that.

OR we could enable it per render array, e.g. #type => 'html_tag', '#nested_html_tag' => TRUE,

I know BC is problematic, but I don't think just potentially breaking sites would be good.

donquixote’s picture

I have not commented on this issue since #49, but I did think about it since.
My current view is we should leave existing render element types and theme hooks unchanged in Drupal 7.
(I already mentioned this to Pol, just never wrote it down in this issue)

The functionality we hope to provide here can easily be achieved in other ways.

The safer solution would be to define a new theme hook and/or element type, instead of changing an existing one.
This was already done in contrib (theme_themekit_container).
We could decide to do something similar in core, if that makes people happy, and someone makes a strong and convincing case why it is needed.

People who already use 'html_tag', be it with '#type' or '#theme' or theme(), want their code to remain valid and the behavior unchanged.
People who need something else can choose the new theme hook or element type.

When designing such a new theme hook and/or element type, the correct mechanism, as already pointed out by Fabianx, is using '#theme_wrappers' for this.
By doing this, Drupal will already do all the work for us, and we don't need additional code to render the children.

In Drupal 7 (and still in Drupal 8 I guess) we have two categories of theme hooks:

  • Theme hooks designed to be used with '#theme', which do not care about '#children'. 'html_tag' is one of them.
  • Theme hooks designed to be used with '#theme_wrappers', which do print what's in '#children' and print their own markup around that as a wrapper. 'container' is one of them.

So what we try to accomplish here is closer in its behavior to 'container' than to 'html_tag'. The only thing missing in 'container' is a property to specify the tag name.
But this does not mean we should modify the 'container' theme hook or element type.
Imo we should leave both 'html_tag' and 'container' alone.
If we absolutely want this in core we should create something new.

markhalliwell’s picture

  • #74.1 - I was under the impression that 8.x had special handling that helped with trigger_error() and E_USER_DEPRECATED (which is typically what's used over E_DEPRECATED). I'm ok with following 8.x here if it actually works... I just thought it wouldn't (granted I didn't really try).
  • #74.2 - Sure. I felt it was appropriate due to backporting and that it uses the admin list, but even that has had issues in the past (in 7.x). We can definitely make this a follow up as it technically would be an intentional BC break for security hardening purposes.
  • #74.3 - No, it's not quite the same. In Renderer::doRender() (8.x), there is code that essentially does this very thing, except that here we made it a #pre_render callback instead of modifying drupal_render() as a whole (which could affect more than just this). In hindsight, I think the way this is actually intended to work (even in 8.x) is if #value is not empty, it doesn't render child elements. IMO, I think this is technically a bug in both 7.x and 8.x. Perhaps #value should just be converted into a #weight neutral child element and #value_prefix => -1 and #value_suffix => 1?Then we wouldn't have to worry about #markup at all and just let #children carry it?
  • #74.4 - Because the theme hook can be invoked, bypassing the Render API entirely, using theme(). In fact, that is what is prominately "suggested" in the comments of theme_html_tag(). Despite this being bad practice, it's quite common to see calls to theme(), specifically for the purpose of using html_tag. The fact that there has been both a theme hook and an element is what has led to this confusion (nevermind the performance implications) and why the theme hook was ultimately removed in 8.x.
  • #74.5 - Ok
  • #74.6 - No. Developers really shouldn't be using the theme hook implementation of html_tag at all. It's the worst offender of the direct theme() invocation out there and can potentially cause the most peformance issues when doing so. It's almost akin to use #markup everywhere. If theme() weren't invocable directly and developers forced to use the Render API to invoke theme hooks (like 8.x), this would be less of an issue. As it stands now, these calls can cause huge performance hits when used inappropriately (which is quite frequent) because it bypasses the Render API entirely, which of course prevents things like render caching from being used. This, by no means, "forces" developers to start using render arrays... but it at least warns them this theme hook is now deprecated and really shouldn't be used directly (because it was removed in 8.x).
  • #74.7 - I'm not entirely sure what you're trying to say here. However, I will attempt to venture a guess :D In some sites (even ones I've coded), it would be possible to do something like #theme => html_tag__nested__suggestion. In which case, simply removing this and then forcing #type => html_tag, this theme hook suggestion would become lost. No. This happens directly in the theme function for html_tag. If a [sub/base]theme implements a theme hook suggestion, the preprocess functions would still run prior to this function being invoked. This function is only ever called when we know it is the absolute last function to call (as it is supposed to return a rendered string). This means that if this function were to be invoked, it is done so with the knowledge that it is literally supposed to be html_tag, regardless of the suggestions prior. That being said, I wouldn't be opposed to perhaps save the theme hook in something like #context][theme_hook prior to unsetting #theme. This way we can at least have some context when it hits the Render API.

#76:
I don't like the idea of introducing a new "d8_html_tag" specific element. That wouldn't be a backport.

While, yes, introducing the void element list is technically a BC change from previous behavior, I think we could get away with providing BC support using something like #void_element => TRUE.

This would allow default behavior to function in the backport way of 8.x, while also providing developers a property to manually set (override default behavior) should they need their super special hypothetical custom element to behave as it did before.

Optionally we could, in theory, provide this void element list as a variable that developers could extend. However, I'm not a big fan of this as 1) 8.x does not do this and 2) introduces this list as something developers "should" customize, lending to the idea that "they know better".

Arguably, whether or not there is a closing tag or not for custom elements really boils down to supporting existing custom tests as they would expect very specific markup. Browsers really don't care about this, one way or the other.

The only reason I said what I did in #51.2 about the space before the closing self tag is because this would be a much broader change that would affect all of core/custom/contrib testing code alike.

If a few custom elements are suddenly now not void elements, I would imagine that the tests for these would be far fewer in number and a relatively easy enough of a change to implement.

But these, again, are just tests. This doesn't actually "break" the functionality of real-world production level code interpreted by the browser.

markhalliwell’s picture

Oh, regarding E_USER_DEPRECATED I was also under the impression we couldn't use this because these were only introduced in PHP 5.3 and 7.x still technically supports a minimum of PHP 5.2.4.

markhalliwell’s picture

#77 - If we absolutely want this in core we should create something new.

No, we shouldn't.

#77 - People who already use 'html_tag', be it with '#type' or '#theme' or theme(), want their code to remain valid and the behavior unchanged.

Which is what this does. As I stated in #78, the "changes" made here actually do not affect functionality (behavior), just tests. It would also be a very small subset of tests compared to the change I mentioned in #51.2.

Modern browsers do not care one way or the other if a tag self closes or has a dedicated closing tag; this is merely a convention from XHTML.

This does not prevent developers from using #theme => html_tag or theme('html_tag'). It still functions the same way, but it would let the developers know that it's been deprecated.

Which brings me to the next point: this should be about backporting the changes for html_tag from 8.x. This has absolutely nothing to do with containers. They're two intentionally separate elements designed to do different tasks.

When designing such a new theme hook and/or element type, the correct mechanism, as already pointed out by Fabianx, is using '#theme_wrappers' for this.
By doing this, Drupal will already do all the work for us, and we don't need additional code to render the children.

Except that isn't how drupal_render() actually works with #markup (try it, step through it).

The entire point of backporting the 8.x implementation is to deprecate and stop using the theme hook html_tag equivalent. There is no #theme_wrappers than can or should be used here.

The html_tag element isn't supposed to be "themeable" (theme hook). It's a low level render element intended to create custom markup without actually using #markup specifically to allow it to remain alterable prior to rendering.

This is why the theme hook was removed in 8.x; along with many, many other reasons.

It's also why we tell people to never use #markup directly and instead use and appropritate theme hook (template) or, at the very least, #type => html_tag.

---

Overall, the discussion around this topic kind of proves my point: developers don't know what the use case of html_tag really is and have been abusing it for years.

This change is necessary because it moves 7.x code inline with what is expected in 8.x.

It gives developers the opportunity to easily locate where these "bad practice" implementations are so they can change their code prior to migrating to future versions of Drupal.

This isn't about and should never be about "introducing a new element" so developers can feel better.

This should be about backporting expected behavior for future proofing site migration.

How we deal with BC, while important, is less of an issue.

We can find non-destructive ways to deal with BC without having to introduce entirely new concepts.

---

That all being said, I do think there is still a semi "bug" in that if it's a void element, then it shouldn't have any #value or child elements.

The patch above probably needs to unset child elements when a void element is detected. The 8.x implementation kind of handles this automatically due to how #markup operates, but in 7.x we'll need to be explicit about that in drupal_pre_render_html_tag() before it moves onto the other #pre_render callbacks.

The way drupal_render() works in 7.x is not the same as 8.x.

This divergence happened with the "safe markup" initiative happened in 8.x and changed... well, everything. The #markup property became an actual "standard" if you will, whereas in 7.x it's mostly there as a backup and for specific cases like this.

This is why these extra #pre_render callbacks are necessary, but also so we can separate out the specific "flattening" aspect (if you will) should the need arise where contrib/custom code wishes to interject their own #pre_render callback before it does so.

markhalliwell’s picture

I also feel like I need to further clarify why the void element change isn't really that big of a deal.

This will use the example in #76: <my-custom-element />

This means that to get this output in the current implementation, all one really has to do this:

$element = [
  '#type' => 'html_tag',
  '#tag' => 'my-custom-element',
];
drupal_render($element); // <my-custom-element />

The reason this outputs a "void element" here is that it has no #value of any kind.

The entire logic of the current implementation is based on whether #value is empty or not.

That's it.

By changing the logic to detect specific void elements, the output would suddenly become <my-custom-element></my-custom-element>.

Functionality (behaviorally) this is the exact same in the browser. The only difference is for specific custom tests that check for exact string literal output matches. As I explained above, this would likely be far and few in between and a relatively minor change to "fix". Because this only affects custom tests, this "change" is highly worth it.

We also don't have to worry about "child elements" because the never worked before so it is highly unlikely there would be any code out there that actually had this.

Regardless, let's play this out hypothetically.

Current implementation ignores any child element artifacts:

$element = [
  '#type' => 'html_tag',
  '#tag' => 'my-custom-element',
  ['#markup' => '<strong>child</strong>'],
];
drupal_render($element); // <my-custom-element />

Now, with the patch applied:

$element = [
  '#type' => 'html_tag',
  '#tag' => 'my-custom-element',
  ['#markup' => '<strong>child</strong>'],
];
drupal_render($element); // <my-custom-element><strong>child</strong></my-custom-element>

This artifact child element actually works correctly because it's placed inside the closing tag.

Whether this is intentional or not (it suddenly shows up) is really up to the developer/site that implemented it.

One could argue that, while technically a "change", it actually serves to highlight code that was originally intended to work this way in the first place (perhaps their theme added child support, in which case this would likely still work the same).

Or maybe it highlights code that should be removed.

Regardless, this is all very hypothetical and likely not happening in any real-world code since children were never supported by html_tag in the first place. Why would anyone just leave this there when it didn't work in the first place?

Note: this would also effectively be the same output it wasn't a child and instead '#value' => '<strong>child</strong>', (i.e. nothing "breaks").

Now let's use <hr />, current implementation:

$element = [
  '#type' => 'html_tag',
  '#tag' => 'hr',
  ['#markup' => '<strong>child</strong>'],
];
drupal_render($element); // <hr />

With patch:

$element = [
  '#type' => 'html_tag',
  '#tag' => 'hr',
  ['#markup' => '<strong>child</strong>'],
];
drupal_render($element); // <hr /><strong>child</strong>

This is the "bug" I mentioned at the end of #80. When it's determined that it should be a void element, we need to prevent all child elements from rendering. We could unset them, but perhaps we should just set #access => FALSE on them instead. I'm fairly certain this is a bug in 8.x as well... but I haven't really tried it out (this is all hypothetical and likely not happening in real-world code at all).

---

Despite all that, we can still add a #void_element boolean property to check for (for BC reasons). By default (NULL), it could determine this value base on #tag. If explicitly set to TRUE or FALSE, then it gives control over this back to the developer.

Supporting BC does not inherently mean we cannot change default output/behavior. Sure, we try to avoid this at all costs. However, in cases like this (backporting), the benefits outweigh the tiny fraction of developers that just might be affected by this.

All it means is that we have to provide some way for developers to easily get the previous (one could argue "desired") output/behavior.

That's all.

donquixote’s picture

@markcarver (#80)
Perhaps I have my priorities different, and "future proofing" is not that important to me.
Even with the perspective of upgrading to D8, I would still prefer my existing D7 site to remain stable, and then do all the work as a rewrite in D8, instead of having to fix the D7 site after core upgrades.
Or even worse, patch old contrib modules.

#77 - People who already use 'html_tag', be it with '#type' or '#theme' or theme(), want their code to remain valid and the behavior unchanged.

Which is what this does. As I stated in #78, the "changes" made here actually do not affect functionality (behavior), just tests. It would also be a very small subset of tests compared to the change I mentioned in #51.2.

The tests only cover a small subset of the permutations that are possible with render arrays.
Perhaps some of these don't make sense, but the developer who did it might no longer be around to be told that.

In the following example the difference would be less subtle.

$element = array(
  '#type' => 'html_tag',
  '#tag' => 'div',
  '#attributes' => array(
    'class' => 'test',
    'id' => 'id'
  ),
  '#value' => 'value',
  '#pre_render' => [],
);

$output = var_export(drupal_render($element));

$output_old = '<div class="test" id="id">value</div>
';
$output_new = '';

Then the other example with '#pre_render' in combination with '#type' => 'tag_name'.
I think this is what Fabianx meant above, perhaps in reference to my own comment #40. I think I misunderstood him earlier.

$element = array(
  '#type' => 'html_tag',
  '#tag' => 'article',
  '#attributes' => array(
    'class' => 'test',
    'id' => 'id'
  ),
  '#value' => 'value',
 # '#pre_render' => [],
  '#theme_wrappers' => ['container'],
);

$output = var_export(drupal_render($element));

$output_old = '<div class="test" id="id"><article class="test" id="id">value</article>
</div>';
$output_new = '<article class="test" id="id"><div class="test" id="id">value</div></article>
';

Perhaps there are more cases with funny combinations.

Is this relevant?
With all the existing Drupal 7 sites with adventurous custom code out there, I'd say yes.

Another thing that would no longer work is theme overrides for theme('html_tag').
I personally think it is a really bad idea to target html_tag in your theme, but I am sure that are sites that do it.

Which brings me to the next point: this should be about backporting the changes for html_tag from 8.x. This has absolutely nothing to do with containers. They're two intentionally separate elements designed to do different tasks.

To me, in D7, a '#type' => 'container' is a html element around element children but limited to div, whereas '#type' => 'html_tag' allows arbitrary tag name but does not support children. '#type' => 'container' might have uses beyond that, but the basic functionality is just that.
The D8 version of 'html_tag' provides the best of both worlds, but takes away the theme layer - which is good.
In D7 I do the same with 'themekit_container', which provides arbitrary tag name and rendering of children.

When designing such a new theme hook and/or element type, the correct mechanism, as already pointed out by Fabianx, is using '#theme_wrappers' for this.
By doing this, Drupal will already do all the work for us, and we don't need additional code to render the children.

Except that isn't how drupal_render() actually works with #markup (try it, step through it).

I think I am lost. I don't know which behavior you assume that I expect, which behavior you expect, if that is the same or different, and which exact combination we are even talking about :)

If we would want to design a new element where children are rendered and then wrapped into a html tag, then '#theme_wrappers' would do the job.
If we want to skip the theme layer, we could use '#post_render' instead.
However, if the goal is to change the behavior of an existing element type (which it is, if the priority is backporting), then perhaps both methods have their problems.

But this said: I don't know what kind of problem with '#markup' vs '#theme_wrappers' you are trying to point out here. I could only guess what you mean.

The html_tag element isn't supposed to be "themeable" (theme hook). It's a low level render element intended to create custom markup without actually using #markup specifically to allow it to remain alterable prior to rendering.

I support this sentiment, Theme overrides of 'html_tag' are a bad idea.

This change is necessary because it moves 7.x code inline with what is expected in 8.x.

It gives developers the opportunity to easily locate where these "bad practice" implementations are so they can change their code prior to migrating to future versions of Drupal.

As mentioned above I think there is a difference in priorities or philosophies.
Should D7 sites make gradual changes to make the transition to D8 easier? Should they even be nudged to do so?
Or is D8 so far gone that we should rather leave the D7 sites in peace?

donquixote’s picture

@Fabianx (#76)

Even that is a BC break though in a way, because if you added some children somewhere and then converted to type html_tag then it would work (as #value is not set), but as soon as above code would run the void element would suddenly have content. It's a real edge case, but could imagine we break something somewhere.

I find it hard to follow these hypothetical examples. It would help if they were more explicit.
Perhaps we should even add a range of different examples to the issue summary, with the different outputs from different implementations.

markhalliwell’s picture

Perhaps I have my priorities different, and "future proofing" is not that important to me.

Possibly. I see issues like this (and others) where there seems to be an insurgence of attempting to sudden add new features into 7.x.

There's at least two major issues with this:

  1. Any new features need to be first vetted and added in HEAD (8.x, soon to be 9.x), first, before making their way back to 7.x.
  2. 7.x is nearing EOL. If new features (that aren't present in future versions) are needed, they should be kept in contrib. If it's because there's resistance to the way 8.x works, perhaps the sites should instead migrate to Backdrop instead of trying to turn 7.x into something it was never meant to be in the first place. This should not backward fork the project into something that doesn't resemble its successor.

The only reason this issue has any grounding at all is due to #552478-193: Restrict "self-closing" tags to only void elements in drupal_pre_render_html_tag explicitly stating that a separate backport issue should be used for 7.x.

And that's what this is and should only be: a backport of upstream code.

This is not about arbitrarily fixing unique (entirely custom) use cases or the desire to introduce the ability to suddenly change container tags.

Nor is this about #theme_wrappers. This isn't about the theme system at all, it's about the Render API and fixing a blatant mistake of introducing a theme hook for html_tag when there should have never been one in the first place.

To actually make the backported code function properly (due to the way drupal_render() actually works in 7.x) some additional #pre_render callbacks are necessary. This is all explained above in the reviews/diff (including GitLab).

Another thing that would no longer work is theme overrides for theme('html_tag').

Not entirely true.

Themes that implement their own THEMENAME_html_tag() functions would still be picked up by the theme system API and overuse cores default implementation.

This is the very foundation of how the theme system works.

If you're referring to the removal of #theme in the element info definition (which was mainly removed for performance reasons), this could be supported via a variable to "add this back in" (for BC purposes) as the core theme function explicitly removes this to prevent recursion. This way any custom implementation could still theoretically work as it did before.

I personally think it is a really bad idea to target html_tag in your theme, but I am sure that are sites that do it.

It is, but has been a necessary "evil" since the element has always invoked the theme hook. I know of several sites I have done this personally actually. This is exactly why it was removed in 8.x and should be deprecated and discouraged here now.

---

Please note it is not my intention for this to come across as a personal attack. I'm just quoting for context and reference. I get your desire here, but there is clear precedence with how this should proceed. It's either a complete backport or nothing at all.

I believe we can backport this fairly easily enough with extremely few disruptions.

We can place existing variable toggles in areas we're not 100% sure all use cases would be applied properly.

Again, I will state, the purpose of backporting is not to provide a one click update and your site remains the same. It's to provide BC options for sites that need previous functionality.

Whether you're upgrading core or contrib, you should always read release notes and change records. Then thoroughly test it prior to actually deploying it to prod.

Sometimes, this includes making some very minor changes because your very specific site needs them.

This isn't the case with every site.

Status: Needs work » Closed (outdated)

Automatically closed because Drupal 7 security and bugfix support has ended as of 5 January 2025. If the issue verifiably applies to later versions, please reopen with details and update the version.