Dom: Proposal: ParentNode.replaceAll() / ParentNode.replaceChildren()

Created on 18 Jul 2017  ·  57Comments  ·  Source: whatwg/dom

The ParentNode.replaceAll() or ParentNode.replaceChildren() method would remove all child nodes from the ParentNode while allowing the insertion of a new set of nodes. The best name is yet settled upon.

A “replace all” concept already exists in the spec.

Syntax

ParentNode.replaceAll();
ParentNode.replaceChildren();

Example:

  1. In a form, changing a Country <select> causes options to change in a corresponding Region <select>:
countrySelectElement.addEventListener('change', () => {
  regionSelectElement.replaceAll(...optionElementsById[countrySelectElement.value]);
});
  1. Any time an element is to be emptied.
elementToBeEmptied.replaceChildren();

Rationale:

Like append(), prepend(), before(), after(), and replaceWith(), we’re allowing developers to do something useful without a loop, e.g:

// remove all child nodes from the ParentNode
while (parentNode.lastChild) {
  parentNode.removeChild(parentNode.lastChild);
}

// insert a new set of Node objects or DOMString objects
parentNode.append(...addedNodes);

Fallback Strategies

There are 2 common strategies to empty a ParentNode before inserting a new set of nodes (using ParentNode.append).

The 1st strategy is to erase the innerHTML of a ParentNode. This is less meaningful and leads one to question the underlying implementation.

ParentNode.innerHTML = ''

The 2nd strategy is to loop over and remove each child of the ParentNode. This is verbosE and leads one to wish for the same loop-free “sugar” provided to append(), prepend(), before(), after(), and replaceWith().

while (parentNode.lastChild) {
  parentNode.removeChild(parentNode.lastChild);
}

Previous proposal (replaceAll)

A previous proposal named this method replaceAll, a name that mirrors a section in the spec. Recent feedback has been that replaceChildren would be a more accurate and less confusing name. Therefore, both are represented in this proposal.


Original Proposal (empty)

The original proposal’s functionality is fully captured in the updated proposal. The updated proposal also reflects an existing concept in the spec.

The ParentNode.empty() method would remove all child nodes from the parent node.

Syntax

ParentNode.empty();

Example:

In a filtering component, a change to one <select> swaps out the <option> elements available to a second <select>:

selectEl1.addEventListener('change', () => {
  selectEl2.empty();

  selectEl2.append(...optionElsByValue[selectEl1.value]);
});

Rationale:

Like append(), prepend(), before(), after(), and replaceWith(), we’re allowing developers to do something useful without a loop, e.g:

while (parentNode.lastChild) {
  parentNode.removeChild(parentNode.lastChild);
}
additioproposal needs concrete proposal needs tests

Most helpful comment

I’ll do that. I may need help along the way.

All 57 comments

Most of the time I just use .innerHTML = "".

I guess the value-add here would be for parent nodes that are not elements, so documents and document fragments. Do you often need to empty such nodes in your code? Could you give an example?

@domenic, yes, an example comes to mind of re-populating <option> elements in an existing <select>. I’ve added an example to the proposal.

IMHO, by using .innerHTML = ""; is something that aesthetically looks strange :smile: Always I was wondering how to clean the _inner HTML_ via API.

@jonathantneal your example could be replaced with selectEl2.innerHTML = "", so it still doesn't help justify this proposal.

@rianby64 The idea that the existing platform functionality "looks strange" is not a good justification for introducing a new feature into the web platform, with all the attendant implementation effort, testing effort, and speccing effort, not to mention the waiting that all web developers have to go through before it is available in all their target browsers.

@domenic, I think the strangeness of innerHTML = '' is that one is switching from working with a DOM tree to working with an HTML parser, similar to document.write (if the ParentNode-ness of my proposal holds up).

OK. So there is no actual use case for this? Then my answer about how that's not a good justification remains.

@domenic , it was my humble opinion about the @jonathantneal 's idea. For me is logic to expect something that empties the content of a tag. The name of empty() may suggest that browser is removing every child from its parent, including listeners and so on. In the other hand, innerHTML looks like a DOM hack. However...

Node.prototype.empty = function() {
  this.innerHTML = '';
};

