Problem/Motivation

Currently, Drupal.AjaxCommands.prototype.insert() always wraps the returned HTML in a div. There are multiple conditions where even the more limited DIV wrapping creates problems (see comments #41-#48).

See the consequences of this in massive duplication related issues, and on screens in #331, and on your sites.

Proposed resolution

The solutions avoids wrapping returned content in all cases where it's possible without breaking existing sites. We will still wrap the inserted markup when trying to insert multiple root elements with a ajax effect. This will be removed from Drupal 9.0.0. For more details: #284. However, the div is now added in a theme function which provides an option for developers to opt out from this.

We can't parse SVGElement without wrapper since innerHTML is only available on HTMLElement. See #316 - #321 about it.

As a result, we decided to first insert the markup into an empty div, and then use .contents() to get the elements (see #336):

// Parse response.data into an element collection.
const $newContent = $('<div></div>').html(response.data).contents();

// ...

// Add the new content to the page.
$wrapper[method]($newContent);

See latest patch

Remaining tasks

  • 🎉™️
CommentFileSizeAuthor
#394 interdiff-736066-391-394.txt1.46 KBGrandmaGlassesRopeMan
#394 736066-394.patch30.78 KBGrandmaGlassesRopeMan
#391 736066-391.patch29.32 KBalexpott
#391 387-391-interdiff.txt888 bytesalexpott
#387 interdiff.txt4.05 KBlauriii
#387 736066-387.patch29.72 KBlauriii
#386 interdiff-384-386.txt4.05 KBAnonymous (not verified)
#386 736066-386.patch29.66 KBAnonymous (not verified)
#384 interdiff-372-384.txt2.42 KBAnonymous (not verified)
#384 736066-384.patch29.34 KBAnonymous (not verified)
#382 interdiff-372-382.txt2.42 KBAnonymous (not verified)
#382 736066-382.patch2.42 KBAnonymous (not verified)
#372 interdiff-364-372.txt14.05 KBAnonymous (not verified)
#372 736066-372.patch28.98 KBAnonymous (not verified)
#370 interdiff-364-370.txt14 KBAnonymous (not verified)
#370 736066-370.patch14.05 KBAnonymous (not verified)
#364 interdiff-361-364.txt9.81 KBAnonymous (not verified)
#364 736066-364.patch30.8 KBAnonymous (not verified)
#361 interdiff-358-360.txt3.22 KBAnonymous (not verified)
#361 736066-360.patch31.12 KBAnonymous (not verified)
#358 interdiff-352-358.txt11.62 KBAnonymous (not verified)
#358 736066-358.patch31.25 KBAnonymous (not verified)
#352 interdiff-339-352.txt5.7 KBAnonymous (not verified)
#352 736066-352.patch27.89 KBAnonymous (not verified)
#350 wrapper-for-multiple-root-case.txt2.31 KBAnonymous (not verified)
#339 interdiff-338-339.txt898 bytesAnonymous (not verified)
#339 736066-339.patch25.62 KBAnonymous (not verified)
#338 interdiff-336-338.txt8.57 KBAnonymous (not verified)
#338 736066-338.patch25.5 KBAnonymous (not verified)
#336 interdiff.txt2.83 KBlauriii
#336 736066-336.patch26.14 KBlauriii
#335 interdiff-333-335.txt559 bytesAnonymous (not verified)
#335 736066-335.patch27.75 KBAnonymous (not verified)
#333 interdiff-322-333.txt6.82 KBAnonymous (not verified)
#333 736066-333.patch27.85 KBAnonymous (not verified)
#333 interdiff-322-328.txt7.56 KBAnonymous (not verified)
#328 736066-296-328.patch23.86 KBandreyks
#324 736066-324.patch26.39 KBpiyuesh23
#322 interdiff-736066-296-322.txt2.94 KBGrandmaGlassesRopeMan
#322 736066-322.patch27.73 KBGrandmaGlassesRopeMan
#320 svg-auth.gif1.13 MBGrandmaGlassesRopeMan
#318 svg.gif573.46 KBGrandmaGlassesRopeMan
#316 svg_test.zip3.06 KBidebr
#316 736066-316-svg-test-do-not-test.patch3.73 KBidebr
#316 bigpipe-rendering-svg.gif314.64 KBidebr
#296 736066-296.patch26.35 KBedurenye
#284 ajax-patch.mov747.5 KBeffulgentsia
#284 ajax-head.mov896.24 KBeffulgentsia
#284 ajax-interdiff-for-testing.txt702 byteseffulgentsia
interdiff-270-275.txt4.68 KBidebr
736066-275.patch23.41 KBabacaba
736066-272.patch539 bytesabacaba
#270 736066-270.patch26.43 KBGrandmaGlassesRopeMan
#270 interdiff-736066-265-270.txt2.27 KBGrandmaGlassesRopeMan
#265 736066-265.patch26.45 KBGrandmaGlassesRopeMan
#265 interdiff-736066-259-265.txt2.43 KBGrandmaGlassesRopeMan
#259 interdiff-255-259.txt856 byteseffulgentsia
#259 736066-259.patch26.51 KBeffulgentsia
#255 interdiff-246-255.txt5.23 KBeffulgentsia
#255 736066-255.patch26.49 KBeffulgentsia
#246 736066-ajax-246-interdiff.txt8.44 KBtim.plunkett
#246 736066-ajax-246.patch30.29 KBtim.plunkett
#246 736066-ajax-246-interdiff.txt8.44 KBtim.plunkett
#243 interdiff-ajax.patch2.8 KBdroplet
#243 736066-ajax-243.patch29.3 KBdroplet
#238 736066-ajax-238.patch29.62 KBdroplet
#238 interdiff.patch10.73 KBdroplet
#234 736066-ajax-234.patch27.21 KBtim.plunkett
#234 736066-ajax-234-interdiff.txt8.6 KBtim.plunkett
#233 736066-233.patch27.45 KBGrandmaGlassesRopeMan
#233 interdiff-736066-230-233.txt2.79 KBGrandmaGlassesRopeMan
#230 736066-230.patch27.42 KBGrandmaGlassesRopeMan
#230 interdiff-736066-220-230.txt927 bytesGrandmaGlassesRopeMan
#220 interdiff.txt5.41 KBnod_
#220 core-ajax-wrap-736066-220.patch27.42 KBnod_
#213 736066-212.patch26.4 KBGrandmaGlassesRopeMan
#210 Patch_failed_202.patch.png73.12 KBdinesh18
#208 ajax_js_insert_command-736066-208.patch26.45 KBdroplet
#208 interdiff.patch2.62 KBdroplet
#202 ajax_js_insert_command-736066-202.patch25.81 KBdroplet
#202 interdiff.patch4.87 KBdroplet
#200 core-ajax-wrap-736066-200.patch25.77 KBdroplet
#198 core-ajax-wrap-736066-198.patch28.87 KBdroplet
#198 interdiff.patch11.12 KBdroplet
#194 core-ajax-wrap-736066-194.patch23.92 KBnod_
#193 interdiff-191-193.txt5.81 KBnod_
#193 core-ajax-wrap-736066-193.patch19.48 KBnod_
#191 core-ajax-wrap-736066-191.patch18.07 KBnod_
#191 interdiff-186-191.txt1.8 KBnod_
#186 interdiff-736066-183-186.txt8.14 KBGrandmaGlassesRopeMan
#186 736066-186.patch17.67 KBGrandmaGlassesRopeMan
#183 interdiff-736066-181-183.txt2.31 KBGrandmaGlassesRopeMan
#183 736066-183.patch16.32 KBGrandmaGlassesRopeMan
#181 interdiff-172-181.txt8.32 KBnod_
#181 core-ajax-wrap-736066-181.patch13.84 KBnod_
#172 interdiff-between-169.patch7.48 KBdroplet
#172 736066-172.patch14.16 KBdroplet
#169 736066-169.patch12.46 KBdroplet
#169 interdiff.patch2.45 KBdroplet
#165 interdiff.txt8.74 KBGrandmaGlassesRopeMan
#165 736066-165.patch13.57 KBGrandmaGlassesRopeMan
#163 736066-163.patch13.03 KBdmsmidt
#163 interdiff-736066-162-163.txt819 bytesdmsmidt
#162 736066-162.patch13.12 KBdmsmidt
#162 interdiff-736066-160-162.txt1.74 KBdmsmidt
#160 736066-160.patch12.96 KBdmsmidt
#160 interdiff-736066-156-160.txt10.59 KBdmsmidt
#157 736066-156-SHOULD_FAIL_new_test.patch9.78 KBdmsmidt
#157 interdiff-736066-153-156.txt3.49 KBdmsmidt
#153 interdiff.txt451 bytesGrandmaGlassesRopeMan
#153 736066-153.patch9.17 KBGrandmaGlassesRopeMan
#151 736066-151.patch9.17 KBdmsmidt
#148 interdiff-736066-137-148.txt6.88 KBdmsmidt
#148 736066-148.patch9.22 KBdmsmidt
#146 interdiff-736066-137-146.txt600 bytesdmsmidt
#146 736066-146.patch9.39 KBdmsmidt
#141 c20170306_212107.png20.47 KBdroplet
#139 Screen Shot 2017-03-06 at 11.31.28.png35.52 KBroborew
#139 Screen Shot 2017-03-06 at 11.28.35.png41.55 KBroborew
#139 Screen Shot 2017-03-06 at 11.23.37.png43.59 KBroborew
#139 Screen Shot 2017-03-06 at 11.09.00.png41.84 KBroborew
#138 drupal-736066-remove_extra_ajax_div-138-do-not-test-7.x.patch3.91 KBguypaddock
#137 interdiff.txt3.87 KBGrandmaGlassesRopeMan
#137 736066-137.patch9.33 KBGrandmaGlassesRopeMan
#132 interdiff.txt2.66 KBGrandmaGlassesRopeMan
#132 736066-132.patch8.58 KBGrandmaGlassesRopeMan
#127 interdiff.txt1.41 KBGrandmaGlassesRopeMan
#127 736066-126.patch8.11 KBGrandmaGlassesRopeMan
#125 736066-125-ajax-insert-tests.patch6.61 KBtedbow
#119 ajax-736066-119.patch2.3 KBdtamajon
#116 ajax_js_insert_command-736066-116.patch2.55 KBadinac
#106 736066_56_Reroll_8.2.x.patch666 bytesVinayLondhe
#103 ajax_js_insert_command_736066_103.patch2.52 KBajalan065
#101 ajax_js_insert_command-736066-101.patch2.54 KBmorsok
#99 ajax_js_insert_command-736066-99.patch2.6 KBmorsok
#96 736066-98-do-not-test.patch765 bytesdrintios
#95 unwrap-with-debug-message-do-not-test.patch2.3 KBdroplet
#81 ajax-insert-736066-80-do-not-test.patch2.52 KBtic2000
#80 after-ajax.png47.27 KBhog
#80 before-ajax.png32.92 KBhog
#78 ajax-insert-p77.png23.02 KBtic2000
#78 ajax-insert-core.png23.17 KBtic2000
#77 D7-ajax-do-not-test.patch2.46 KBdroplet
#76 D7-ajax-do-not-test.patch8.17 KBdroplet
#68 736066-interdiff-67-68.txt2.8 KBtic2000
#68 736066-68.patch8.02 KBtic2000
#67 736066-interdiff-64-66.txt998 bytestic2000
#67 736066-66.patch5.22 KBtic2000
#64 736066-64.patch5.18 KBtic2000
#58 736066-56.patch660 bytesericduran
#43 ajax-js-avoid-hardcoded-divs.patch789 byteshrmoller
#30 ajax-commandinsert-736066-30.patch2.07 KBeffulgentsia
#26 ajax-commandinsert-736066-26.patch1.96 KBeffulgentsia
#24 ajax-commandinsert-736066-24.patch1.64 KBeffulgentsia
#22 ajax-commandinsert-736066-22.patch1.75 KBeffulgentsia
#21 ajax-commandinsert.patch2.82 KBcasey
#19 test-ajax-insert.zip1.5 KBcasey
#15 ajax-commandinsert.patch2.35 KBcasey
#4 ajax_wrapping_div-736066-4.patch1.8 KByched
remove_safari_bug_check.patch809 bytespeterpoe

Comments

casey’s picture

+1 We don't need to solve bugs of browsers nobody uses any more.

yched’s picture

aspilicious’s picture

Status: Needs review » Reviewed & tested by the community

Well I agree. And google recently announced dropping the support for chrome 3.0 so chrome is definitely safe. And I couldn't find any recent browser statistics about safari 3.0, so that version must be dead by now :)

yched’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new1.8 KB

After some testing, var new_content = $(response.data); does not work when response.data is some HTML with a text node at the toplevel :

- response.data = '<span>foo</span>bar'
--> only <span>foo</span> gets inserted

- response.data = '<span>foo</span>bar<span>baz</span>'
--> JS execution stops during new_content.hide(); a couple lines below.

It does seem like a wrapping tag is needed. In this case, we should make sure it is removed when we're done inserting and applying effects.

Attached patch does that.

yched’s picture

Component: javascript » ajax system

recategorize

katbailey’s picture

At a glance, and without having tested it at all, it looks like this will cause problems when Drupal.attachBehaviors is called. As its first parameter it's expecting a jQuery wrapped set to use as the context, but it could end up getting something like <span>foo</span>bar<span>baz</span>. If I'm right, then you'll need to defer removing the temporary wrapper until after Drupal.attachBehaviors is called. I'll try and test if I get a chance later today...

seutje’s picture

is there any reason we can't retain the wrapper and make it's display property match the parent's?

sun’s picture

I was under the assumption that top-level text nodes were fixed/supported in jQuery 1.4 now, but it seems I got that wrong.

If I'm not mistaken, then we have another issue regarding #wrapper in the queue that might be closely related.

yched’s picture

@seutje : there are contexts where the mere presence of the div messes your layout :

<table>
  <tr>
    <td>...</td>
    <div><td>...</td></div> <!-- FAIL -->
    <td>...</td>
  </tr>
  <div> <!-- FAIL -->
    <tr>
      ..
    </tr> 
  </div>
</table>

It can also cause CSS rules to break.

This div just means you don't control the HTML you're outputting, which is really embarrassing.

yched’s picture

@katbailey #6:

If I'm right, then you'll need to defer removing the temporary wrapper until after Drupal.attachBehaviors is called.

Hm. Animations being asynchronous, removing the div after both animations and behaviours attachment are done might be tricky.
Is the current order (animate, attach behaviors) a requirement, or could we attach behaviors before showing the new content ?

casey’s picture

I think the whole Drupal.ajax.prototype.commands.insert() function is weird. Can't we split it up into the following?

insert() // add response.data into wrapper using .html()
empty() // remove children of wrapper
replace() // replace wrapper using .replaceWith()
remove() // already exists

the method .replaceAll() seems useless in this context; it allows to replace/move the wrapper to response.data (expected to be a selector string)

katbailey’s picture

I'm using the ajax link example from ajax_example module to test this, and the issue of context being passed to Drupal.attachBehaviors is proving difficult to test due to this weirdness: #825318: Drupal.attachBehaviors() is called an extra time, unnecessarily and for document context, when there is no form.
@yched, are you testing this with ajax forms?

yched’s picture

@katbailey: Yes, testing with a form. I have to say I didn't really look closely at the behaviors nor noticed they got fired twice.
I'm not sure it's really related to the current issue, but it does make things a little confusing as to how to properly fix the extra div issue :-/.

effulgentsia’s picture

From #4:

After some testing, var new_content = $(response.data); does not work when response.data is some HTML with a text node at the toplevel :

That's a shame, but if it's true, then I don't think a temporary wrapper that gets removed is the right answer, because of the issue in #6. What about adding an AJAX command setting (or another property attached to the AJAX command object being returned) for letting people turn off the wrapper, so people can do so when they know they're returning a well-formed fragment (like something already in a 'td')?

casey’s picture

StatusFileSize
new2.35 KB

What about this?

Minor regression is that if there are no ".ajax-new-content" elements and an effect is being used, text nodes at top-level will be show immediately (without the effect).

yched’s picture

@eff #14

I don't think a temporary wrapper that gets removed is the right answer, because of the issue in #6 [behaviors]

What about the following workflow ?
- attach behaviours (done as a last step currently)
- fire animations
- remove the wrapping div in a callback at the end of the animations

Animations are asynchronous anyway, so even in current HEAD, behaviours get attached on dom elements while they're being animated. I don't think it would make a difference for behaviors if they were attached *before* the animations ?

casey’s picture

Behaviors might check for visible elements only...

effulgentsia’s picture

+++ misc/ajax.js	12 Jun 2010 10:58:53 -0000
@@ -321,13 +321,22 @@
+    new_content = new_content.contents();

I'm not enough of a JS / jQuery expert to know if this gets us into the trouble that katbailey warns about in #6 when new_content is then used as the context in the attachBehaviors() that follows. How can we find out?

Re #16:
A common use-case for attaching behaviors is wysiwyg editors, which use IFRAMEs. DOM manipulation (like removing a container DIV) of elements containing IFRAMEs causes the IFRAME to reload which messes up the editor, so we should absolutely not do any DOM manipulation after behaviors have been attached.

65 critical left. Go review some!

casey’s picture

StatusFileSize
new1.5 KB
yched’s picture

#19 doesn't answer effulgentsia's remark in #18 : won't behaviors end up with context being a mix of tags and top level text nodes, as pointed in #6 ? - although this hasn't yet been proved to be an actual issue

A couple thoughts :
- would it be valid to pass the 'target' as context for behaviors in all cases ?
(currently, it's the target if method is 'replaceWith' / 'replaceAll', and the content of the target if method is 'html')
- could a solution lie around the idea of adding a wrapper div only if response.data doesn't contain a wrapping tag itself (i.e <div>a</div><div>b</div> or <div>a</div>b<div>c</div>) ?

casey’s picture

StatusFileSize
new2.82 KB
effulgentsia’s picture

StatusFileSize
new1.75 KB

@yched:

could a solution lie around the idea of adding a wrapper div only if response.data doesn't contain a wrapping tag itself

great idea!

@casey: great implementation!

So how about making this issue just that, as in this patch, and putting the effects code cleanup of #21 into a separate issue for further discussion?

yched’s picture

- I'm wondering about the case where content.data has several top-level tags (i.e <div>a</div><div>b</div>). For those, #21 and #22 remove the wrapper as well, and animations and behaviors are attached to a jq object with n elements, instead of currently on the elements as a whole. Shouldn't we also keep the div in those cases ?

- minor : is there a way we could create new_content without the div and add it if needed, rather than adding it and removing it just after that ?
+ when there are no text nodes, children() might be more explicit than content() ?
from http://api.jquery.com/contents :
"The .contents() and .children() methods are similar, except that the former includes text nodes as well as HTML elements in the resulting jQuery object."

effulgentsia’s picture

StatusFileSize
new1.64 KB

With #23.

effulgentsia’s picture

Actually, I'm not sure if #24 works right (see #4).

effulgentsia’s picture

StatusFileSize
new1.96 KB

Tested it and #24 does not work. This goes back to #22 but adds the consideration from #23 that our effects code currently assumes a single element (at least when there isn't an ajax-new-content class in the content).

is there a way we could create new_content without the div and add it if needed, rather than adding it and removing it just after that ?

Not that I can think of. I added a comment in the patch that explains why not. But according to http://api.jquery.com/jQuery/#creating-new-elements, jQuery itself often implements $(HTML_STRING) as creating a temporary DIV and removing it.

when there are no text nodes, children() might be more explicit than content() ?

Well, the if statement must use .contents() since it doesn't know yet if there are text nodes. So while inside the if could be done with .children(), it seems to me that sticking with .contents() makes more sense.

effulgentsia’s picture

I think the code comments in #26 makes this clear, but just wanting to address this question from #23 head on:

and animations and behaviors are attached to a jq object with n elements, instead of currently on the elements as a whole. Shouldn't we also keep the div in those cases ?

This should not be a problem for behaviors (it's possible there are broken behaviors in contrib that rely on the context to be a single element, but that's a broken behavior). But it is a problem for the effects code as it is currently written. We could try to make the effects code more robust, as per #21, but I continue to think that should be a separate issue. #26 takes into account the currently delicate effects code while still solving the primary use-case this issue started with.

yched’s picture

Agreed on the remarks in #26 / #27. Patch looks good to me.
Casey, what do you think ?

casey’s picture

Yes looks good to me too; nice and clear comments. One thing though; I'd add contents() to a var:

    var new_content_contents = new_content.contents();
    if (new_content_contents.length == 1 && new_content_contents.get(0).nodeType == 1) {
      new_content = new_content_contents;
    }
effulgentsia’s picture

StatusFileSize
new2.07 KB
yched’s picture

Status: Needs review » Reviewed & tested by the community

Yay.

casey’s picture

Still applies.

mh86’s picture

this patch helps a lot when working with the ajax system (e.g. for appending another row to a table) and still applies.

dries’s picture

Is this meant to be a permanent solution? It looks like it is a bit of a hack. Maybe we should add a @todo for Drupal 8?

sun’s picture

AFAICT, this is a permanent solution, as long as AJAX accepts any kind of returned value, string, or fully-fledged HTML structure, and as long as the corresponding server-side calling code does not preemptively specify the nature/structure of the delivered content upfront. It is hard to tell whether this could be improved in any way.

merlinofchaos’s picture

http://drupal.org/node/919994 is a dup of this. FYI, this functionality is critical to the CTools D7 port.

sun’s picture

#30: ajax-commandinsert-736066-30.patch queued for re-testing.

dries’s picture

Status: Reviewed & tested by the community » Fixed

Committed to CVS HEAD. Thanks.

Status: Fixed » Closed (fixed)

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

manu manu’s picture

A note to googlers or those who followed the link from misc/ajax.js:501 and that have some trouble inserting several table rows in a HTML table, like this:

$html = '
<tr>
  <td>foo</td>
</tr>
<tr>
  <td>foo</td>
</tr>';

$commands[] = ajax_command_after('table#products-table tbody', $html);  

You will need only one top-level dom element in $html otherwise the rows will be wrapped into a <div> element.

Having several <tbody> is OK, so you can do something like this:

$html = '
<tbody>
  <tr>
    <td>foo</td>
  </tr>
  <tr>
    <td>foo</td>
  </tr>
</tbody>';

$commands[] = ajax_command_after('table#products-table tbody:last', $html);  
OnkelTem’s picture

Status: Closed (fixed) » Active

Well, aren't we care about inserting DIVs into HEAD?

When a stylesheet is returned with the 'insert' command with data like:

"<style type="text/css" media="all">@import url("http://.../sites/all/themes/mytheme/layouts/mylayout/mylayout.css?m5cq3m");</style>
"

this results to wrapping inside the 'div' and placing it into the 'head' element because of this code:

if (new_content.length != 1 || new_content.get(0).nodeType != 1) {
      new_content = new_content_wrapped;
    }

The reason is:

1) In the above example, the style element for some reason is suffixed with a newline, which creates two elements in the new_content variable: style element and text node, so making the if-expression to trigger on .length check (which = 2)
2) If we were get more then 1 stylesheet from a server, then amount of elements are simply more then 1, working out the same condition.

I don't know how bad it is to have divs in head, but I personally dislike it.
I propose not to use DIVs at all, when the wrapper is set to 'head':

if ((new_content.length != 1 || new_content.get(0).nodeType != 1) && wrapper.get(0).nodeName !== 'HEAD') {
    new_content = new_content_wrapped;
  } 
hrmoller’s picture

Status: Needs review » Active
StatusFileSize
new789 bytes

I think the general problem with this code is that it possibly produces invalid HTML.

if ((new_content.length != 1 || new_content.get(0).nodeType != 1)) {
    new_content = new_content_wrapped;
} 

E.g. if your PHP code produces a bunch of <li>-elements to be inserted into an existing <ul>, the <li>'s will get wrapped inside a <div> which is not allowed at top-level in <ol>/<ul>, following the W3-definition

Ideally it should be possible to pass a flag to indicate whether you want your content wrapped or not. That flag could be added to the the response object in PHP and checked for in ajax.js:

PHP:

$append = ajax_command_append('.selector', $html);
$append['no_wrap'] = TRUE;
$commands[] = $append;

ajax_deliver(array('#type' => 'ajax', '#commands' => $commands));

And in ajax.js, line 489:

var new_content_wrapped;
if(response.no_wrap) {
  new_content_wrapped= $(response.data);
} else {
  new_content_wrapped = $('<div></div>').html(response.data);
}
var new_content = new_content_wrapped.contents();

Note: This might be too much of a hacky solution and it should rather be seen as inspiration...

greg boggs’s picture

Status: Active » Needs review

Here's a jQuery work around to remove the invalid divs from a given #selector without affecting the contents.

http://stackoverflow.com/questions/6428254/removing-only-the-parent-elem...

$(document).ajaxComplete(function() {
  var div = $("#selector div");
  var tmp = div.children().clone();
  var parent = div.parent();
  div.remove();
  tmp.appendTo(parent);
}
lmeurs’s picture

Issue summary: View changes

A side effect of the committed patch of #30 is that the inserted wrapping DIV's can get nested endlessly.

In our case we use the Form API's managed file field in a custom form. When uploading or removing a file, file_ajax_upload() returns multiple HTML elements on each AJAX call, these elements get wrapped and the nesting begins.

EDIT1: I think the File module probably should not append extra elements outside the AJAX wrapper, see #2302011: Managed file elements get wrapped + nested on each AJAX file upload.

EDIT2: Why not add ie. the .ajax-new-content-wrapper class to the new_content_wrapped element so it is easier to identify?

dcam’s picture

Version: 7.x-dev » 8.x-dev

The code added by #30 still exists in 8.x. If there is an issue with it then it will have to be fixed there first.

andypost’s picture

Status: Needs review » Needs work
Related issues: +#2245901: Trim trailing EOF whitespace from templates

no patch for 8.0.x

effulgentsia’s picture

Title: ajax.js insert command wraps content in a div » ajax.js insert command sometimes wraps content in a div, potentially producing invalid HTML and other bugs
Priority: Normal » Major
Issue summary: View changes
Issue tags: +JavaScript
Related issues: +#1237012: An empty status message in the AJAX response is added to the commands[] array and added to the DOM as an empty div tag, +#2302011: Managed file elements get wrapped + nested on each AJAX file upload

Raising to Major, because multiple issues are running into different flavors of this same underlying bug, and because per #43, producing invalid HTML is bad.

Adding the "JavaScript" tag for obvious reasons :)

Also updated the summary.

fubhy’s picture

Many false positives on this check:

    if (new_content.length != 1 || new_content.get(0).nodeType != 1) {
      new_content = new_content_wrapped;
    }

can be circumvented by simply str.trim()'ing the response.data to ensure that jQuery does not do weird things in .html() resulting in the creation of empty text nodes (I think this occurs when there is a "\n" at the end or beginning of the response data).

This solved it for me:

    var new_content_wrapped = $('<div></div>').html(response.data.trim());
    var new_content = new_content_wrapped.contents();

it doesn't really fix the underlying problem though...

andypost’s picture

Not sure that trim() is a nice way to clean-up response that could be big enough.
I still think the batter to make that at the server side

guedressel’s picture

The fact that I can't inject multiple javascript files via ajax into a page makes me sad :-(
In commet #42 this side-effect is explained already, here my use case for JavaScript files:

In a ajax callback I add multiple javascript files via drupal_add_js(), which results in a ajax-insert-command with multplie <script> tags. Those are then added to <head>, wrapped in a <div>, and that for not interpreted by my browser.

I'll be happy if any solution makes it into 7.x upstream.

guedressel’s picture

I've to take back my critisism a little: even tough my scripts are wrapped by a <div> they get interpreted by my browser. Hence not so sad anymore :-|

steamx’s picture

Issue tags: +UX, +DX

Just wanted to "push" this issue as nothing has been done to fix this yet. It is highly annoying for me to work with D8 Ajax while theming. As there will be an RC out there soon I'd request at least a workaround e.g. have a constructor parameter for new AjaxCommand(..., $wrapper = FALSE) where $wrapper would decided about doing wild div stuff.

tim.plunkett’s picture

Issue tags: -UX, -DX +DX (Developer Experience)

I don't see how it affects Usability (which is the tag we use, not UX). Fixing the DX tag.

steamx’s picture

In my terms UX => User experience because

tags might break the theme in unexpected ways. However, it's debatable whether this really affects the user or the developer/themer.
andypost’s picture

ericduran’s picture

StatusFileSize
new660 bytes

So an empty div is always being appended now since all render function seem to be returning an \n.

I haven't tracked this down fully but for now he's a hacked up patch.

I'm undecided if the new line should be fixed here or in the theme. This JS is too sensitive right now which is why I'm doing it here. It might also be fair to just removed the need to check if there's multiple elements on the root of the doc. I'll look into this further.

Posting patch for now to move forward on my issue.

andypost’s picture

@ericduran thanx for bumping and idea about trimming with right way!
Please elaborate on

the need to check if there's multiple elements on the root of the doc

That's now obvious that trimming should be done after render, maybe that's cheaper to do at the backend, because some requests could be big.

meaton’s picture

Thank you very much!

zviryatko’s picture

Also it doesn't work when twig.config.debug is enabled.

wim leers’s picture

wim leers’s picture

Issue tags: +blocker

Now effectively confirmed. Which means this potentially blocks BigPipe from being added to Drupal 8.1, see #2469431-204: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts.

tic2000’s picture

StatusFileSize
new5.18 KB

What about this solution?

It allows the code to behave the same if nothing changes. But on top of that allows the developer to set another wrapper or force not using a wrapper at all.

tic2000’s picture

Status: Needs work » Needs review
StatusFileSize
new5.22 KB
new998 bytes
tic2000’s picture

StatusFileSize
new8.02 KB
new2.8 KB

This should fix the tests

spheresh’s picture

Issue tags: +Needs backport to D7

Patch #58 works fine, but it needs backport to D7

tic2000’s picture

I talked with @nod_ on IRC and took a look on jQuery to see what changed since the launch of D7 when was the last time I checked this issue.

Looking at http://api.jquery.com/Types/#htmlString we have

When a string is passed as an argument to a manipulation method such as .append(), it is always considered to be HTML since jQuery's other common interpretation of a string (CSS selectors) does not apply in those contexts.

For explicit parsing of a string to HTML, the $.parseHTML() method is available as of jQuery 1.8.

// Syntax error, unrecognized expression: bye<b>hello</b>
$( "bye<b>hello</b>" ).appendTo( "body" );
 
// Appends bye<b>hello</b>:
$( $.parseHTML( "bye<b>hello</b>" ) ).appendTo( "body" );

Which means we could remove all the code with new_content and new_content_wrapped and do

      // Add the new content to the page.
      new_content = response.data;
      wrapper[method](response.data);

But the issue then is on the following code, not the actual insert. new_content.find('.ajax-new-content') will throw an error on text nodes.

If we do var new_content = $($.parseHTML(response.data)); then Drupal.attachBehaviors(new_content.get(0), settings); will run only for the first node and throw an error if the first node is a text node. We can fix that with:

  new_content.each(function() {
    if (this.nodeName != '#text') {
      Drupal.attachBehaviors(this, settings);
    }
  });

I imagine this can raise some performance concerns. But if you want to insert multiple table rows or list items now you need to send multiple AJAX Commands which would be even slower.

To fix this we have decide what path we take.

My opinion is that we should not wrap in a div. The developer should have full control of what the result of the command is. Of course with freedom should come responsibility too.

Also note that using $.parseHTML() will not be possible in D7.

lokapujya’s picture

something like this:
bye<b>hello</b>
the developer can wrap in a <div> anyway. But it sounds like this:

<td>new row</td>
<td>2nd new row</td>

shouldn't be wrapped in a div (because that would be invalid HTML) Drupal AJAX insert doesn't currently allow it to be not wrapped, but should. However if the <td>'s are not wrapped in a div, I imagine that would that mean that the jQuery effect would happen to each <td> instead of the whole block of newly inserted <td>'s? Although according to #41, you can get around this by wrapping the <td>'s in a <tbody>.

tic2000’s picture

Something like bye<b>hello</b> could be inserted in a span. But that can be wrapped by the developer too.

You can wrap tr in a tbody, not td. And as I said, now the workaround for inserting 2 td's like in your example is to call InsertCommand twice which means everything is run for each td anyway, including the attachment of behaviors.

droplet’s picture

StatusFileSize
new8.17 KB

Can't it ?

EDIT: Ouch, skip this patch

droplet’s picture

StatusFileSize
new2.46 KB

correct patch:

tic2000’s picture

StatusFileSize
new23.17 KB
new23.02 KB

I investigated the new line issue and after I found why it happens I searched d.o and saw that the issues is actually related to this one. #1237012: An empty status message in the AJAX response is added to the commands[] array and added to the DOM as an empty div tag

Regarding #77, that solution can work for D8 too. The "drawback" is that it can expose any badly written behavior implementation.

HTML output in core
HTML output in core

HTML output with patch in #77
HTML output with patch in #77

hog’s picture

Patch #68 tested.

Before ajax:
before-ajax

After ajax:
after-ajax

But i have a question: we really need wrap content in div?

hog’s picture

StatusFileSize
new32.92 KB
new47.27 KB
tic2000’s picture

StatusFileSize
new2.52 KB

A patch using the parent approach. Like I said previously this may make the context become "body" so run for everything on the page exposing behaviors that didn't use .once().

I upload it for testing and review. The comment would need changes for a commit.

andypost’s picture

Issue tags: -blocker +Needs manual testing
effulgentsia’s picture

I haven't manually tested #81 yet, but #73 says:

new_content.find('.ajax-new-content') will throw an error on text nodes.

Is that true? If so, wouldn't #81 trigger that, since that's exactly the code that's running between the removed hunk and added hunk?

tic2000’s picture

In #81 for multiple elements or when the first is a text it sends the parent. Which means instead of creating the div parent we send whatever the parent of the new content is. In the worst case it's "body". Which means if we send 2 table rows the parent will be tbody instead of wrapping them in a div. In 73# I was talking about sending the new content as is.

effulgentsia’s picture

But prior to the patch's new_content = new_content.parent(); line, there's existing code in ajax.js that does:

// Determine which effect to use and what content will receive the
// effect, then show the new content.
if (new_content.find('.ajax-new-content').length > 0) {
  new_content.find('.ajax-new-content').hide();
  new_content.show();
  new_content.find('.ajax-new-content')[effect.showEffect](effect.showSpeed);
}
else if (effect.showEffect !== 'show') {
  new_content[effect.showEffect](effect.showSpeed);
}

So those find()s happen on the sent content, not the parent. Also, if we enter the else block, then we're running an effect on potentially multiple nodes and/or text nodes, and is that ok?

tic2000’s picture

The issue about new_content.find() was when I was doing

  // Add the new content to the page.
  new_content = response.data;
  wrapper[method](response.data);

If I was doing new_content = $($.parseHTML(response.data)) that was not a problem. While I don't parse the HTML in this patch, I also don't remove this part which does the parsing now:

  var new_content_wrapped = $('<div></div>').html(response.data);
  var new_content = new_content_wrapped.contents();
nod_’s picture

The patch works, making body the context sometimes is fine. Now when using the file upload field before and after we still have leftovers <span class="ajax-new-content"> but not they're not nesting infinitely in divs, they just add up at the same level.

Why do we tiptoe around edge cases to begin with? Do we have an exemple of a module that needs whitespaces and textnodes+domelements on the root of the response, and that can't be replaced with something better? I don't see why it's not ok to just say "don't do it anymore" and make a 'text' ajax command that would insert text if needed.

nod_’s picture

Blast from the past where people had to comment to subscribe :þ Turns out I agreed with tic2000 already #1031600-10: AJAX requests cause duplicate, invalid CSS includes and multi-nested response data output, so I'm obviously ok with this solution.

duaelfr’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs manual testing

I just got affected by this issue. It took me a long time to understand what was wrong.
Thanks all for your work. I did the manual testing.

@nod_ I think it's too late to break BC, even on frontend, so let's commit this fix then open another issue to discuss about changing how everything works (for D9, maybe).

catch’s picture

#87/#89 if there are no real examples relying on that behaviour, I think it's OK to change it in a minor release. i.e. the concrete bug in #87 is worth fixing compared to the theoretical regression that might be introduced for code we don't know about.

tic2000’s picture

What is described in #87 can not be solved by this patch in any way. What is described in #87 is most we can do. There the issue is that we have DOM A, we add to it DOM B, and replace DOM A with A + B. Repeat over and over, so we end up with multiple B.

But I took a look at #2588013: <span class="ajax-new-content" style="display:inline-block;"> causes unwanted whitespace and provided a fix there too. There was another solution already provided, so there is even a choice in what approach we take there.

catch’s picture

Status: Reviewed & tested by the community » Needs review

DuaelFR was that an RTBC for #68 or #81?

duaelfr’s picture

Status: Needs review » Reviewed & tested by the community

Sorry. #81 patch is good to me, according to my tests and the reading of #87

catch’s picture

Status: Reviewed & tested by the community » Needs work

OK in that case as #81 says the comment needs updating for the new approach.

droplet’s picture

Sorry! I reviewed it again. I think #85 is a problem for current sites. Anime is changed. Do it in 8.1 is more suitable.
#68 has similar problem but developer can make decision for unexpected anime during development.

What if adding a `unwrap()` as callback ?

** I leaving the debug message there. So if you go to `admin/structure/types/manage/article/form-display` and click config. You can see better ajax result.

- Can we do refactor and break BC for Drupal.AjaxCommands.insert API in D8.1?
- Any popular modules output wired code now?

drintios’s picture

StatusFileSize
new765 bytes

Added unwrap if we have node type 1, if we have other node types we have wrapper, and if we have nothing we just return nothing , this might be a good approach to this issue.

drintios’s picture

Status: Needs work » Needs review
morsok’s picture

Issue tags: +Needs reroll
StatusFileSize
new2.6 KB

Patch in #81 needed a reroll. (there was merge conflicts I hope I did it right).

morsok’s picture

StatusFileSize
new2.54 KB

Previous reroll had errors ($ missing).

morsok’s picture

Status: Needs work » Needs review
ajalan065’s picture

Issue tags: -Needs reroll
StatusFileSize
new2.52 KB

Previous patch was throwing some errors.
Uploading the new patch.

droplet’s picture

this approach (#103) raised by me and rejected by myself. Sorry :(.

Now we have few approaches
1. @droplet #81 (#103)
Problem: Anime effect is changed. After patching, it's apply anime to each div without single parent root

2. @tic2000 #68
Problem: Similiar to #1, But developers know it's problem on coding

3. @droplet #95
Problem: ( no known issues until this momont )

VinayLondhe’s picture

StatusFileSize
new666 bytes

Posting reroll patch for ericduran's 736066-56 patch. Disregard this.

Saphyel’s picture

Status: Needs work » Needs review

Testing the previous patch and @VinayLondhe you didn't follow the file name standards.

dom.’s picture

This also affect views with AJAX exposed filters. Because of the extra div, the views contextual links (for quick edition) are broken after your "Apply" your filters once and does not show anymore.

dom.’s picture

Status: Needs review » Reviewed & tested by the community

Sorry, after test of #106: it appears that the div is no more included, although the "contextual links" does not appear anyway.
So this is RTBC, and the contextual links will go to another issue.

fabianx’s picture

Status: Reviewed & tested by the community » Needs work

I suggest we start with a functional JS test first to get this issue back on track.

Then we can much more easily see which solutions fix the problem and which don't.

adinac’s picture

StatusFileSize
new2.55 KB

Re-roll for ajax_js_insert_command-736066-101.patch.

samuel.mortenson’s picture

+ if ($new_content.length !== 1 || $new_content.get(0).nodeType !== 1) {

This logic doesn't work when theme debugging is enabled, or in other use cases where comments and whitespace are returned alongside a single Node Element. The debug comments will make $new_content.length greater than 1, and $new_content.get(0).nodeType is either Node.TEXT_NODE (newline) or Node.COMMENT_NODE. This makes development difficult as I have to turn off theme debugging while working on any AJAX logic.

I think what we actually want to check for is that there is only one Node.ELEMENT_NODE type element in $new_content, although this logic would require a loop that makes sure that the other siblings are not significant to the user (text nodes with whitespace and comments are probably OK, Node.DOCUMENT* and Node.PROCESSING_INSTRUCTION_NODE are not).

dtamajon’s picture

StatusFileSize
new2.3 KB

I leave a patch with a different approach. It still continues adding the extra DIV, but after the first request it recognizes the previous created DIV and replace it avoiding infinite nested layers.

lokapujya’s picture

I forget why we need a DIV. Why couldn't we just add a class to all the new elements being added? Is it because of something like this:
bye<b>hello</b>

// For legacy reasons, the effects processing code assumes that new_content
// consists of a single top-level element. Also, it has not been

In other words, what legacy reason?

wim leers’s picture

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new6.61 KB

Ok. so here is patch that just adds tests. Write now the tests pass without any changes to ajax.js but that is because I am not sure what desired behavior is.

I didn't write the tests using the dialog system but used a direct call to Drupal.ajax().

The 3 tests cases I thought are response of:

  1. <div>text</div>
  2. not wrapped text
  3. outside-text<div>text</div>

What other test cases should be test for?

Also what should be desired behavior of each?

samuel.mortenson’s picture

@tedbow Based on my comment in #118, there are (at least) two cases that the committed fix did not cover:

  1. <!-- Theme debug comment --><div>Test</div>
  2.     <div>leading whitespace (including newlines)</div>

In both cases, with the current logic, a wrapping <div> is added, even though the HTML string only contains one top-level element that is already wrapped. This is explained a bit more in #118.

GrandmaGlassesRopeMan’s picture

Version: 8.3.x-dev » 8.4.x-dev
StatusFileSize
new8.11 KB
new1.41 KB

@tedbow Thanks for the tests. This, I think, will address the test cases and wrap things correctly.

tedbow’s picture

@samuel.mortenson thanks for the clarification.
Do you think the test I added is good way to test the functionality generally or should be just use use-ajax dialog links?

I can add the additional test cases you mentioned.

samuel.mortenson’s picture

Some review:

  1. +++ b/core/misc/ajax.js
    @@ -1041,9 +1039,14 @@
    +      var $responseDataWrapped = $('<div></div>').html(response.data);
    +      var $new_content = $responseDataWrapped.contents()
    +        .filter(function (index, value) {
    +          return value.nodeType === 1;
    +        }).length === 1 && $responseDataWrapped.contents().length === 1 ?
    +          $responseDataWrapped.children() :
    

    if "response.data" included a text node and a dom element, like "test<b>test</b>", then the returned length from the filter function would be "1", and the response would not be wrapped. I'm not sure what's expected here, but that's an edge case I thought of.

  2. +++ b/core/misc/ajax.js
    @@ -1041,9 +1039,14 @@
    +          $('<div></div>').html(response.data);
    

    I think this could just be $reponseDataWrapped, instead of wrapping the response in another call.

  3. +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js
    @@ -0,0 +1,24 @@
    +  Drupal.behaviors.insertTest = {
    

    Testing with use-ajax links would be a bit simpler than this, I think.

It would be great if we could get test coverage for leading comments and whitespace as well. Thanks for taking this task on!

samuel.mortenson’s picture

#129.1 is incorrect, I missed the $responseDataWrapped.contents().length call.

GrandmaGlassesRopeMan’s picture

@samuel.mortenson Thanks for the review. U+1F44D

I want to possibly circle back to #120 and ask again, why we are even wrapping the response? It feels like we are going to get caught up in a bunch of really complicated logic that will never accurately handle all the possible edge cases that can happen with AJAX'd data.

GrandmaGlassesRopeMan’s picture

StatusFileSize
new8.58 KB
new2.66 KB

At a high level this will sequentially add non-element nodes (text, comment, etc) from an HTML string until an element node (div, p, etc) is found. At that point, the resulting wrapped non-element nodes and the the element nodes are added to $new_content. If there are subsequent after the first element node is encountered, the process continues.

samuel.mortenson’s picture

+++ b/core/misc/ajax.js
@@ -1041,9 +1039,30 @@
+      var wrapper = null;
+      $responseDataWrapped.contents().each(function (index, value) {
+        if (wrapper && value.nodeType !== 1) {
+          wrapper.append(value);
+        }
+        if (!wrapper && value.nodeType !== 1) {
+          wrapper = createWrapper();
+          wrapper.append(value);
+        }
+        if (wrapper && value.nodeType === 1) {
+          $new_content.append(wrapper, value);
+          wrapper = null;
+        }
+      });
+
+      $new_content = $new_content.children();

If $responseDataWrapped.contents() returns a single value with nodeType === 1, $new_content will be empty as "wrapper" is NULL. This should be testable with response like <div>hello</div>.

guypaddock’s picture

With #77 applied in D7, it breaks the admin UI for fields on entities.

GrandmaGlassesRopeMan’s picture

@samuel.mortenson

Yeah. I didn't update the tests to account for the new behavior.

@GuyPaddock

Yeah. I have not been testing this at all in D7. I assume some additional work will need to take place to backport this.

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
StatusFileSize
new9.33 KB
new3.87 KB

Fixes a few edge case issues and updates the tests.

guypaddock’s picture

The error I was seeing in D7 was:

Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
    at Ga (jquery.js?v=1.4.4:130)
    at Function.css (jquery.js?v=1.4.4:128)
    at $.fn.init.hide (jquery.js?v=1.4.4:147)
    at Object.insert (ajax.js?v=7.54:536)
    at Drupal.ajax.success (ajax.js?v=7.54:428)
    at Object.success (ajax.js?v=7.54:189)
    at Object.a.success (jquery.form.js?v=2.52:12)
    at Function.handleSuccess (jquery.js?v=1.4.4:143)
    at XMLHttpRequest.w.onreadystatechange (jquery.js?v=1.4.4:142)

This affects selecting Media in the media browser for an entity that has no images yet, as well as when trying to rearrange fields in Fields UI. It appears that the case it's running into is a case where the content being added ends up being text DOM nodes, which makes the window the target.

The attached patch is a variant of 77 that works around the issue, but seems to break effects.

roborew’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new41.84 KB
new43.59 KB
new41.55 KB
new35.52 KB

Test for failure, this code currently causes the issue described, with the ajax wrapper breaking a select list.


    foreach ($options as $key => $option) {
      $html .= '&lt;option value=&quot;&#039; . $key . &#039;&quot;&gt;&#039; . $option . &#039;&lt;/option&gt;';
    }

    $response = new AjaxResponse();
    $response->addCommand(new HtmlCommand($element_class, $html));
    return $response;

Un-commenting the optgroup tag fixes the issue, but then causes problems when using the select list with the chosen module.

Post Patch Tests:
With wrapper already set:


    $html =&#039;&lt;optgroup&gt;&#039;;
    foreach ($options as $key => $option) {
      $html .=&#039;&lt;option value=&quot;&#039; . $key . &#039;&quot;&gt;&#039; . $option . &#039;&lt;/option&gt;&#039;;
    }
    $html .=&#039;&lt;/optgroup&gt;&#039;;
    $response = new AjaxResponse();
    $response->addCommand(new HtmlCommand($element_class, $html));
    return $response;

Return without wrapper:


    foreach ($options as $key => $option) {
      $html .=&#039;&lt;option value=&quot;&#039; . $key . &#039;&quot;&gt;&#039; . $option . &#039;&lt;/option&gt;&#039;;
    }
    $response = new AjaxResponse();
    $response->addCommand(new HtmlCommand($element_class, $html));
    return $response;

Return string:


    $html = 'Returning string should be wrapped';

    $response = new AjaxResponse();
    $response->addCommand(new HtmlCommand($element_class, $html));
    return $response;

Patch works with the expected behaviour for the three scenarios above. Will set this to review and tested, but realise there might be tests required that i might have missed outside of my use case, happy to take further guidance and test more scenarios as needed.

droplet’s picture

Status: Reviewed & tested by the community » Needs review

Having a quick review. It looks very similar / same as #103 but with extra redundant conditions?

Anyway, the content will be lost if data returned something like #95

var new_content_wrapped = $('<div class="test"></div>').html('BEGIN<div style="height: 100px; font-size: 30px">test</div><div style="height: 100px; font-size: 30px">test</div>END');

Missing END

droplet’s picture

StatusFileSize
new20.47 KB

Also, the same anime issue as I summarized in #104 ?

(If we allowed let it break in D8.3/4, is #103 ready to go?)

GrandmaGlassesRopeMan’s picture

#139

Patch works with the expected behaviour for the three scenarios above. Will set this to review and tested, but realise there might be tests required that i might have missed outside of my use case, happy to take further guidance and test more scenarios as needed.

Was this tested using the patch from #137?

#140

Anyway, the content will be lost if data returned something like

var new_content_wrapped = $('<div class="test"></div>').html('BEGIN<div style="height: 100px; font-size: 30px">test</div><div style="height: 100px; font-size: 30px">test</div>END');

I believe that the patch from #137 would account for this case and not drop the END text.

roborew’s picture

Hey @drpal, yes tests in #139 were made against your patch #137.

dmsmidt’s picture

Status: Needs review » Needs work

I've tested #137 in combination with the Refreshless module. Now I get an empty divs before the inserted content instead of a wrapping div.

The problem stems from indentation. If the response.data contains for example a space before the a div, we get an extra empty div.
We should probably skip text containing only whitespace or linebreaks.

dmsmidt’s picture

Status: Needs work » Needs review
StatusFileSize
new9.39 KB
new600 bytes

Something like this works in my case (would need test).

Edit: argggg typ0, working on version with tests

dmsmidt’s picture

Status: Needs work » Needs review
StatusFileSize
new9.22 KB
new6.88 KB

Here is a better version, it also has an extra test for the leading white space case.
Note, I rewrote the test a bit so that it runs faster (less page requests).

As mentioned earlier, we still haven't figured out what to do with theme debug comments. These get wrapped in a div now.
It could also be a follow up IMHO.

droplet’s picture

one leading or trailing SPACE is validated we cannot trim it?

dmsmidt’s picture

Status: Needs work » Needs review
StatusFileSize
new9.17 KB

@droplet, what do you mean?
Are you saying we should allow leading and trailing spaces?
I think it causes too many problems, as can be seen in the Refreshless module. If someone really wants a leading or trailing space in an unwrapped response, he/she could add &nbsp;.

New patch: Removed debug line.

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
StatusFileSize
new9.17 KB
new451 bytes

Fixes a minor nit with checking for length from #151. Additionally, I think it's probably a good to handle leading whitespace in this way. U+1F44D

droplet’s picture

Hmm. What behaviors do we expect and what will change in the current patch?

For example:

In patch #153

BEGIN <div>testing</div> END

Became:

<div>BEGIN </div><div>testing</div>

`BEGIN ` was an INLINE element, now this is BLOCK level. And DIV can be styled with CSS.

Comment in the current code:

we check if the new content satisfies the requirement of a single top-level element

Is it still true?
Assumed this is FALSE, then we need not do anything with `response.data`?

dmsmidt’s picture

Status: Needs review » Needs work

@droplet,
So what I understood about wrapping the TEXT_NODE / COMMENT_NODE in a div is that we do that because it is needed for animations. My preference would be that we don't wrap (we don't actually need it for comment nodes), but..

We have a bit of a deadlock here:
1. We don't want to change the DOM structure when replacing content, because that can break CSS (.some-class > .my-div). And can lead to heavy divitis (for example with the Refreshless module).
2. We don't want to really wrap text nodes and comment nodes, however it is needed for animation.
3. Currently when the return data consists out of multiple nodes, animation can be weird. For example if you use the slide animation, multiple items will slide instead of one (the later is the current behavior).

We also can't just temporarily wrap multiple nodes so that we have a single animation, and then unwrap after the animation, because styling could in that case break during animation.

My preference would be that we don't try to compensate for multiple animations, in the cases that for example a single slideToggle is needed the returned data should just be wrapped in a single block element. That is up to the implementer.

Even so, the last patch is still not complete.
The code below is responsible for attaching behaviors to the new content.

        Drupal.attachBehaviors($new_content.get(0), settings);

This only works for a single item.

So when, for example (with the latest patch) we turn theme debugging on, we only attach behaviors for the newly inserted theme debug comment. Ouch!

GrandmaGlassesRopeMan’s picture

@dmsmidt

Would it be possible for you to update the tests to provide an example where this fails?

dmsmidt’s picture

Status: Needs work » Needs review
StatusFileSize
new3.49 KB
new9.78 KB

@drpal sure, here is a patch with an additional test on top of what we already have.

dmsmidt’s picture

Assigned: Unassigned » dmsmidt

So that failed as expected, working on a patch fix.

dmsmidt’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests +Needs JavaScript review
StatusFileSize
new10.59 KB
new12.96 KB

Attached patch fixes the 'context' problem mentioned above. And tries to fix earlier mentioned problems as well.

So, to answer some questions: text nodes should not be passed to attachBehaviors since jQuery could interpret them as selectors, so we should still wrap those (sadly enough).
Currently we wrap them in a <div>, this can be destructive for a design since we return a block level item. Returning a should be less destructive to designs, and still gives the necessary design possibilities on inserted elements for themers. This has been changed in the given patch. I think this is not a BC break in practice. I also updated the tests to reflect this, and changed some typ0's.

Furthermore, comments should never be wrapped. We also don't need them in 'context' when attaching behaviors. Not wrapping them has huge benefits for when theme debugging is turned on. I changed the patch accordingly and added a test case for that as well.
The wrapping logic has been optimized in the same go. Instead of an integer I use the 'Node.COMMENT_NODE' constant for type checking. This is more readable and works in IE9+.

Lastly I also made the test a bit stricter, now it is checked that the target div is directly wrapping the new data (to ensure no other divs/spans are added).

GrandmaGlassesRopeMan’s picture

@dmsmidt

Nice work. Thanks for handling that.

+++ b/core/misc/ajax.js
@@ -1027,22 +1027,52 @@
+      var elementsReturned = $responseDataWrapped.contents().length;

Just a slight concern about the variable name here since it's just the number of elements.

dmsmidt’s picture

StatusFileSize
new1.74 KB
new13.12 KB

@drpal, improved var naming.

Also I found that this wasn't enough when we insert new elements with the replaceWith method.
We should test the different insert techniques that we support.
This version at least tries to make it work with 'replaceWith' next to 'html'.

However.. I don't know what will happen if we use 'replaceWith' with multiple elements.. Needs some work.

dmsmidt’s picture

Assigned: dmsmidt » Unassigned
Issue tags: +Need tests
StatusFileSize
new819 bytes
new13.03 KB

Scary that those tests pass, since I have some test sites that break hard. Thus adding 'need tests'.
At least the following patch works on an AJAX heavy test site (Refreshless + AJAX views).

I probably don't have time in the next days to work on the needed tests.
The currently added tests in this patch work with the 'html' method. We need to check that context is alright for the different inserting techniques ('replaceWith', 'append', 'prepend', 'before', 'after', or 'html').
Of those I think 'replaceWith' is the most important. The others probably work like 'html'.

pavelculacov’s picture

last patch test, work perfect.
after apply patch, on custom block where get rendered View and commerce variante where after ajaxReplace appear empty div.

Thanks @dmsmidt

GrandmaGlassesRopeMan’s picture

StatusFileSize
new13.57 KB
new8.74 KB

Refactored tests, adding support for replaceWith.

dmsmidt’s picture

I like the rewrite. One discussion point:

+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -82,4 +95,70 @@ public function testDrupalSettingsCachingRegression() {
+    foreach (['html', 'replaceWith'] as $method) {
+      foreach ($test_cases as $test_case) {
+        $this->drupalGet('ajax-test/insert');

This possibly makes testing slower, I'm not sure how significant it is, but we could easily skip it for the 'html' method. For 'replaceWith' it is necessary since the #wrapper will be gone after the first replace.
We could also generate different wrappers for each test type and method. Than we just need to request only a page once.
Any thoughts?

GrandmaGlassesRopeMan’s picture

@dmsmidt

Thanks. I don't really have an opinion other than it's nice to have more complete coverage.

wim leers’s picture

needs JavaScript review was done by @drpal. Test coverage is present, and looks rather comprehensive.

So… is this then RTBC again?

droplet’s picture

StatusFileSize
new2.45 KB
new12.46 KB

Hmm. with the new behavior, I'm interested what I'm missed in below patch or the test cases don't catch.

(Patch should be failed. I didn't trim the SPACE and remove SPAN in tests)

EDIT:
Just realized the code around isAllElementNode conditional can be simplified. (But with the same logic. I will update if it's right approach)

droplet’s picture

With #169,

we able to do further and wrap "NODE_ELEMENT only" response with DIV.

Any "NODE_ELEMENT only" response matches:

DIV, SPAN, UL, A... 
// (few more I think, will find it out later)
// whitelist better than blacklist I think..

It must be safe to ADD A WRAPPER. doesn't it?

droplet’s picture

Status: Needs work » Needs review
StatusFileSize
new14.16 KB
new7.48 KB

- Fixed failure tests
- Enhanced the tests
- It seems big_pipe will sent out empty responses.
- In this patch, I introduced a little idea to keep inline element. But the result is the dimension of parent SPAN will be wrong and little semantic problem. However, it will keep single-parent-element result.

+++ b/core/misc/ajax.js
@@ -1031,21 +1035,26 @@
+        if (responseDataWrappedContents.length === 1 || ($wrapper.css('display') !== 'block')) {
+          $new_content = $('<span class="ajax-inline"></span>').html(response.data);
dmsmidt’s picture

@droplet I fear you want to over-optimize, and the big rewrites are a bit hard to follow without a bit more explanation.
We have a working patch in #165, could you tell what your problems are with that approach? Did you actually find problems with that patch?
I'm up for improvements, but it would be great if you could explain them a bit more elaborate so that I can follow.

In #160 I explained the considerations for the last changes before your version. The 'inline-element' styling trick you introduce in #172 I find a bit too much, and out of scope. We already swapped

with for text nodes only, this way the layout flow isn't as destructive anymore as previously.
droplet’s picture

@dmsmidt,

To my understanding, your patches does not fulfill the deadlock your told in #155. But for some reason (technical?), it changed the way to render things.

#165 changed:
1. Adding SPAN to TEXT_NODE
2. Anime effect is changed.
3. No more single-parent root.

#172 fixed the bugs and keeps no (lesser) BC layer changes:
1. the dimension of parent SPAN will be wrong and little semantic problem. (Note: rendering is correct.)
2. (Basically, anime is same as now)
3. Still no single-parent root with some cases but we could do it. @see #170

with for text nodes only, this way the layout flow isn't as destructive anymore as previously.

Could you update the test case to make it fail. Thanks.

(Anyway, both patches have their Pros and Cons. I really want to find out if we give it up because of the technical issue or we really want things working that way.)

dmsmidt’s picture

@droplet, while making the patch of #165 I took the 'deadlock' issues into consideration, and thought about what would be the best way forward.

1. We don't want to change the DOM structure when replacing content, because that can break CSS (.some-class > .my-div). And can lead to heavy divitis (for example with the Refreshless module).

The patch ensured minimal DOM structure changes, this is the biggest win! Indeed only for root text nodes a SPAN is added. The span is less destructive than DIVS for the layout. We need a wrapper, otherwise we can't pass it as behavior context.

2. We don't want to really wrap text nodes and comment nodes, however it is needed for animation.
3. Currently when the return data consists out of multiple nodes, animation can be weird. For example if you use the slide animation, multiple items will slide instead of one (the later is the current behavior).

See above, we do need to wrap text nodes, also for context. Animations I'm not that concerned with anymore. If someone wants a consistent single animation, he/she should wrap the return data in a single node element.

Currently AJAX inserts/replaces break the DOM structure. This is more problematic than the rare cases that only root text nodes are returned or the combination of multiple root elements in the return data with a slide effect. I haven't really encountered those edge cases in the wild, but in case they do, functionality doesn't break, only the animation is less pretty.

My questions about #172:

1. the dimension of parent SPAN will be wrong and little semantic problem.

We probably have a language barrier between us, because I don't really understand this ;-) Could you help me?

2. (Basically, anime is same as now)

How can this be? If we want to keep animation as how Drupal works now, we always need a single root element.
One single parent root element is just the things we want to prevent in this patch.
For example, if someone is appending items to a list of items via AJAX, every time we append, there will be an extra nesting (in case we use a single root parent element. This is problematic.

3. Still no single-parent root with some cases but we could do it. @see #170

See the above.

Does this answer your question about finding out why certain approaches where used?

wim leers’s picture

This was RTBC in #140, on March 4, more than 2 months ago.

Can we please continue here? This has been causing strange bugs for years, and has been causing problems for BigPipe users in particular too: #2738685: BigPipe leaves wrapper <div>s in place, because AJAX system leaves them — this can cause some CSS to break.

dmsmidt’s picture

#165 was the last completely functional patch as fas as I know. #140 didn't take care of multiple things (the discussion after that is not completely useless). Both @drpal and me are happy with #165, and thought it RTBC worthy. @droplet, then did a major rewrite, but never got passing tests.
Still waiting for his reply for over a month. I'd like to have this fixed a.s.a.p., but I guess we need another person to review the two versions and have an opinion about it.

nod_’s picture

Assigned: Unassigned » nod_

reviewing

wim leers’s picture

\o/ Thank you, @nod_!

nod_’s picture

Assigned: nod_ » Unassigned
StatusFileSize
new13.84 KB
new8.32 KB

Both patches had issues, but droplet had the simplest one. The approach was pretty much the same since parseHTML was doing what the previous patch tried to.

Tests pass and more importantly it fixes the issue with the image upload widget.

In any case there are some strange things going on in testing: the test ajax command return 2 command, the 'html' or 'replace' command and a second one with 'prepend'. Don't know where that second one comes from but it's strange.

I did cheat on one test, since the response is trimmed one got rid of one space in the expected data in the test.

Put back the detachbehavior on the wrapper, no idea why that got changed along the way.

My questions about #172:

1. the dimension of parent SPAN will be wrong and little semantic problem.

We probably have a language barrier between us, because I don't really understand this ;-) Could you help me?

He means it's possible for <div> to end up inside of <span> like with the inline exemple.

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
StatusFileSize
new16.32 KB
new2.31 KB

Fixes the tests from #181, since we don't return a div anymore.

dillix’s picture

Tested drpal's patch, +1 for RTBC

dmsmidt’s picture

Status: Needs review » Needs work

Some nits and questions.

  1. +++ b/core/misc/ajax.js
    @@ -1027,22 +1027,42 @@
    +      var $new_content;
    

    Please use lowerCamelCased.

  2. +++ b/core/misc/ajax.js
    @@ -1027,22 +1027,42 @@
    +      // If only NODE_ELEMENT or COMMENT_NODE elements are returned skip wrap processing.
    

    Exceeds 80 chars.

  3. +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js
    @@ -0,0 +1,40 @@
    + * Provides method to test ajax requests.
    

    Can we be a bit more specific with the description? Also let ppl know we are testing that attaching Behaviors work, this was a simple but important addition I think!

  4. +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js
    @@ -0,0 +1,40 @@
    +      $('.ajax-insert').once('ajax-insert').on('click', function (event) {
    ...
    +      $('.ajax-insert-inline').once('ajax-insert').on('click', function (event) {
    

    We have some code repetition here (only the wrapper changes).

  5. +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js
    @@ -0,0 +1,40 @@
    +      $(context).addClass('processed');
    

    Add a comment to let others know this is for testing if behaviours work well.

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -82,4 +95,76 @@ public function testDrupalSettingsCachingRegression() {
    +      $assert->assertWaitOnAjaxRequest();
    ...
    +      $assert->assertWaitOnAjaxRequest();
    

    Shouldn't we be replacing these for the assertions that waits for an element?

  7. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -82,4 +95,76 @@ public function testDrupalSettingsCachingRegression() {
    +      // Extra span added by a second prepend command on the ajax requests.
    +      $assert->responseContains($this->wrapAjaxTarget('<span class="processed"></span>' . $test_case['expected']));
    

    Help, where and why does this happen again?

GrandmaGlassesRopeMan’s picture

StatusFileSize
new17.67 KB
new8.14 KB

From #185

  • 1 - Fixed
  • 2 - Fixed
  • 3 - Fixed, although we aren't explicitly testing behaviors here.
  • 4 - I think it's probably better to have some more readable code, even if it is somewhat duplicated.
  • 5 - Again, not actually testing behaviors.
  • 6 - We'd probably be dependent on having an expected locator, which might match the previous test case giving us a false positive.
  • 7 - We don't actually need the span, removed.
GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
dmsmidt’s picture

Thanks again.

4: ok!

Concerning 3&5: now it is not clear at all why we add the 'processed' class.
I added it because before this patch we just had one node element passed to attachBehaviors, but now we can have multiple. And we want to be sure that they all have been the 'context' for attachBehaviors.
Somehow we should make that clear at least.
I think it is currently the closest thing to behaviors testing we have (although still very simplistic).

didebru’s picture

#186 does not work for 8.3.1 . Although the file 8.4.x ajax.js and the 8.3.x/1 ajax.js are identical... The patch could not be applied.

nod_’s picture

yes, this is the usual process. We fix the bug in the dev version, which is 8.4, and if we think it's possible or relevant to backport we do that after it it fixed in the dev version. For now we focus on 8.4.

nod_’s picture

StatusFileSize
new1.8 KB
new18.07 KB

Just for the record, #1 is scope creep, this is not a clean-up patch.

We seem to be missing a kind of test:

<div>top-level node #1</div><div>top-level node #2</div>

About 3&5 it's less about testing behaviors than making sure attach behaviors are called when ajax content gets inserted. We're already assuming they work. And since we had one type of test missing it didn't make as much sense since we were only dealing with one top-level element in all the other cases.

wim leers’s picture

I'm so very tempted to RTBC this, this seems so very close! Leaving that to those who've been closely involved lately though. Just wanted to say I'm cheering from the sidelines to see this very tricky, nasty bug nearing its final breath!

nod_’s picture

StatusFileSize
new19.48 KB
new5.81 KB

Did a bit or refactor to simplify the code a little. We have tests so it's fine, don't worry about it :þ

Remove the 'hack' for the mixed content where the leading whitespace was removed, inserted as-is now.

Another edge case, make sure that whitespaces in the middle of top-level-only nodes wrap the content:

<div>top-level node #1</div>      <div>top-level node #2</div>

I think we covered everything important?

nod_’s picture

StatusFileSize
new23.92 KB

Reroll for ES6.

droplet’s picture

Oh, we almost patched the whole insert function now. Here's few suggestions (can do in follow-up also):

1. Use ES6 directly in insert function. no var, use const...etc

2.
settings = response.settings || ajax.settings || drupalSettings;
move 2 settings to top.

3. caching $newContent.find('.ajax-new-content')

4. ideally, new JS file should be ES6 ready \core\modules\system\tests\modules\ajax_test\js\insert-ajax.es6.js

oriol_e9g’s picture

Minor:

-    $green_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')");
+    $green_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color span:contains('green')");
 
... 

-    $red_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('red')");
+    $red_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color span:contains('red')");

... etc

If we have an span instead of a div the var name should not be: $green_div, $red_div, etc.

wim leers’s picture

#195: let's do that in a follow-up indeed. Let's first finally land this! :)

#196: nice nitpicks!

droplet’s picture

StatusFileSize
new11.12 KB
new28.87 KB

Let me reroll it for real quick. Actually, the ES6 give us a clean overview on scopes.

- Reroll
- ES6 syntax on insert and new file
- Fixed #195
- Fixed #196
- Fixed typo.

droplet’s picture

Status: Needs work » Needs review
StatusFileSize
new25.77 KB

Same as #198 but remove a non relevant file

GrandmaGlassesRopeMan’s picture

  1. +++ b/core/misc/ajax.es6.js
    @@ -1011,38 +1011,69 @@
    +      const onlyElementNodes = responseDataNodes.every(function (element) {
    

    =>

  2. +++ b/core/misc/ajax.es6.js
    @@ -1052,36 +1083,39 @@
    +        $newContent.each(function () {
    

    =>

Some minor nits about arrow functions.

droplet’s picture

StatusFileSize
new4.87 KB
new25.81 KB

Thanks @drpal. I thought to keep it simple so didn't update functions with this keyword.

@seee new patch. Even cleaner code now :) We know where the `this` from.

dinesh18’s picture

Status: Needs review » Needs work
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 6b15204a26..65659e0f53 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -472,46 +472,74 @@
 
   Drupal.AjaxCommands = function () {};
   Drupal.AjaxCommands.prototype = {
-    insert: function insert(ajax, response, status) {
+    insert: function insert(ajax, response) {
       var $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
       var method = response.method || ajax.method;
       var effect = ajax.getEffect(response);
-      var settings;
 
-      var $new_content_wrapped = $('<div></div>').html(response.data);
-      var $new_content = $new_content_wrapped.contents();
+      var settings = response.settings || ajax.settings || drupalSettings;
+
+      var $newContent = void 0;
+
+      var trimmedData = response.data.trim();
+
+      var responseDataNodes = void 0;
 
-      if ($new_content.length !== 1 || $new_content.get(0).nodeType !== 1) {
-        $new_content = $new_content_wrapped;
+      if (trimmedData === '') {
+        responseDataNodes = [document.createTextNode(response.data)];
+      } else {
+        responseDataNodes = $.parseHTML(trimmedData, true);
       }
 
+      var onlyElementNodes = responseDataNodes.every(function (element) {
+        return element.nodeType === Node.ELEMENT_NODE || element.nodeType === Node.COMMENT_NODE;
+      });
+
+      if (onlyElementNodes) {
+        $newContent = $(trimmedData);
+      } else {
+          if (responseDataNodes.length === 1 || $wrapper.css('display') !== 'block') {
+            $newContent = $('<span></span>');
+          } else {
+            $newContent = $('<div></div>');
+          }
+
+          $newContent.html(response.data);
+        }
+
       switch (method) {
         case 'html':
         case 'replaceWith':
         case 'replaceAll':
         case 'empty':
         case 'remove':
-          settings = response.settings || ajax.settings || drupalSettings;
           Drupal.detachBehaviors($wrapper.get(0), settings);
+          break;
+        default:
+          break;
       }
 
-      $wrapper[method]($new_content);
+      $wrapper[method]($newContent);
 
       if (effect.showEffect !== 'show') {
-        $new_content.hide();
+        $newContent.hide();
       }
 
-      if ($new_content.find('.ajax-new-content').length > 0) {
-        $new_content.find('.ajax-new-content').hide();
-        $new_content.show();
-        $new_content.find('.ajax-new-content')[effect.showEffect](effect.showSpeed);
+      var $ajaxNewContent = $newContent.find('.ajax-new-content');
+      if ($ajaxNewContent.length) {
+        $ajaxNewContent.hide();
+        $newContent.show();
+        $ajaxNewContent[effect.showEffect](effect.showSpeed);
       } else if (effect.showEffect !== 'show') {
-        $new_content[effect.showEffect](effect.showSpeed);
+        $newContent[effect.showEffect](effect.showSpeed);
       }
 

As per the Drupal coding standards we should not use "else if" -- always use elseif.

nod_’s picture

Status: Needs work » Needs review

those are PHP standards :) in js elseif doesn't exists

dinesh18’s picture

oops sorry... my bad :(

nod_’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed, this is good to go.

oriol_e9g’s picture

I can still find #196 nitpiks. The var name and texts like "The selected color DIV is green" should be "The selected color SPAN is green".

$green_span

-    $green_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')");
+    $green_div = $this->assertSession()->waitForElement('css', "#ajax_selected_color span:contains('green')");
     $this->assertNotNull($green_div, 'DOM update: The selected color DIV is green.');

$green_span2

-    $green_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('green')");
+    $green_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color span:contains('green')");
     $this->assertNotNull($green_div2, 'DOM update: After reload - the selected color DIV is green.');

$red_span2

-    $red_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color div:contains('red')");
+    $red_div2 = $this->assertSession()->waitForElement('css', "#ajax_selected_color span:contains('red')");
     $this->assertNotNull($red_div2, 'DOM update: After reload - the selected color DIV is red.');
droplet’s picture

StatusFileSize
new2.62 KB
new26.45 KB
dinesh18’s picture

StatusFileSize
new73.12 KB

I tried to apply the patch #202 and it is failing.
PFA screenshot

wim leers’s picture

#2880007: Auto-fix ESLint errors and warnings landed, which conflicted with this. This needs a reroll.

dinesh18’s picture

Issue tags: +Needs reroll

Added needs Re-roll tag

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new26.4 KB
droplet’s picture

I thought I will mark it RTBC but the logic code contributed by me also. I may not the right person to review it.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC.

nod_’s picture

Status: Needs work » Reviewed & tested by the community

testbot strangeness.

effulgentsia’s picture

Status: Reviewed & tested by the community » Needs work

I really like #213 a lot! Great work, everyone! All I could find is nits.

  1. +++ b/core/misc/ajax.es6.js
    @@ -1002,38 +1002,69 @@
    +      // When there are other types of top-level nodes, the response need to be
    

    s/need/needs/

  2. +++ b/core/misc/ajax.es6.js
    @@ -1002,38 +1002,69 @@
    +        // If response.data contains only one TEXT_ELEMENT if the target element
    +        // is not styled as a block, response.data will be wrapped with SPAN.
    

    s/TEXT_ELEMENT if/TEXT_ELEMENT and/

  3. +++ b/core/misc/ajax.es6.js
    @@ -1002,38 +1002,69 @@
    +        if (responseDataNodes.length === 1 || ($wrapper.css('display') !== 'block')) {
    +          $newContent = $('<span></span>');
    +        }
    

    Clever!

  4. +++ b/core/misc/ajax.es6.js
    @@ -1002,38 +1002,69 @@
    +        else {
    +          $newContent = $('<div></div>');
    +        }
    

    But this means we would wrap hello <em>world</em> in a DIV instead of a SPAN, even if the target element is not a block element, since there's more than one DOM node. Has this been discussed already? Maybe we should open a followup issue for seeing if we can cover that case? And in any case, I think a comment here for why it's ok or linking to the followup issue would be helpful.

  5. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -16,6 +16,19 @@ class AjaxTest extends JavascriptTestBase {
    +   * Wrap HTML with an AJAX target element.
    

    s/Wrap/Wraps/

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -16,6 +16,19 @@ class AjaxTest extends JavascriptTestBase {
    +  protected function wrapAjaxTarget($html) {
    +    return 'data-drupal-ajax-target="">' . $html . '</';
    +  }
    

    Can we add a comment explaining why it's okay that this returns an incomplete/malformed HTML fragment? I.e., that this function is / should only be called by code that only needs to test a substring and not by any callers that expect well-formed HTML.

  7. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -82,4 +95,97 @@ public function testDrupalSettingsCachingRegression() {
    +        'render_type' => 'not-wrapped',
    +        'expected' => '<span class="processed">not-wrapped</span>',
    

    Can we also add a test for the 'not-wrapped' render type when targeted into a block-level element?

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new27.42 KB
new5.41 KB

Fixed all feedback + removed the use strict from the js file (we don't use that since es6).

Can't add a test for 7 since it can't happen, if there is one textnode in the response'll always be a span. we can't have two text nodes from a single string. But I did add a test for when there is a comment + textnode where a wrapping div will be used when a span should have been used.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
droplet’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/misc/ajax.es6.js
@@ -1059,12 +1059,16 @@
+        const hasCommentNodes = responseDataNodes.some(

has some? It seems not right

nod_’s picture

At this point in the code, it doesn't matter if we have 1 or 10 comment nodes, it should be in a span. What is important is that we have at least one comment node with our textnode.

droplet’s picture

Status: Needs work » Needs review

Why has COMMENT_NODE should be SPAN?

nod_’s picture

It's not only about the comment node:

Before:

// 'not-wrapped' added with:
<span class="processed">not-wrapped</span>
// '<!-- COMMENT -->comment-string-not-wrapped' added with:
<div class="processed"><!-- COMMENT -->comment-string-not-wrapped</div>

After (fixed):

// 'not-wrapped' added with:
<span class="processed">not-wrapped</span>
// '<!-- COMMENT -->comment-string-not-wrapped' added with:
<span class="processed"><!-- COMMENT -->comment-string-not-wrapped</span>

No surprises when adding an comment before/after a string in an ajax response.

droplet’s picture

#226 makes sense. but with .some:

<div>test</div><!-- COMMENT -->comment-string-not-wrapped

will be

<span><div>test</div><!-- COMMENT -->comment-string-not-wrapped</span>
wim leers’s picture

Status: Needs review » Reviewed & tested by the community

This was RTBC for the longest time, and @effulgentsia nearly committed it in #219. @droplet un-RTBC'd, and I see @nod_ explaining to @droplet why it's correct the way it is. So moving back to RTBC.

It'd be great to finally have this fix shipping with Drupal 8.4!

droplet’s picture

Status: Reviewed & tested by the community » Needs review

I don't think it's correct and it broke the tests also. How can it RTBC?

the condition should filter the comment node and then count the remaining node is non-block level, not put any response with comment_node into SPAN.

I will RTBC if we drop #220 additional condition.

GrandmaGlassesRopeMan’s picture

StatusFileSize
new927 bytes
new27.42 KB

- Very minor adjustments to the tests based on the latest wrapping condition.

@droplet from #229 - Would you consider a followup to this? I do agree that having a span potentially wrapping a div goes against the spec. It' might be worth it though to get this fix in and address a spec violation immediately after.

droplet’s picture

@see below comment

droplet’s picture

Sorry about that took some time on EDITING previous comment. I've done real code testing to ensure I didn't miss any code changes.

Assumed the $wrapper is DIV ( block-level )

A.

TEXT <span class="comment-not-wrapped">comment-not-wrapped</span>

Patch #220: DIV
Patch #230: DIV

B.

TEXT <!-- COMMENT --> <span class="comment-not-wrapped">comment-not-wrapped</span>

Patch #220: DIV
Patch #230: SPAN

Then I will ask why A isn't SPAN in Patch #230

---

And I'd like to point out @effulgentsia's this point is wrong (I also overlooked at this point before)

But this means we would wrap hello world in a DIV instead of a SPAN, even if the target element is not a block element, since there's more than one DOM node. Has this been discussed already? Maybe we should open a followup issue for seeing if we can cover that case? And in any case, I think a comment here for why it's ok or linking to the followup issue would be helpful.

The patch detects the $wrapper over there. If $wrapper is SPAN, then SPAN. If not, then DIV. Therefore, Patch #230 will change a top block-level to inline-level elements.

Thanks.

GrandmaGlassesRopeMan’s picture

StatusFileSize
new2.79 KB
new27.45 KB

- An adjustment to make a better judgement call about wrapping responses with a span or a div. 🤷‍♀️

tim.plunkett’s picture

StatusFileSize
new8.6 KB
new27.21 KB

I was reviewing this, and wanted to fix the test to more clearly test each case. Now when a single case fails, you will clearly know which one it was.

I think a couple more cases need to be added to address #232. There is a COMMENT DIV one and a SPAN COMMENT but no COMMENT SPAN.

nod_’s picture

Thanks @droplet, insightful as usual :)

#233 is what I had in mind too :)

droplet’s picture

Hmmm, it hinted me something new:

+++ b/core/misc/ajax.es6.js
@@ -1062,13 +1062,13 @@
-        if (responseDataNodes.length === 1 || hasCommentNodes || $wrapper.css('display') !== 'block') {
+        if (onlyTextOrCommentNodes || $wrapper.css('display') !== 'block') {

if onlyTextOrCommentNodes met the condition, then we will have:

Before Ajax: content is BLOCK LEVEL

<div class="wrapper"><!-- COMMENT -->comment-string-not-wrapped</div>

After Ajax: content changed to INLINE LEVEL

<div class="wrapper"><span class="processed"><!-- COMMENT -->comment-string-not-wrapped</span></div>

After Ajax with replaceWith: (Change from BLOCK -> INLINE)

<span class="processed"><!-- COMMENT -->comment-string-not-wrapped</span>

Therefore, we only need this SINGLE ONE condition?

if ($wrapper.css('display') !== 'block') {
tim.plunkett’s picture

That may be true, but please write the test cases first! If the logic is wrong now, you should be able to come up with cases that fail.

droplet’s picture

StatusFileSize
new10.73 KB
new29.62 KB

- [NEW]: Split tests into INLINE & BLOCK level
- [BUG FIX]: these 3 test cases should get the same result, no wrapped.

      'top-level-only' => '<div>element #1</div><div>element #2</div>',
      'top-level-only-pre-whitespace' => ' <div>element #1</div><div>element #2</div> ',
      'top-level-only-middle-whitespace' => '<div>element #1</div> <div>element #2</div>',

- [NEW]: Added `pre_wrapped_div` and `pre_wrapped_span` and dropped `inline` and `pre_wrapped`

This patch should be failed

tedbow’s picture

@droplet thanks for the new patch.
for those of us that aren't following this issue that closely can you explain why #238 should fail..
Thanks

GrandmaGlassesRopeMan’s picture

@droplet

Maybe you forgot to add your bug fix? top-level-only-middle-whitespace is now being wrapped with a span, which is incorrect.

droplet’s picture

@drpal #241,

I didn't add code fix to above patch. Just enhanced the tests. @see below explanation.

@tedbow #239,

** Note 1: Patch #238 changes based on my conclusion of #236.
** Note 2: Don't forget we have `replaceWith` method:: If the script rendered with`html` method, DIV or SPAN usually doesn't matter and I can see both have their Pros & Cons. However, to use `replaceWith` method, we should treat the content same inline/block level as before.

Attached my local test failture log:

A:

Starting test 'Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "not_wrapped" ('not-wrapped', '<WRAPPER_PLACEHOLDER class="p...OLDER>')'.
E

Single TEXT_NODE
Patched: SPAN
Expected: depends on $wrapper, it can be DIV or SPAN

B:

Starting test 'Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "comment_string_not_wrapped" ('comment-string-not-wrapped', '<WRAPPER_PLACEHOLDER class="p...OLDER>')'.
E

Single TEXT_NODE.
Patched: SPAN
Expected: depends on $wrapper, it can be DIV or SPAN

C:

Starting test 'Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "top_level_only_middle_whitespace" ('top-level-only-middle-whitespace', '<div class="processed">elemen...</div>')'.
E

Patched: DIV
Expected: no wrappers, it should treat as top-level-only.

D:

Starting test 'Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "empty" ('empty', '<WRAPPER_PLACEHOLDER class="p...OLDER>')'.
E

Patched: SPAN
Expected: depends on $wrapper, it can be DIV or SPAN

E:

Starting test 'Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertInline with data set "top_level_only_middle_whitespace" ('top-level-only-middle-white
space', '<div class="processed">elemen...</div>')'.                                                                                                           
E                       

Patched: SPAN
Expected: no wrappers, it should treat as top-level-only.

droplet’s picture

Status: Needs work » Needs review
StatusFileSize
new29.3 KB
new2.8 KB

- Fixed failures

AjaxTest is 100% passed on my local :)

droplet’s picture

  1. +++ b/core/misc/ajax.es6.js
    @@ -1051,24 +1051,22 @@
    +          || element.textContent.trim().length === 0,
    

    this fixes the middle space issue

  2. +++ b/core/misc/ajax.es6.js
    @@ -1051,24 +1051,22 @@
    +      if (onlyElementNodes && trimmedData !== '') {
    

    this fixes the empty content

  3. +++ b/core/misc/ajax.es6.js
    @@ -1051,24 +1051,22 @@
    +        if ($wrapper.css('display') !== 'block') {
    

    this fixes the single node / comment mixed types

very tired. see all of you tomorrow :p (it's 3 a.m. here)

tim.plunkett’s picture

StatusFileSize
new8.44 KB
new30.29 KB
new8.44 KB

I spent a couple hours with @drpal on this today, and I have a few changes.

1)
When the method is 'html', and the new content begins with an HTML tag, no wrapping is needed.
This can be seen in the 'empty' test case.
This contradicts #244.2, but I think this is correct to change.

2)
If the response data has a DIV in it, it shouldn't matter what the destination markup is, it is already block level HTML.
We should do our very best to determine the from the data what the wrapper is. This patch is a *HUGE* step forward at removing needless divs.
But switching based on the destination markup is a mistake IMO.

3)
I removed the WRAPPER_PLACEHOLDER trick and switched each test case to the correct result.

droplet’s picture

@tim.plunkett,

Do you disagree this point?

** Note 2: Don't forget we have `replaceWith` method:: If the script rendered with`html` method, DIV or SPAN usually doesn't matter and I can see both have their Pros & Cons. However, to use `replaceWith` method, we should treat the content same inline/block level as before.

In patch #246, "Link replaceWith not-wrapped" always wrapped SPAN, I don't think it's correct. (this is also explained in #236)

droplet’s picture

So if #246 run with #243 tests,

1) Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "not_wrapped" ('not-wrapped', '<WRAPPER_PLACEHOLDER class="p...OLDER>')      
Behat\Mink\Exception\ExpectationException: The string "data-drupal-ajax-target=""><div class="processed">not-wrapped</div></" was not found anywhere in the HT
ML response of the current page.                                                                                                                              
                                                                                                                                                                                                                                                                                                              
2) Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "comment_string_not_wrapped" ('comment-string-not-wrapped', '<WRAPPER_PLACEHO
LDER class="p...OLDER>')                                                                                                                                      
Behat\Mink\Exception\ExpectationException: The string "data-drupal-ajax-target=""><div class="processed"><!-- COMMENT -->comment-string-not-wrapped</div></" w
as not found anywhere in the HTML response of the current page.                                                                                               
                                                                                                                                                              
                                                                
3) Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertBlock with data set "empty" ('empty', '<WRAPPER_PLACEHOLDER class="p...OLDER>')                  
Behat\Mink\Exception\ExpectationException: The string "data-drupal-ajax-target=""><div class="processed"></div></" was not found anywhere in the HTML response
 of the current page.                                                                                                                                         
                                                                
                                                                                                                                                              
4) Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertInline with data set "mixed" ('mixed', '<WRAPPER_PLACEHOLDER class="p...OLDER>')                 
Behat\Mink\Exception\ExpectationException: The string "data-drupal-ajax-target=""><span class="processed"> foo <!-- COMMENT -->  foo bar<div class="a class"><
p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p></span></" was not found anywhere in the HTML response of 
the current page.                                                                                                                                             
                                                                                                                                                              
                                                           
5) Drupal\FunctionalJavascriptTests\Ajax\AjaxTest::testInsertInline with data set "empty" ('empty', '<WRAPPER_PLACEHOLDER class="p...OLDER>')                 
Behat\Mink\Exception\ExpectationException: The string "data-drupal-ajax-target=""><span class="processed"></span></" was not found anywhere in the HTML respon
se of the current page.                                    

I have no comments to 3 and 5 (empty), I also plan to remove it, just no time to figure out why we kept it before (e.g. a second action will insert HTML into first .processed). It's a similar issue: #2588013: <span class="ajax-new-content" style="display:inline-block;"> causes unwanted whitespace.

tim.plunkett’s picture

In the case of both 1 and 2, the HTML being inserted contain no block level elements. So why wrap in a DIV?
And the opposite is true of 4, the HTML already contains a DIV, so wrapping in a SPAN is pointless.

GrandmaGlassesRopeMan’s picture

From #248,

- 1 & 2, lets not add extra divs when we don't need them.
- 4, I think this goes back to wrapping block level elements with inline elements, lets not do that.

➕1️⃣ from me.

droplet’s picture

You didn't look at `replaceWith` I think? and my principle is to keep it as same as BEFORE Ajax call as possible as we can.

I really don't know should I call TEXT inline or block level honestly. I could only tell it's whole HTML code is block level.
(I think TEXT NODE has no inline/block concept, only HTML tags had.)
<div>TEXT</div>

`replaceWith`, it's clearly the whole HTML code is inline level.
<span>TEXT</span>

If previous HTML tag is inline level:

Before Ajax call: DIV is on next line.
<span class="prev"></span><div>TEXT</div>

After Ajax call: SPAN is on the same line
<span class="prev"></span><span>TEXT</span>

#246.2 agreed on this point I think but the patch showed a different behavior? It's a bit confusing. and SPAN isn't equal to inline-level. that's why I added $wrapper.css('display') !== 'block'

and on my point:

If the script rendered with`html` method, DIV or SPAN usually doesn't matter and I can see both have their Pros & Cons.

Generally, we wrote CSS (e.g. CSS RESET) this way:

div,
div div,
div + div,
{
color: GREEN
}

span,
span span,
span + span,
{
color: RED
}

Patch #246 changed the text color.

droplet’s picture

droplet’s picture

and it reveals a bug I mentioned in previous comments again. (I wasn't given an example clearly)

SPACE before TEXT & Inline Tags does matter. We cannot trim it.
https://codepen.io/anon/pen/OjmQvE

tedbow’s picture

From #248
1,2 I agree with @tim.plunkett and @drpal I don't see why we would wrap these responses in divs.
3. I also think we should not be wrapping an empty response in a div, so the behavior and test coverage in #246 is correct.
4,5 I don't think we should be looking at the target and have that determine what we wrap the response in. This seems like it really open up a lot edge cases.

Adding tag " Needs issue summary update" because I don't think the summary will help understand the problem and solution.

effulgentsia’s picture

StatusFileSize
new26.49 KB
new5.23 KB

This whole business with guessing when to add a wrapper or not, and whether to make it a span or a div just seems plagued by complexity. What if we simplify to never wrap? Here's a patch that does that. Tests will fail, because I haven't adjusted them yet to not expect the wrapper. Would someone else like to do that? If not, I'll try to get to it tomorrow.

I'll also update the issue summary tomorrow for why I think it's ok to never wrap. It does have an impact on how effects are rendered, but I'll write up why I think that's ok, and better than us trying to second guess the response data.

dmsmidt’s picture

I like the idea of not wrapping, let the implementer make sure he/she has all the wrappings that are needed, for animations this patch already took that approach.
Wrappers where further only needed for easy context handling for behaviors I think. I see you now only pass element nodes as context. As a developer I could live with that, no magically appearing extra elements.

effulgentsia’s picture

Sorry, all. I wasn't able to get this finished today, so I think it'll be 8.5-only at this point.

I'm pulling my hair out with phantomjs. On my machine, even with HEAD, I get the same error for BigPipeRegressionTest that's in https://www.drupal.org/pift-ci-job/739001. Which makes me think that something got cached from when I had the patch applied. But I tried reinstalling everything, from the codebase to my local site to my computer, and nothing is fixing it. I'm hoping to make progress on this quickly once I can get my local testing tools working correctly.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new26.51 KB
new856 bytes
+++ b/core/misc/ajax.es6.js
@@ -1020,68 +1020,8 @@
-        responseDataNodes = $.parseHTML(trimmedData, true);
+      const $newContent = $($.parseHTML(response.data));

While still working on figuring out my local test environment, let's at least get a DrupalCI run when we undo the accidental change of removing the true argument for keeping scripts.

maximpodorov’s picture

Ts there the same issue for D7?
Now we have to use the following ugly solution (D7):
if ((new_content.length != 1 || new_content.get(0).nodeType != 1) && (wrapper.index($('head')) < 0)) {
new_content = new_content_wrapped;
}

wim leers’s picture

Can we please finally bring this to the finish line?

#2913563: Facets blocks loaded with BigPipe wrapped in unwanted <div> due to bug in AJAX system and #2738685-10: BigPipe leaves wrapper <div>s in place, because AJAX system leaves them — this can cause some CSS to break are again reporting this bug anew, and are blaming BigPipe. The root cause is the AJAX system, this issue.

We were so close to getting this committed in August.

andypost’s picture

Related #1268180: Newline character in HtmlTag causes unwanted whitespace also can help to remove some noise (for cases when ajax returns element)

droplet’s picture

I don't support this BC broken actions for D8.4 and the last few patches but if I'm the only blocker here. I will leave this issue to let it go. Thanks all.

EDIT:
my comments #251, #252, #253 has examples to demo the problem of the last patches.

GrandmaGlassesRopeMan’s picture

StatusFileSize
new2.43 KB
new26.45 KB

Alright, an effort to get this rolling again. This patch changes none of the logic from #259, but does fix the test coverage.

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
wim leers’s picture

@droplet: so do you prefer @effulgentsia's approach in #255/#259?

droplet’s picture

@Wim Leers,

I haven't reviewed @effulgentsia's patch in #259 carefully yet but the concept is simple. We may think of it too simple.

Let's say a 3000 words article WITHOUT p tag wrapped for each paragraph. It has many links, color, bolded styles. It easily ends up with a few hundreds top level of tag. I see it as a performance issue. (it'd be a huge impact to big_pipe-like modules I think)

Then we have default $.hide / $.show anime. And it applies to TAGs wrapped contents only. The inline texts are still VISIBLE. It animes around the text. It's really odd. Example: https://codepen.io/anon/pen/OjmQvE

For the latter problem, it able to do one more simple change:

To accept `NULL|'none'` to remove default `$.hide / $.show` effects. (clean up all default anime)

      // Immediately hide the new content if we're using any effects.
      if (effect.showEffect !== 'show') {
        $new_content.hide();
      }

EDIT: I even have not evaluated the jQuery.once / Drupal.attachBehaviors issues

----

With a clearly BC break solution for D8.5, a group decision is better than single one's opinion

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
StatusFileSize
new2.27 KB
new26.43 KB

I forgot these changes last time. 🤷‍♀️

wim leers’s picture

Assigned: Unassigned » effulgentsia

@effulgentsia in #255:

This whole business with guessing when to add a wrapper or not, and whether to make it a span or a div just seems plagued by complexity. What if we simplify to never wrap? Here's a patch that does that.

The code is much simpler indeed. @drpal got it to a place where it passes all tests.

I think we just need this now, again quoting @effulgentsia from #255:

I'll also update the issue summary tomorrow for why I think it's ok to never wrap. It does have an impact on how effects are rendered, but I'll write up why I think that's ok, and better than us trying to second guess the response data.

Then this can be RTBC.

wim leers’s picture

Status: Needs work » Needs review

I've unpublished comments #272 through #281, to remove all the noise. @drpal, you said you were going to update the issue summary? :) Very much looking forward to that, because I can RTBC after that!

droplet’s picture

Before @effulgentsia's cool comments.

My Evaluation:
the behavior changes of the last patch compare to GIT HEAD: (don't just look at the tests, that designed to get passed)

  • Anime
    - No more single-root anime.
    - And very odd anime for some cases. e.g., #269 example
  • Drupal.attachBehaviors
    Only applies to ELEMENT_NODE. All further callback involved the inserted content (non-ELEMENT_NODE) would be broken.
    Since it chopped the callback content into small pieces. Unexpected behavior may happen (for example, developers think single insertion before and apply jQuery.once or Durpal Behaviors to that block, now they have to deal with all chopped blocks. It's very bad DX IMO.)

    (I see this as a bug of the patch actually)

  • Performance impact for huge Ajax-calls sites. (depends on top level elements numbers: Drupal.attachBehaviors / jQuery.once / Anime )
  • Difficulty to fix/adopt the new change.
    HARD
    For the sites will run into the issue, you have to add an extra callback to modify the responses or change response from sources.

Few questions/suggestions:

  • after to drop the wrapper, do you think the test is overcomplicated?
  • have you ever thought to add no-wrapper as a NEW API?
  • clean up the anime in further?
  • Can we doc the current patch better to point out what the NEW insert is? We sealed it as a new thing and will not look back.
    - The main feature of this function
    - limitations of the new insert

(everyone looking for a different little piece of this patch, I hope our reviewers think of each problem carefully.)

effulgentsia’s picture

StatusFileSize
new702 bytes
new896.24 KB
new747.5 KB

Re #271, sorry I haven't updated the IS here yet. Before I do so, I want to clarify what I wrote in #255:

It does have an impact on how effects are rendered

Here's a way to see that. Apply this interdiff on top of #270. If you do that, then if you enable the ajax_test module, navigate to /ajax-test/insert-inline-wrapper, and click on "Link html mixed", you'll see the animation shown in the ajax-patch.mov file attached to this comment.

If you then revert the changes that this patch makes to ajax.js, clear caches, refresh the page, and click that link again, you'll see the animation shown in the ajax-head.mov file attached to this comment.

I think this odd behavior of the patch version is what @droplet is referring to in #283 as "No more single-root anime...very odd anime for some cases."

As I wrote in #255, I think that's ok though, or at least preferable to any other option we've explored, including leaving HEAD as-is. Modules that encounter an unexpected animation as a result can alter their responses to provide a wrapper. But I'd like to get a front-end framework manager to weigh in on that as well.

Regarding the other feedback in #283:

Drupal.attachBehaviors only applies to ELEMENT_NODE. All further callback involved the inserted content (non-ELEMENT_NODE) would be broken.

What's an example where that's a problem? What are possible behaviors that act on non-elements nodes?

Unexpected behavior may happen (for example, developers think single insertion before and apply jQuery.once or Durpal Behaviors to that block, now they have to deal with all chopped blocks.

What's an example of a behavior that would function differently when applied to each top-level element separately vs. to a wrapper element?

Difficulty to fix/adopt the new change...have you ever thought to add no-wrapper as a NEW API?

I think a #ajax setting to either opt-in or opt-out of an auto-generated wrapper is worth some consideration. For me, which one should be the default depends on whether it's only animations that are affected, or if behaviors are also affected, so I'd love some more insight on real or hypothetical behaviors that might be affected.

do you think the test is overcomplicated?

Yes, I think it probably is. We could probably remove most of the scenarios, but still leave key ones like the "mixed" ones.

clean up the anime in further?

how?

Anonymous’s picture

Great work here! Patch #270 works like a charm! It removes the disgusting div, whose source is not entirely obvious (since it does not contain any identifying attributes).

In the times of css flex, this is a very annoying wrapper, since the sensitivity to the nesting of the elements is again increased. Another important reason is the increased use of AJAX in 8.5.

As a simple development, I super plus the idea to never wrap any response. Or at least, make this behavior configurable (ui checkbox with disable wrapping by default).

I do not know how much important animated luster. While broken layout is a fatal problem. So, +1000 for open follow-up, if we have problem with animation after it, but fixed layout.


IMO, if the developer did not take care of the necessary wrapper, then finding a problem, he will only blame himself for it. While trying to figure out the reason undefined wrapper, he is unlikely to be so self-critical.
s.messaris’s picture

Many thanks for #270. I have used ajax extensively since D7 and have been applying dirty hacks to get around layout breaking because of the wrapping. I was a bit disheartened when first trying D8 and seeing this affected BigPipe as well. Since using #270 I have had no issues, I am very happy about that! Many thanks to everyone that has worked and is still working on this!

Leagnus’s picture

drpal, thank you,
#270 resolves adjacent issue

khiminrm’s picture

Patch from #270 helps me to fix 'html' method in ajax for select element in custom form. Without patch I have div wrapper for value returned by ajax callback. Thanks!

wim leers’s picture

Can we please get that issue summary update here, so that we can move this back to RTBC?

GrandmaGlassesRopeMan’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Updated.

wim leers’s picture

Assigned: effulgentsia » Unassigned
Status: Needs review » Reviewed & tested by the community
GrandmaGlassesRopeMan’s picture

Issue summary: View changes

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

edurenye’s picture

Status: Reviewed & tested by the community » Needs work

The patch needs a reroll.

edurenye’s picture

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

Rerolled.

GrandmaGlassesRopeMan’s picture

Status: Needs review » Reviewed & tested by the community

Alright. I think this reroll looks good. 🎉™️

dillix’s picture

Is there a chance to get it fixed for 8.5.0?

wim leers’s picture

#298: Yes, but it's at committer discretion. We should assume it won't make it into 8.5.x.

dillix’s picture

It would be great if we will see this in 8.5.x after 8 years of patching core :)

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

Tests only failed because HEAD is currently broken.

alexpott’s picture

Issue tags: +Needs change record

I think we need a change record to detail that the wrapping div is no longer going to be there. And whilst I agree it is not API someone might be targeting something using the div in CSS. I don't think we should hold back from changing this because of that though.

GrandmaGlassesRopeMan’s picture

Issue summary: View changes
Issue tags: -Needs change record
GrandmaGlassesRopeMan’s picture

Issue summary: View changes
Anonymous’s picture

Status: Needs work » Reviewed & tested by the community
marcoscano’s picture

Queued #296 to be tested against 8.5.x, in the hope that it can be cherry-picked.

I have manually tested it with 8.5.x and it applies cleanly and works as expected in our project scenario.

itsekhmistro’s picture

Manually applied patch from the comment #296 to 8.4.4 (there is difference in js variable names), it works well. No regression has been found so far.

spadxiii’s picture

There is a little issue with this patch: inline svg does not work anymore in Chrome. Somehow it works fine in Firefox...

I traced it down to var $newContent = $($.parseHTML(response.data, true)); which seems to break the inline svg.
Replacing this with a simple var $newContent = $(response.data); seems to work, but I'm not sure why.

In my case, I have a menu-block in the footer, where each link has an inline svg (with use). This works fine without bigpipe, but when bigpipe is enabled, the menu-block is added via js and the svg is not showing anymore.

I've added this comment in case others run into this problem. I'll leave this as RTBC as my issue might be more of an edge-case.

idebr’s picture

To expand on the issue in #310: I have noticed this occurs when using svg markup that references an external file, for example:

<svg title="Find me on Twitter" role="img">
  <use xlink:href="/assets/map.svg#twitter"/>
</svg>

The project I am working on includes the svg4everybody library. Enabling the 'polyfill' option that replaces this markup with the actual svg markup fixes the display issue.

GrandmaGlassesRopeMan’s picture

@SpadXIII

The reason we need to use $.parseHTML() in addition to wrapping the response as a jQuery object is due to leading or trailing text nodes. For example,

// Throws a syntax error, related to the leading text node.
$('xyz <div>hello world</div>');

// No error is thrown, but the trailing text node is dropped. 
$('<div>hello world</div> xyz');

// A DIV and and a TEXT node are returned. 
$($.parseHTML('<div>hello world</div> xyz'));

Here is some more information on what is actually happening with $.parseHTML().

@idebr

I built a few test cases with an XML Document being returned and didn't notice anything strange happening. Perhaps there is some malformed XML being returned that causes the parser to fail?

dillix’s picture

What we need to get this committed?

alexpott’s picture

I've tried to look at the #311

$a = jQuery.parseHTML('<svg title="Find me on Twitter" role="img"><use xlink:href="/assets/map.svg#twitter"/></svg>', true);
jQuery($a).prop('outerHTML');

This outputs:

<svg title="Find me on Twitter" role="img"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/map.svg#twitter"></use></svg>

It would be great if @SpadXIII and @idebr could do some more debugging since things look sensible as far as I can see.

GrandmaGlassesRopeMan’s picture

@alexpott

That's the behavior I observed. I tried inserting the HTML a bunch of different ways and didn't notice any strange behavior with inline XML.

idebr’s picture

StatusFileSize
new314.64 KB
new3.73 KB
new3.06 KB

#314, #315 Sorry for the late comment.

I have attached a module 'svg_test' that displays the failing behavior:
- /svg-test/bigpipe renders an inline svg that references an external svg sprite.
- /svg-test/default is the control group. It uses default rendering.

Regarding your observations: Yes, the output is identical before and after the patch. However, after patch the icon is no longer rendered. I have only been able to reproduce this behavior in Chrome.

Attached screencast shows the behavior including dramatic mouse movements:

GrandmaGlassesRopeMan’s picture

@idebr Would it be possible for you to post the module code some place, perhaps Github? I'm not totally thrilled about downloading a zip from a relatively unknown location.

GrandmaGlassesRopeMan’s picture

StatusFileSize
new573.46 KB

@idebr

I tested the svg_test patch in Chrome 63 this morning. I wasn't able to replicate any strange behavior with the external svg loading.

idebr’s picture

Re #318:

In your screencast I noticed you are not logged in. BigPipe only uses ajax for authenticated users, so you will notice no regression when testing with an anonymous user.

GrandmaGlassesRopeMan’s picture

StatusFileSize
new1.13 MB

ugh

Ok. So I see this when I'm actually on the correct branch. The side effect of working on too many things at once.

GrandmaGlassesRopeMan’s picture

Status: Reviewed & tested by the community » Needs work

Alright. Marking this as Needs work. 😭

There's a couple of issues here, I'll do my best to highlight what is happening. When we call $() on the result of $.parseHTML(response.data, true);, this uses the browsers innerHTML method. Since innerHTML is only available on HTMLElement, not SVGElement we can't use it to parse XML-like elements.

There is an experimental technology, DOMParser, which would allow us to parse an XML document. However that opens a door to inspecting every string returned for XML-like nodes and parsing them with a different parser.

If you want to create a new a new SVG element, you can do something like,

document.getElementById('my_id').appendChild(
  document.createElementNS('http://www.w3.org/2000/svg', 'circle');
);

This uses the native browser API's, and additionally sets the namespace of the created element.

There is however a workaround to all of this...

<div>
  <svg height="100" width="100">
  </svg>
</div>

const parse = data => $('<div></div>').html(data).contents()
$('svg').append(parse('<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />'))
$('div').html($('div').html());

One additional thing to mention, this fails in Chrome and Safari, but not Firefox. There's probably some additional investigation there. Some browsers appear to be implementing the spec correctly, while others do not.

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
StatusFileSize
new27.73 KB
new2.94 KB

Alright. I don't know if this is a good idea. This is probably a bit too brittle, but does solve the exact issue with AJAX insertion of SVG data.

GrandmaGlassesRopeMan’s picture

+++ b/core/misc/ajax.es6.js
@@ -1025,39 +1025,35 @@
+          const hash = `svg-${Math.random().toString(36).substring(7)}`;

Let's hold off on this until #2946522: Provide a mechanism to get a random string, suitable for element IDs is done.

piyuesh23’s picture

StatusFileSize
new26.39 KB

Needed to use this on an ongoing project running on 8.4.5 & re-rolled the patch in comment #119. Attaching it here that it helps anyone else seeking the patch.

andypost’s picture

btw Anyone used to test inline JS?

marcoscano’s picture

Let's hold off on this until #2946522: Provide a mechanism to get a random string, suitable for element IDs is done.

Do we really want to depend on that? The new method will likely only be included in 8.6, and this is a very old bug affecting many projects, so perhaps the benefit of committing it now would outcome the drawback of not using the new method?

Maybe we can try to get this in as is, with a @todo to make the change once #2946522: Provide a mechanism to get a random string, suitable for element IDs lands?

dillix’s picture

@drpal, may be we should commit this issue as is and create follow up for full svg support?

@piyuesh23 can you re-roll patch for 8.5?

andreyks’s picture

StatusFileSize
new23.86 KB
GrandmaGlassesRopeMan’s picture

@andreyks Can you post an interdiff? It will really help with figuring out what changed.

Anonymous’s picture

Additional negatives from this unexpected div:

File upload/remove:

Layout rearrange:

Field UI:
Field UI

#322 still works fine!

Anonymous’s picture

NR for #322. RTBC++

Anonymous’s picture

Related issues: +#2612402: Field UI table get wrapper after each ajax event
StatusFileSize
new7.56 KB
new27.85 KB
new6.82 KB

#325: if it is a question about html tags inside of <script>, then this is a separate issue #2822525: FilterHtml leaves unclosed tags inside inline scripts.

#330: attached interdiff-322-328, but I did not understand why this was done :)

#333 review:

  1. +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
    @@ -329,6 +336,7 @@ protected function getRenderTypes() {
    +      'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"/></svg>',
    
    +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -2,6 +2,7 @@
    +use Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver;
    
    @@ -11,30 +12,13 @@
    +  protected $minkDefaultDriverClass = DrupalSelenium2Driver::class;
    

    Add test coverage for svg (PhantomJS failed, so Selenium).

  2. +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
    @@ -74,7 +77,7 @@ public function insertLinksBlockWrapper() {
    -        '#markup' => '<div id="ajax-target" data-drupal-ajax-target="">Target</div>',
    +        '#markup' => '<div class="ajax-target-wrapper"><div id="ajax-target">Target</div></div>',
    
    @@ -108,7 +114,7 @@ public function insertLinksInlineWrapper() {
    -        '#markup' => '<span id="ajax-target-inline" data-drupal-ajax-target="">Target inline</span>',
    +        '#markup' => '<div class="ajax-target-wrapper"><span id="ajax-target-inline">Target inline</span></div>',
    
    <code>
    +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -11,30 +12,13 @@
    -  protected function wrapAjaxTarget($html) {
    
    @@ -231,4 +208,18 @@ public function providerTestInsert() {
    +  protected function assertWaitPageContains($text) {
    
    @@ -111,19 +95,13 @@ public function testDrupalSettingsCachingRegression() {
    -    $assert->responseContains($this->wrapAjaxTarget($expected));
    +    $this->assertWaitPageContains('<div class="ajax-target-wrapper"><div id="ajax-target">' . $expected . '</div></div>');
    ...
    -    $assert->assertWaitOnAjaxRequest();
    -    $assert->responseContains($expected);
    -    $assert->responseNotContains($this->wrapAjaxTarget($expected));
    +    $this->assertWaitPageContains('<div class="ajax-target-wrapper">' . $expected . '</div>');
    

    Improve tests (i hope). Because $assert->responseContains($expected); does not works for 'empty' case in last version of mink. It seems this is another regression in addition to #2948700: Casting $text value to string in responseContains/responseNotContains methods. But it's good, because testing a '' on a page is pointless.

    So additional wrapper is added, and the 'contains' assert is replaced by 'same' assert. Due to this, there is no need to assert responseNotContains anymore.

  3. +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
    @@ -65,6 +65,9 @@ public function renderTypes($type) {
    +   * @return array
    +   *   Renderable array of AJAX response contents.
    
    @@ -99,6 +102,9 @@ public function insertLinksBlockWrapper() {
    +   * @return array
    +   *   Renderable array of AJAX response contents.
    

    Fixed few nit doc flaws.

lokapujya’s picture

+++ b/core/misc/ajax.es6.js
@@ -1066,24 +1062,35 @@
+      // effect, then show the new content.Content.find('.ajax-new-content')[effect.showEffect](effect.showSpeed);

Why is this code in the comment?

Anonymous’s picture

StatusFileSize
new27.75 KB
new559 bytes

#334: good question, reverted.

lauriii’s picture

I changed the svg handling into a more elegant and simple solution which should fix the bug. Few weeks ago I tried to write a test case for this but I couldn't figure out how to do that since the bug was caused by browser rendering bug.

We should still try to address the change in effect behavior when passing multiple root elements. I proposed that we would add a wrapping div and give a deprecation warning. However, later I realized that we don't have a way to set deprecation messages in JavaScript.

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new25.5 KB
new8.57 KB

#336: WOW! I'm not very good at this part of the patch, but it looks really elegant!

The only reason the test failed was the 'processed' class appeared:

- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">...
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" class="processed">...

I hope we can easily live with this extra class, so just updated the expected value.
Also replaced the model 2 test + dataProvider to 1 test + 2 asserts. Much faster testing now:

+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -91,121 +91,84 @@ public function testDrupalSettingsCachingRegression() {
-  public function testInsertBlock($render_type, $expected) {
+  public function assertInsertBlock($render_type, $expected) {
...
-  public function testInsertInline($render_type, $expected) {
+  public function assertInsertInline($render_type, $expected) {
...
-  public function providerTestInsert() {
+  public function testInsertAjaxResponse() {
Anonymous’s picture

StatusFileSize
new25.62 KB
new898 bytes

Opps! I was so happy that I lost one of the test cases in a hurry. Restored.

lauriii’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update
Anonymous’s picture

Issue summary: View changes
Status: Needs work » Needs review
lokapujya’s picture

very nitty:

+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -82,4 +85,105 @@ public function testDrupalSettingsCachingRegression() {
+    $test_cases['pre-wrapped-whitespace'] = " <div class=\"pre-wrapped-whitespace processed\">pre-wrapped-whitespace</div>\n";

let's be consistent and use single quote here.

Anonymous’s picture

\n not works inside of single quote. So double quote here.

lauriii’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update
dinesh18’s picture

#339 looks good to me.
I agree with comment #343. +1 to RTBC

zenimagine’s picture

It is many that I do not understand why there is a div around some view. I finally found the right question

lokapujya’s picture

Remove the remaining task. Put the answer in the issue summary.

lokapujya’s picture

Status: Needs review » Needs work
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -82,4 +85,105 @@ public function testDrupalSettingsCachingRegression() {
+    // Test that not wrapped response data (text node) is inserted wrapped and
+    // all top-level node elements (context) are processed correctly.

I'm confused. If it is inserted wrapped, why is the response not wrapped. Does this need to be clarified or am I just not understanding the comment correctly?

lauriii’s picture

#347: I don't think we've made a decision how are we handling that BC break and that's why it's in the remaining tasks.
#348: Good catch, I think we need to update the comment.

Anonymous’s picture

StatusFileSize
new2.31 KB

+1 to make this decision in follow-up issue :)

Animation problem is negligible compared to the broken pages on the site (css rules and js scripts can be based on page layout).

But if not, what about decision from #336 with wrapper from Drupal.theme (attached file).

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new27.89 KB
new5.7 KB

#351: Thank you, @lokapujya! +1

Also after a series of experiments I was convinced that we really should not by default wrap multi-root-elements.

Because any space, any html comment makes the response as multi-root-elements. This is extremely inconvenient.

It is much better to make an empty wrapper by default, and CR with instructions like:

If you do not have enough wrapper for your response data, it is highly recommended just add it yourself. You can also completely restore the previous behavior, through the js theming:

(function($, Drupal){
  Drupal.theme.ajaxWrapperMultipleRootElements = function (elements) {
    return $('<div></div>').append(elements);
  };
}(jQuery, Drupal));

A patch is created based on the proposed idea + extended test for the custom js-wrapper.

skaught’s picture

#352 -- this should be in the change record:

If you do not have enough wrapper for your response data, it is highly recommended just add it yourself. You can also completely restore the previous behavior, through the js theming:

(function($, Drupal){
  Drupal.theme.ajaxWrapperMultipleRootElements = function (elements) {
    return $('<div></div>').append(elements);
  };
}(jQuery, Drupal));
Anonymous’s picture

@SKAUGHT, thanks for quick feedback! Yes, if we all (and especially the mantainers) agree with this way, then I will update the change records (although I will be grateful in the help of those who speak English better than I, that is from anyone 😉)

I also want to draw attention to the fact that @effulgentsia made the demonstration of "odd" effect in #284 in the slow motion (for better study). In reality, this is a fairly fast process. >300ms animation is annoying, I remember that it is a bad design practice for usual cases.

lauriii’s picture

Also after a series of experiments I was convinced that we really should not by default wrap multi-root-elements.

Because any space, any html comment makes the response as multi-root-elements. This is extremely inconvenient.

The multi-root ajax responses are only a problem if there's an effect. How about we only add the wrapping div if there's multiple elements on the root level, and there's an effect?

Anonymous’s picture

For example, next multiple cases haven't problem with effect:

  • With space before tag: <div>Response data</div>
  • With HTML comment: <!-- start response data --><div>Response data</div><!-- end response data -->

Therefore, the wrapper will not be of use to them.

Can it be better to just disable the effect for multi-roots? I think not. Odd animation effect looks not so bad for me.

In #2969658: ajax.js insert command: Decide what to do with the effect behavior change if there's multiple root elements. we can try to improve next part:

$newContent[effect.showEffect](effect.showSpeed);

Example, add special animation-delay via effect to each tag-elements from multiple-root (because we know count elements, and effect speed).

But it seems to me, that we are just trying to help make a good animation for an incorrectly wrapped data. I am not against that, but is this the reason for the delay? Maybe it's better to leave it for the follow-up. Perhaps when more developers start works without this 'div', we will get more examples of animation problem, and we will be able to create the best solution.

lauriii’s picture

This is important because there are existing sites that rely on this behavior and for that reason we should try to avoid changing it. It is not so much about does it look bad or not, but about the fact that someones site might break because of this. For that reason we also cannot defer fixing that in a follow-up, unless we decide to do a breaking change.

The support for effects with multiple root elements is meant to be temporary. I also proposed that we give a warning for any users that would be affected by this logic, but we don't have a way to do that until #2962344: [policy, no patch] Document how to handle deprecations in JavaScript has been resolve.

So if we only add the div when there's multiple root elements and an effect, the bug would be fixed in most cases, and there wouldn't be any known breaking changes for existing sites.

Anonymous’s picture

StatusFileSize
new31.25 KB
new11.62 KB

Thanks for clarifying. It makes sense :(

Done via additional theming method ajaxWrapperNewContent .

Because we can not guarantee that this div is needed only for official drupal js-effects (it can be used in css or in custom js).
I also added special filter to exclude from the counting such elements as '#comment' and '#text' that contains only whitespace chars.

But if a developer wants to wrap any type of multi-root element, he can just do next:

(function($, Drupal){
  Drupal.theme.ajaxWrapperNewContent = function ($newContent, ajax, response) {
    if($newContent.length > 1) {
      $newContent = $('<div></div>').append($newContent);
    }
    return $newContent;
  };
}(jQuery, Drupal));

If a developer wants to custom wrapper only for default multiple root logic:

Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => {
  return $('<div class="my-wrapper"></div>').append($elements);
};

Also now the test coverage should check:
  • all single root cases never wrapped
  • all multi root cases never wrapped if "none" effect.
  • all multi root cases wrapped if other effect.
  • cases with 'whitespace' or 'html comments' nodes are not multi root by default.
  • we can theming multi root wrapper.
  • we can theming full response wrapper.

All checks should be for inline/block pages.
All checks should be for insert/replace method.

I adjusted the tests for this, but the check about 'theming full response wrapper' out of the loop, because it is already too big :)

Anonymous’s picture

I also doubt that it will be correct to deprecated multiple root elements response. If I want to return a set of cards, and I want them to slide from one point, then I just need that "oddly" animation effect, and multiple root elements.

At the moment, Drupal is wrapping everything multi root elements, and we want to keep it for BC. But in fact it limits the use cases. With two new methods from #358 developers will finally have the opportunity to break these fetters.

GrandmaGlassesRopeMan’s picture

Status: Needs review » Needs work
  1. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,46 @@
    +    let typeEffect = response.effect || ajax.effect;
    

    Use const here. typeEffect is never reassigned.

  2. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,46 @@
    +      for (let i = 0; i < $newContent.length; i++) {
    +        if (
    +          ($newContent[i].nodeName === '#comment') ||
    +          ($newContent[i].nodeName === '#text' && /^(\s|\n|\r)*$/.test($newContent[i].textContent))
    +        ) {
    +          countEffectElements--;
    +        }
    +      }
    

    This is a bit awkward. It's generally discouraged to use a for...in loop now. Since $newContent is a jQuery element you can probably use .filter() to reduce the array of elements.

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new31.12 KB
new3.22 KB

@drpal, thanks for hints! Done.

lokapujya’s picture

  1. We still need to fix #348. These comments for the test cases are the documentation. I think that
    +    // Test that not wrapped response data (text node) is inserted wrapped and
    

    should be

    +    // Test that not wrapped response data (text node) is inserted not wrapped and
    

We can clean up the comments in a followup. For example, "not wrapped" maybe could be "unwrapped". Or, "Test that not wrapped response data (text node) is inserted wrapped" could maybe just be "Test that text is inserted wrapped". But the comments should at least say the correct action before RTBC.

GrandmaGlassesRopeMan’s picture

Status: Needs review » Needs work

Sorry I didn't catch some of this before.

  1. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,42 @@
    +  Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => {
    +    const typeEffect = response.effect || ajax.effect;
    +    if (typeEffect !== 'none') {
    +      // Exclude all nodes that do not interfere with animation effect.
    +      const $filteredContent = $newContent.filter(function(i) {
    +        let isHtmlComment = $newContent[i].nodeName !== '#comment';
    +        let isWhitespaceText = $newContent[i].nodeName === '#text' && /^(\s|\n|\r)*$/.test($newContent[i].textContent);
    +        return !(isHtmlComment || isWhitespaceText);
    +      });
    +      if ($filteredContent.length > 1) {
    +        $newContent = Drupal.theme('ajaxWrapperMultipleRootElements', $newContent);
    +      }
    +    }
    +    return $newContent;
    +  };
    

    The sum of this function is to either return $newContent unmodified, or wrap $newContent if some conditions are met. I think we can reduce the complexity just a bit,

      Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => (
        (response.effect || ajax.effect) !== 'none' &&
          $newContent.filter(
            i =>
              !(
                $newContent[i].nodeName !== '#comment' ||
                ($newContent[i].nodeName === '#text' &&
                /^(\s|\n|\r)*$/.test($newContent[i].textContent))
              ),
            ).length > 1
          ? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent)
          : $newContent
      );
    
  2. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,42 @@
    +      const $filteredContent = $newContent.filter(function(i) {
    

    Arrow function.

  3. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,42 @@
    +  Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => {
    +    return $('<div class="wrapper-ajax-multiple-root-elements"></div>').append($elements);
    +  };
    

    You can use the implied return syntax here.

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new30.8 KB
new9.81 KB

#362: Sure, done.
#363: Wow, looks nice, thanks! Done.

Also few other fixes in php-test, the main of which:

+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -133,7 +133,7 @@ public function testInsertAjaxResponse() {
-    $test_cases = $test_all_cases;
+    $test_cases += $test_all_cases;

Because of this flaw, the test did not check all cases, and as result some code was wrong.

Also move $custom_wrapper_multiple_root script out of loop too, because now the default ajaxWrapperMultipleRootElements wrapper checks the processing of multi-roots, and for the custom it will be enough just to check one case.

dillix’s picture

Looks good, plus for RTBC.

lauriii’s picture

Status: Needs review » Needs work

Thanks everyone for the fantastic work! 🏋️‍♂️

  1. +++ b/core/misc/ajax.es6.js
    @@ -1025,39 +1059,20 @@
    +      // Parse response.data into an element collection.
    

    This comment should be updated.

  2. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxFormPageCacheTest.php
    --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    

    We should go through the test cases and the documentation and make sure all of them are up to date. It seems like that there's multiple comments that are out of date.

  3. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,40 @@
    +  Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => (
    ...
    +  Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => (
    

    We should document that these are deprecated and will be removed in Drupal 9.0.0. We should also open a follow-up for triggering a deprecation warning here.

  4. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -82,4 +85,156 @@ public function testDrupalSettingsCachingRegression() {
    +    $multiple_root_cases = [
    

    Should we rename the variable to something like $expect_wrapper_cases? We could also add a comment here that this is temporary behavior.

lauriii’s picture

Issue summary: View changes

Updating the issue summary with information about the BC layer for the multi root element inserts with effects.

Anonymous’s picture

#366.3: If I understand correctly, then we should not make a behavior change between major releases of Drupal anymore. That is, we can not use wrapper in D8, and stop doing it in D9. This should happen between minor releases.

Should we mark both theming methods like deprecated? Looks like not. They provide an opportunity to theming response data.

The problem is only in one line:
$('<div class="wrapper-ajax-multiple-root-elements"></div>').append($elements)
Because by default Drupal should not decide whether to wrap response data.

So, do I correctly imagine a further plan:

Step 1. Now:

Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => (
  // @todo: add deprecated trigger after https://www.drupal.org/project/drupal/issues/2962344
  // See https//our-issue-about-add-trigger.
  $('<div class="wrapper-ajax-multiple-root-elements"></div>').append($elements)
);

Step 2. After the implementation of the trigger (example in 8.5.x or in early 8.6.x):

Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => (
  trigger_error('Default wrapper is deprecated and will be removed in 8.7.x', E_USER_DEPRECATED);
  $('<div class="wrapper-ajax-multiple-root-elements"></div>').append($elements)
);

Step 3. During the full minor release we show the deprecated message (if the user does not override it and has related cases). And in the next minor release:

Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => $elements;

We will not remove the ability to theming response, just do not wrap it by default. And the user can always easily get the previous behavior.


Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => (
This method generally remains unchanged, it allows to theming even more globally. For example, add a wrapper to any response regardless of the effect or multi-root case.
lauriii’s picture

If I understand correctly, then we should not make a behavior change between major releases of Drupal anymore. That is, we can not use wrapper in D8, and stop doing it in D9. This should happen between minor releases.

We want to avoid making changes that are likely to break existing code. I think the current logic is fine; it removes the div unless there is multiple root level elements and an effect attached to the ajax event.

Should we mark both theming methods like deprecated? Looks like not. They provide an opportunity to theming response data.

This is only temporary. We deprecate those theme functions to educate people that they will be removed in Drupal 9.0.0 since we don't want to add the wrapping div in any use case.

Step 3. During the full minor release we show the deprecated message (if the user does not override it and has related cases). And in the next minor release:

I'd say the step 3 should be removing the theme functions fully from Drupal 9.0.0. They are only there temporarily added to allow people remove the div if they are confident that it doesn't affect their site.

Anonymous’s picture

Status: Needs work » Needs review
Issue tags: +Needs change record updates
Related issues: +#2973400: Mark functions for wrapping Ajax response as deprecated
StatusFileSize
new14.05 KB
new14 KB

Thanks for this additional explanation!

#366.1: Done (i hope)
#366.2: Not found problem in AjaxFormPageCacheTest. But big rewrite AjaxTest :) Because it looks that it will be better for reading and support. Sorry if not.
#366.3: Done (#2973400: Mark functions for wrapping Ajax response as deprecated)
#366.4: Done above $render_multiple_root_wrapper.

Reveiw "big rewrite AjaxTest":

  1. +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
    @@ -327,25 +327,33 @@ public function dialogClose() {
    -    $render_types = [
    +    $render_single_root = [
    ...
    +    $render_multiple_root = [
    
    +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -93,67 +96,40 @@ public function testDrupalSettingsCachingRegression() {
    +    $render_single_root = [
    ...
    +    $render_multiple_root_unwrapper = [
    

    Just decouple on two groups. Now it is the same content (easy to copy/paste).

  2. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -173,22 +148,22 @@ public function testInsertAjaxResponse() {
    -    $this->assertInsertBlock('empty--effect-none', $expected, $custom_wrapper_new_content);
    -    $this->assertInsertInline('empty--effect-none', $expected, $custom_wrapper_new_content);
    ...
    +    $this->assertInsert('empty', $expected, $custom_wrapper_new_content);
    

    We everywhere check block/inline insert support, so combine in one assert.

  3. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -231,8 +195,8 @@ public function assertInsertInline($render_type, $expected, $script = '') {
    -      // After the effects can remain empty styles.
    -      $content = str_replace(' style=""', '', $page->getContent());
    +      // Clear content from empty styles and "processed" classes after effect.
    +      $content = str_replace([' class="processed"', ' processed', ' style=""'], '', $page->getContent());
    
  4. If we clear content from 'processed' classes, we get rid of the need to manually set it for expected values. What simplifies the test for wraps problem.

Status: Needs review » Needs work

The last submitted patch, 370: 736066-370.patch, failed testing. View results

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new28.98 KB
new14.05 KB

#370: double interdiff, sorry!

lokapujya’s picture

why remove the test comments? those were basically the only documentation. Edit: Although looking closely, they are kind of redundant; The class names are somewhat descriptive of the test case.

lokapujya’s picture

Are there any contrib modules that will test against this once it goes in?

Anonymous’s picture

I updated change record. Still tag 'Needs change record updates' for review.

#373: Yes, test cases have keys with good names, and they are grouped together, and now completely coincide with data from AjaxTestController. Thus, comments are not of much more value. I hope for support from the author of these comments, because I know his preference for Spartan brevity in such things as code.

lokapujya’s picture

In the the change record, it's not clear that we are only NOT removing the div ONLY when both a.) for multiple root elements and b.) when we have an ajax affect. It is clear in the issue summary.

Anonymous’s picture

@lokapujya, thanks for review! CR updated per #376.

lokapujya’s picture

Status: Needs review » Reviewed & tested by the community

A patch was RTBC'd months ago. More improvements were done and now it needs some real world testing.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 372: 736066-372.patch, failed testing. View results

lauriii’s picture

It seems like we are close to 🚀here!

  1. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,46 @@
    +   * @todo: Mark the function as deprecated after it will be possible. See
    +   * https://www.drupal.org/project/drupal/issues/2973400
    ...
    +   * @todo: Mark the function as deprecated after it will be possible. See
    +   * https://www.drupal.org/project/drupal/issues/2973400
    

    We could mark these deprecated already, we just cannot trigger an error.

  2. +++ b/core/misc/ajax.es6.js
    @@ -1025,39 +1065,20 @@
    +      // Parse response.data to get element collection.
    

    This comment should be updated since we are not parsing anymore.

  3. +++ b/core/misc/ajax.es6.js
    @@ -1025,39 +1065,20 @@
    +      $newContent = Drupal.theme('ajaxWrapperNewContent', $newContent, ajax, response);
    

    Can we add a comment here why are are calling this theme function?

  4. +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
    @@ -82,4 +88,119 @@ public function testDrupalSettingsCachingRegression() {
    +    // This is temporary behavior for BC reason.
    

    👍

droplet’s picture

Someone calling me. I scanned few comments quickly and would like to point out something really quick on SVG

+++ b/core/misc/ajax.es6.js
@@ -1025,39 +1025,19 @@
+      const $newContent = $($.parseHTML(response.data, true));

#296 - latest patch without SVG patching.

#312 @drpal explained few reasons that why to use $.parseHTML.

But, in jQuery 3.x+, $.parseHTML has a hidden feature to stop scripts being executed (during parsing). It using document.implementation internal rather than executed in the same document context.

You can checkout the source code here:
https://github.com/jquery/jquery/blob/2d4f53416e5f74fa98e0c1d66b6f3c285a...

+++ b/core/misc/ajax.es6.js
@@ -1025,39 +1065,20 @@
+      let $newContent = $('<div></div>').html(response.data).contents();

And above code is the currently planned workaround for SVG.

So what's the difference between jQuery 3.x $.parseHTML and above $.html and jQuery 2.x $.parseHTML.

Yeah. You Got it!. the execution scope! Then, we could passing `document` into it:

const $newContent = $($.parseHTML(response.data, document, true));

In advanced usage, we could grep the content to decide if to pass the `document` scope or not.

Cheers.

Anonymous’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record updates
StatusFileSize
new2.42 KB
new2.42 KB

#378: thank you, @lokapujya!

#380: thank you @lauriii! All points done except #2 (because #381).

#381: a pleasant surprise to see the post from @droplet again! I was so excited that I did not understand anything, but I will gladly accept your version!) Especially since it makes again relevant the comment:
// Parse response.data into an element collection.
And we do not need to think about a new comment :)

Status: Needs review » Needs work

The last submitted patch, 382: 736066-382.patch, failed testing. View results

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new29.34 KB
new2.42 KB
lauriii’s picture

Status: Needs review » Needs work

@droplet thank you very much for the comment! 😊

The problem indeed is that parseHtml() is using document.implementation.createHTMLDocument() as context on browsers that support it. Using that as context will prevent browser from loading assets for the parsed markup, which as a result breaks the SVGs with dependencies and probably some other things as well. So as a solution we force it to use document as context always and then we can safely use parseHtml() in our use case. 🎉

@vaplas thank you for your long-term commitment to fixing this issue! 🚣‍♂️

There's few more things related to documentation I would like to get sorted before marking this as RTBC:

  1. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,52 @@
    +   * Provide a wrapper for new content via Ajax.
    

    Let's provide a bit more documentation about when we are adding the wrapper. 🐾

  2. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,52 @@
    +   * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0.
    ...
    +   * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0.
    

    This should be 8.6.x since I don't think we will be able to backport this into 8.5.x. 👮‍♂️

  3. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,52 @@
    +   * Use data with desired wrapper.
    ...
    +   * Use data with desired wrapper.
    

    We should create a separate change record for this with instructions how to avoid break after this BC layer gets removed. Once we've done that, add the link here for more background information. 📝

  4. +++ b/core/misc/ajax.es6.js
    @@ -979,6 +979,52 @@
    +   * @todo: Add trigger an error after it will be possible. See
    ...
    +   * @todo: Add trigger an error after it will be possible. See
    

    Nit pick: we don't want to trigger an error but a deprecation warning 🕵️‍♂️

  5. +++ b/core/misc/ajax.es6.js
    @@ -1025,39 +1071,22 @@
    +      // For backward compatibility, in some cases a wrapper will be added. Use
    +      // theming to change it. See https://www.drupal.org/node/2940704.
    

    It would be probably good to mention one more time that this will be removed before Drupal 9.0.0 🧐

Anonymous’s picture

Status: Needs work » Needs review
StatusFileSize
new29.66 KB
new4.05 KB

@lauriii thank you for your long-term helps with it too!

385.1: Done (i hope)
385.2: Done.
385.3: Done. https://www.drupal.org/node/2974880
386.4: Done.
385.5: Done (i hope)

+ Nit clean up in AjaxTestController.

lauriii’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new29.72 KB
new4.05 KB

Some minor improvements to the documentation. I also removed the class from the div added as wrapper. Other than that looks good for me!

webchick’s picture

I spoke to @lauriii about this, and he said that given the trickiness here, it could use an additional framework manager review. Tagging for that.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 387: 736066-387.patch, failed testing. View results

dillix’s picture

Status: Needs work » Reviewed & tested by the community
alexpott’s picture

StatusFileSize
new888 bytes
new29.32 KB

Re-rolled and fixed a code style issue.

lauriii’s picture

Saving the issue credit

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 391: 736066-391.patch, failed testing. View results

GrandmaGlassesRopeMan’s picture

Status: Needs work » Needs review
StatusFileSize
new30.78 KB
new1.46 KB

- fixes failures from #391.

justafish’s picture

Status: Needs review » Reviewed & tested by the community

👌

  • lauriii committed 5f0f49d on 8.6.x
    Issue #736066 by droplet, drpal, vaplas, dmsmidt, effulgentsia, nod_,...
lauriii’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -blocker, -Needs framework manager review +v6.x-1.12 blocker

Thank you everyone for the amazing work on this issue. The most recent change has been reviewed by multiple subsystem maintainers and droplet. We've also provided a BC layer for the animation problem that has been a long standing concern on the issue.

Committed 5f0f49d and pushed to 8.6.x. 🚀 Thanks!

lauriii’s picture

Issue tags: -v6.x-1.12 blocker

Oops, I didn't mean to tag this as a v6.x-1.12 blocker.

lauriii’s picture

Issue tags: +⬅️✌️➡️

⬅️✌️➡️

dmsmidt’s picture

Awesome! Good job getting this over the finish line after so long!

dillix’s picture

Will this be backported to D7?

andypost’s picture

D7 needs separate issue

nod_’s picture

Great work, thanks a lot for getting this one done!

jibran’s picture

When did we stop backporting bug fixes(major in this case) to 8.5.x? I know in some cases we don't backport bugs but can core committer please state the reason of not backporting in the commit comment? Thanks!

abacaba’s picture

Great! Thank you for your work! :)

lauriii’s picture

@jibran bug fixes where the benefits out-weights the potential risk of disruption are generally backported to the previous minor branch to be included in the next patch release. I decided not to backport this issue to 8.5.x. because of the risks involved with the markup change. I will pay more attention in future to include the reasons behind the decision to not backport certain issues.

jibran’s picture

wim leers’s picture

💦💦💦💦💦💦

(That's the sweat that was poured into this issue.)

🎉🎉🎉🎉🎉🎉

(That's the celebration for seeing this finally fixed, after nearly 8.5 years.)

Having this fixed also fixed #2738685: BigPipe leaves wrapper <div>s in place, because AJAX system leaves them — this can cause some CSS to break (the only BigPipe bug!) and #2913563: Facets blocks loaded with BigPipe wrapped in unwanted <div> due to bug in AJAX system (Facets was affected by the BigPipe bug which really was this bug.)

Status: Fixed » Closed (fixed)

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

dave reid’s picture

So one thing to be aware of for this, is that with multi-element AJAX inserts, those elements are now root-level elements when the Drupal JS behavior attachments are run. When you use $('.my-element', context); as the selector and .my-element is a root-element loaded via AJAX, this element can no longer be found. This caused a regression in the Token module: #2994671: Token modal unusable with Drupal 8.6. I don't think this behavior of jQuery selectors was considered or mentioned in the change record. I would anticipate more modules that load contents via AJAX and then process the content via JS behavior attach to have regressions.