Skip to content


Subversion checkout URL

You can clone with
Download ZIP


[1.9] make replaceWith() clone elements where required #920

wants to merge 2 commits into from

4 participants


I thought I'd try jQuery over the new comprehensive test suite we have for the various insertion methods in Bonzo and discovered that jQuery's replaceWith() wasn't properly cloning where required.

Inside replaceWith() this.each() is used which then passes a singular collection for each element in this to before() or append() which then invokes domManip(), the clone check inside domManip() (see iNoClone = results.cacheable || l - 1 -- l always == 1).

This change pulls the call to domManip() up into replaceWith() with the basic logic that used to be performed by before() and append() used where appropriate.

With love from @Ender!


Sorry, here's a quick test for you on current jQuery to see what on earth I'm talking about:

jQuery('<div class="replaceable">foo</div><div class="replaceable">bar</div>')
  .replaceWith(jQuery('<span class="replacement">foo</span><span class="replacement">bar</span>'));

console.log(jQuery('.replaceable, .replacement').map(function () { return this.tagName + '[' + this.className + ']' }));

You should see 4 spans rather than 2.


Is there a ticket for this?

((9 lines not shown))
- jQuery( this ).remove();
@mikesherov Collaborator

Why did this move to after the manipulation?

rvagg added a note

Good question. (1) because I had a bad call to next.parentNode.insertBefore( elem, this ); (should have been insertBefore( elem, next )) which has been fixed in the latest commit. Also because of the nodeType === 1 check, it seemed appropriate to remove regardless of whether the insertion happened or not so doing it below was easiest. Anyway, the nodeType issue is for you guys to sort out so I'll leave that one with you. In the meantime I've fixed the insertBefore() and moved the remove() up in the latest commit: f698ccbb9a.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
((9 lines not shown))
- jQuery( this ).remove();
+ if ( this.nodeType === 1 ) {
@mikesherov Collaborator

Why are we not allowing replaceWith on documentFragments (or other nodeTypes for that matter?)

@mikesherov Collaborator

More specifically, the jQuery( this ) .remove() needs to come before appending for the other node types that replaceWith can be called on. The fact that this doesn't break a unit test only shows that our unit tests aren't covering enough cases for replaceWith... I only know this because of the work done in #924

@dmethvin, @rwldrn, thoughts on correct behavior here?

@dmethvin Owner
dmethvin added a note

I did a quick look when i was trying to solve the "$('<div>abc</div>') has a parentNode" problem in 1.8.1 and just decided the whole method was too far gone to clean it up until 1.9. Since the isDisconnected() method doesn't return false for html conjured from strings, it doesn't have its intended non-effect there--but why should it be a no-op for that case anyway? Also, it's one of the few methods that do a conditional pushStack() which seems totally wrong because you can't reliably do $(selector).find(something).replaceWith(content).end() and be assured you're in the right place. Finally, it only accepts its first arg as content and not all its args like the other ap/pre-penders, although that could be fixed by passing arguments instead of [ value ] in this patch. Plus Rod's bug. So yeah we need much better test coverage here.

In general we don't reliably support anything but nodeType===1 in collections do we? If it works on other node types that's great, but if there's extra work required to support replacing comment and text nodes I'd prefer not to do it.

@rwaldron Collaborator
rwaldron added a note

I had a similar feeling when I was looking at the same bug. The function is FUBAR'ed and I'm inclined to halt any further changes.

@mikesherov Collaborator

@dmethvin, for the record, prepend and append work on nodeType 11 (documentFragment)...

rvagg added a note

fwiw, I was following the lead of append() which is currently used in replaceWith() although I missed the fact that it was also checking for 11, so I've fixed that in f698ccb, however, before() doesn't do such a check. I'm not sure what the reasoning with these is, if there's a good reason to restrict to 1 and 11 in appendChild() cases but not in insertBefore() then perhaps the 1 & 11 check should be moved to the else above the appendChild() call?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

This may not be landed as-is since there are some other cleanups and fixes to be done in 1.9, but the discussion is useful. This isn't a critical enough bug to risk a fix in 1.8.2, it's been around for a while.


Hey @rvagg can you rebase this? I'd like to land it in 1.9.


np @dmethvin, done

rvagg added some commits
@rvagg rvagg make replaceWith() clone elements where required
Bypassing domManip() for the whole collection skips the check that
determines whether the element needs to be cloned or not.
Unless the replacement is a non-node (e.g. '<foo>'), it needs to
be cloned where it's used >1 times.
@rvagg rvagg remove old before inserting new, allow nodeType 11 7e72368
@rwaldron rwaldron closed this in 551c2c9
@tp9 tp9 referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 9, 2012
  1. @rvagg

    make replaceWith() clone elements where required

    rvagg authored
    Bypassing domManip() for the whole collection skips the check that
    determines whether the element needs to be cloned or not.
    Unless the replacement is a non-node (e.g. '<foo>'), it needs to
    be cloned where it's used >1 times.
  2. @rvagg
This page is out of date. Refresh to see the latest.
Showing with 27 additions and 19 deletions.
  1. +17 −19 src/manipulation.js
  2. +10 −0 test/unit/manipulation.js
36 src/manipulation.js
@@ -276,32 +276,29 @@ jQuery.fn.extend({
value = jQuery( value ).detach();
- this.each( function( i ) {
- var next = this.nextSibling,
- parent = this.parentNode,
- // HTML argument replaced by "this" element
- // 1. There were no supporting tests
- // 2. There was no internal code relying on this
- // 3. There was no documentation of an html argument
- val = !isFunc ? value : this, i, this );
+ return this.domManip( [ value ], true, function( elem, i ) {
+ var next, parent;
if ( isDisconnected( this ) ) {
- // for disconnected elements, we replace with the new content in the set. We use
- // clone here to ensure that each replaced instance is unique
- self[ i ] = jQuery( val ).clone()[ 0 ];
+ // for disconnected elements, we simply replace with the new content in the set
+ self[ i ] = elem;
- jQuery( this ).remove();
+ if ( this.nodeType === 1 || this.nodeType === 11 ) {
+ next = this.nextSibling;
+ parent = this.parentNode;
- if ( next ) {
- jQuery( next ).before( val );
- } else {
- jQuery( parent ).append( val );
+ jQuery( this ).remove();
+ if ( next ) {
+ next.parentNode.insertBefore( elem, next );
+ } else {
+ parent.appendChild( elem );
+ }
- });
- return this;
+ });
detach: function( selector ) {
@@ -356,7 +353,8 @@ jQuery.fn.extend({
i === iNoClone ?
fragment :
- jQuery.clone( fragment, true, true )
+ jQuery.clone( fragment, true, true ),
+ i
10 test/unit/manipulation.js
@@ -1205,6 +1205,16 @@ test("replaceWith(string) for collection with disconnected element", function(){
equal(newSet.filter("span").length, 1, "ensure the new element is in the new set");
+// #12449
+test("replaceWith([]) where replacing element requires cloning", function () {
+ expect(2);
+ jQuery("#qunit-fixture").append("<div class='replaceable'></div><div class='replaceable'></div><div class='replaceable'></div>" );
+ // replacing set needs to be cloned so it can cover 3 replacements
+ jQuery("#qunit-fixture .replaceable").replaceWith( jQuery("<span class='replaced'></span><span class='replaced'></span>") );
+ equal( jQuery("#qunit-fixture").find(".replaceable").length, 0, "Make sure replaced elements were removed" );
+ equal( jQuery("#qunit-fixture").find(".replaced").length, 6, "Make sure replacing elements were cloned" );
test("replaceAll(String|Element|Array<Element>|jQuery)", function() {
jQuery("<b id='replace'>buga</b>").replaceAll("#yahoo");
Something went wrong with that request. Please try again.