The justifications are to not use a loop (selectEl.empty(), document.empty()), detach children without re-parsing the element (no innerHTML = '', Related StackOverflow), and to match prior art (jQuery, Bliss.js).

If innerHTML = '' (and I suppose document.open(); document.close()?) is the preferred semantic method to empty a parent node, then feel free to close this issue.

There's no such thing as "re-parsing an element". innerHTML = "" does the same thing as your proposed .empty().

I'll let @annevk make the call, but yes, I am pretty sure that is the preferred method for emptying an element. For documents, you've yet to present an example use case where you want to empty a document (as opposed to e.g. creating a new one using new Document()).

@domenic, understood. Thank you.

You’ve indicated I do not have an example for emptying documents.

I’ve only needed to empty iframe documents.

iframe.contentDocument.empty()

Being fully transparent, I only do this for creating “clean” shadow-dom-like fallbacks. And, admittedly, I see people using strings (via srcdoc) for this as well: https://codepen.io/chriscoyier/pen/rwvjVN

Perhaps it’s old instincts, but I really thought target.innerHTML = '' uses the HTML parser on target. I was also under the impression (from the Stack Overflow reference above regarding emptying a node with innerHTML) that this is much slower and should be avoided. My apologies if I’ve misunderstood this technology — it was not my intention.

You can also use textContent. Performance should be identical.

We've had this proposed once before and the main argument then too was aesthetics. I guess it's worth considering, but it's also rather risky to add something like empty() as it'll clash with userland functions.

There are likely to be web compat issues here, since PrototypeJS defines the same name on Element but has _very_ different semantics.

http://api.prototypejs.org/dom/Element/empty/

@annevk great input. Out of curiosity what does happen to handlers on discarded nodes from target.innerHTML = ''? Assuming cleanup on next GC run.

@snuggs if there are no other references to the nodes or handlers, yes.

Didn’t realize we’re still working around prototype. Sure, sure, I need no convincing. How about ... ParentNode.removeChildren()?

Are these performance statistics still relevant? If so, it definitely seems like this kind of functionality would still be helpful.

I think the performance statistics are a good argument for filing a bug on various browsers. Please do so, if you can!

If removeChildren looks good to you, then appendChildren may have a chance to be in the spec too.

Other than being slightly clearer, removeChildren() doesn't seem particularly ergonomic:

x.removeChildren();
x.textContent = "";
x.innerHTML = "";

Thanks. Well, in PostCSS, it’s called removeAll().

x.removeAll();
x.textContent = "";
x.innerHTML = "";

@annevk @domenic if I understand correctly the main argument for _not_ adding method proposed here is "hey, we've got something like this already in the form of x.textContent="" or x.innerHTML="" and existing solution are as concise to write as x.removeChildren()".

While I completely agree with this reasoning IMO the "ergonomics" should be seen in the broader context and not only number of characters to type. For me the _main_ issue with lack of sth like x.removeChildren() is that people spending brain cycles (to figure out _which_ method to use) and CPU cycles (to benchmark various methods of clearing up nodes). As an example you can check a popular JS framework benchmark that _agonizes_ over various ways of removing all children of a given DOM element: https://github.com/krausest/js-framework-benchmark/blob/f1cdea5d9f8472fe12148f079ea9623915a6c498/vanillajs-non-keyed/src/Main.js#L262-L284

I'm completely subscribing to the idea of keeping the platform APIs as simple and small as possible. But IMO the addition proposed here is valuable not because it will save few keystrokes - for me the main benefit would be a "platform-blessed" way of clearing children. This in turn could people less anxious about having too many choices and would ultimately make people happier :-)

I guess tl'dr; is that I would like to add +1 for the proposal (whatever the final method name is).

I think the performance statistics are a good argument for filing a bug on various browsers. Please do so, if you can!

@domenic are you saying that people should file perf-related issues if different ways of clearing child nodes perform differently (ex. removeChild in a loop slower as compared to using innerText)? Or just file bugs if we have numbers that _suggest_ removal being slow?

I'm asking since in a test / benchark app I'm using removal of _all_ (~1500) nodes take almost 1/2 time needed to create and attach those nodes in JS (~10ms to create nodes vs. ~4ms to delete them). It _seems_ a lot but without having one "blessed" method of clearing children nodes and without knowing underlying implementation details it is hard to draw any conclussions that would justify opening issues on browsers side....

removeChildren() has more meaning as it opens the idea to add appendChildren(NodeList) instead of using, e.g.

[...document.querySelectorAll('div')].forEach(node => document.body.appendChild(node));

I know there are many ways to do append/remove of NodeList, but with these functions implementers may choose the best way to perform removeChildren and appendChildren.

We already have append() for easily appending elements. It’s in my opening post. https://dom.spec.whatwg.org/#dom-parentnode-append

I recommended an alternative name, removeAll(), only as I thought @annevk suggested I’d need a shorter method name, and @dmethvin suggested empty() is failed due to Prototype in the wild.

@pkozlowski-opensource removeChild() in a loop will be slower since it doesn't use the https://dom.spec.whatwg.org/#concept-node-replace-all path and therefore ends up creating way more mutation records. textContent = "" and innerHTML = "" should be equivalent in performance however and should also be the fastest. If that's not the case that'd be a bug of sorts. I would consider the latter two the current blessed way to remove all children (given the code path they take), but I can sympathize with the desire for a clearer endpoint.

The slowest way to empty a Node _should_ be the fastest, therefore, we recommend the slowest way? I get it, sort of. Reality vs Theory. Does that close out this issue?

Kudos to the people who have time to convince browser vendors to make innerHTML the fastest.

Perhaps, if replace all is a concept, browsers can just expose some form of replaceAll()

Slowest? Pointer? Per the link above textContent is the fastest. If innerHTML has different performance that would be a bug I think. No real reason for such a difference.

Exposing "replace all" sounds interesting.

fwiw setting textContent and setting innerHTML are not the same. documentElement.innerHTML = "" will insert a <head> and <body> so you're not left with no children. innerHTML invokes the parser and may actually insert children even when an empty string is passed, there's a number of elements with different parsing behavior. Doing .textContent = null (or empty string) will always remove all the children (ignoring mutation events) though.

It looks like Gecko has an optimization for innerHTML:
https://dxr.mozilla.org/mozilla-central/source/dom/base/FragmentOrElement.cpp#2493

and enumerates all the special cases for parsing:
https://dxr.mozilla.org/mozilla-central/search?q=%2Bcallers%3AnsINode%3A%3ASetHasWeirdParserInsertionMode%28%29

WebKit and Blink don't do that, but probably should. It still doesn't help in lots of cases though, for example it's disabled for tables which is a place where folks often clear all children to insert new rows.

I do think a removeChildren() API would make more sense though since it removes the attractive footgun of innerHTML with its special cases. The DOM is hard enough to use already, having to remember the right way to remove all the children shouldn't be something authors need to worry about.

@esprehn what do you think about the replaceAll() idea? Basically exposing the primitive textContent and innerHTML end up using. If we make it the same as before() and such it would end up removing all children if you don't pass it any nodes.

Actually use a loop is 100x faster...
See:

https://jsperf.com/innerhtml-vs-removechild/15

I'll note as a library maintainer (Mithril, a vdom-based framework), something like replaceAll or even clear would be useful for optimizing the case of patching an entire tree where the whole thing needs rewritten (think: m("div.foo", [cond ? m("div.bar") : undefined])). We already do similar optimizations for strings with textContent, but if we had a way to avoid a loop for that common case, that'd be nice.

(FWIW, browsers could optimize this case to just detaching the first and last child and setting previousSibling and nextSibling both for all the children to null, which is a lot quicker than what library writers could do. They could also more efficiently recalculate layout.)

I’ve updated the proposal from empty() to replaceAll(), which I feel reflects the direction our momentum was going. replaceAll() provides the same functionality I was looking for, improves upon it, and better fits the concept already in the spec.

I’ve left the original proposal below it, for context.

@cdumez @smaug---- @tkent-google what do you think?

Given that we added before(), after() (both really strangely named) and replaceWith() and remove() which are just syntactical sugar on top of older APIs, replaceAll() sounds quite reasonable to me.

I have requested feedback internally at Apple, I will let you know our position as soon as I hear back.

I'm not sure about naming though. replaceChildren() perhaps ?

Thanks for looking into this! I’ve found myself reaching for this functionality more as I work in the Shadow DOM, where I write and manipulate a lot of generated markup.

In case it matters, I have no opinion on the name; both seems great and anything more from me would probably be bikeshedding. 😄

Looks like feedback is positive from our side, although we'd like a less confusing name. We like @smaug---- 's replaceChildren() proposal better than replaceAll().

replaceChildren() is a bit confusing because it actually replaces all descendants... I think we should research precedent in user-space libraries. There's of course the ever-popular https://api.jquery.com/replaceAll/#replaceAll-target , which points toward the original replaceAll suggestion.

I also think short names are worth preserving; if the point of this function is ergonomics anyway, it's probably a good idea to keep them short.

I posted https://mobile.twitter.com/domenic/status/1113524670626660352

You don't replace descendants, but children. Those children may then have their own child nodes.

All the existing descendants get replaced, though, with new descendants.

I am with @smaug---- here, replaceChild() is pre-existing API and replaces both the child and its descendants. I think the naming would be consistent.

removeChild also removes the all descendants of a node but we don't call it removeDescendants. It's implicit that a child may or may not have more descendants.

A few responses to @domenic’s tweet poll caught my attention.

  1. parentNode.children returns only elements, which could lead to some misunderstanding when first encountering replaceChildren. However, replacing _only_ child elements with other content while also preserving sibling text nodes seems illogical. Also, I could not find any other pattern of non-specificity inferring _Elements_ in the DOM API.

  2. range.selectNodeContents, range.extractContents, and other range functions target elements, text, or a mixture of both. Following this pattern would lead to replaceContents, although this name was not an option in the poll.

Given replaceChild() and removeChild(), I think replaceChildren() would be consistent. setChildren() would also be acceptable if something shorter is preferred.

Contents is really only used by ranges, not nodes, and signifies that ranges "contain" something different from children. All only has an established meaning for retrieval APIs.

I still think replaceAll() is best personally, but given that my Twitter followers' intuition was pretty strongly in favor of replaceChildren() over replaceAll(), I am happy to consider myself as an out-of-touch jQuery fan and go with replaceChildren() instead.

I do think any Range-based precedents are not good given how Range is kind of a "bad part" of the platform.

Range isn't that bad? But a range can contain something smaller than an individual node, hence it using the word "contents" and not "children".

@jonathantneal are you interested in driving this? What remains here is a PR to the DOM Standard and a PR to WPT.

I’ll do that. I may need help along the way.

PR #755 has been made to add this functionality to the DOM spec (for those who just follow this conversation).

This is my first pull request to this repository, so my instinct is that corrections will need to be made. I will do my best to address corrections promptly and be patient with reviews.

As time permits, I will look into filing a remaining PR to WPT repository.

WebKit team at Apple supports the addition of this API. WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=198578

@rniwa IIRC I believe the API is supposed to be ParentNode.prototype.replaceChildren.
Will cut down on a bit of confusion as @annevk pointed out

Just find replaceChildren() method. It is awesome!
container.innerHTML = '' will cause page flicker, but replaceChild/replaceChildren won't.

container.innerHTML = '' will cause page flicker, but replaceChild/replaceChildren won't.

This isn't correct. They are both implemented using the exact same code paths in browsers, so any differences you are seeing are likely due to some other part of your setup.

This isn't correct. They are both implemented using the exact same code paths in browsers, so any differences you are seeing are likely due to some other part of your setup.

In theory, I think you're right.

// refresh all
container.innerHTML = '';
container.appendChild(createDom());
container.appendChild(createDom());
...

Above code is usually used to setup, and createDom() is a sync method. I often see flicker in Chrome, but replace doesn't flicker.

If that's the only variable, I'd recommend filing a bug against Chrome.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jakearchibald picture jakearchibald  ·  3Comments

Ginden picture Ginden  ·  4Comments

annevk picture annevk  ·  5Comments

annevk picture annevk  ·  9Comments

jonathanKingston picture jonathanKingston  ·  4Comments