Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: ParentNode.replaceAll() / ParentNode.replaceChildren() #478

Closed
jonathantneal opened this issue Jul 18, 2017 · 60 comments · Fixed by #851
Closed

Proposal: ParentNode.replaceAll() / ParentNode.replaceChildren() #478

jonathantneal opened this issue Jul 18, 2017 · 60 comments · Fixed by #851
Labels
addition/proposal New features or enhancements needs concrete proposal Moving the issue forward requires someone to figure out a detailed plan needs tests Moving the issue forward requires someone to write tests

Comments

@jonathantneal
Copy link

jonathantneal commented Jul 18, 2017

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);
}
@domenic
Copy link
Member

domenic commented Jul 18, 2017

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?

@jonathantneal
Copy link
Author

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

@rianby64
Copy link

IMHO, by using .innerHTML = ""; is something that aesthetically looks strange 😄 Always I was wondering how to clean the inner HTML via API.

@domenic
Copy link
Member

domenic commented Jul 18, 2017

@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.

@jonathantneal
Copy link
Author

@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).

@domenic
Copy link
Member

domenic commented Jul 18, 2017

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

@rianby64
Copy link

@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 = '';
};

@jonathantneal
Copy link
Author

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.

@domenic
Copy link
Member

domenic commented Jul 18, 2017

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()).

@jonathantneal
Copy link
Author

@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.

@annevk
Copy link
Member

annevk commented Jul 19, 2017

You can also use textContent. Performance should be identical.

@annevk
Copy link
Member

annevk commented Jul 19, 2017

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.

@dmethvin
Copy link

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/

@snuggs
Copy link
Member

snuggs commented Jul 24, 2017

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

@annevk
Copy link
Member

annevk commented Jul 24, 2017

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

@jonathantneal
Copy link
Author

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.

@domenic
Copy link
Member

domenic commented Jul 26, 2017

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

@rianby64
Copy link

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

@annevk
Copy link
Member

annevk commented Jul 26, 2017

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

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

@jonathantneal
Copy link
Author

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

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

@pkozlowski-opensource
Copy link

@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).

@pkozlowski-opensource
Copy link

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....

@rianby64
Copy link

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.

@jonathantneal
Copy link
Author

jonathantneal commented Jul 26, 2017

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.

@annevk
Copy link
Member

annevk commented Jul 26, 2017

@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.

@jonathantneal
Copy link
Author

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()

@annevk
Copy link
Member

annevk commented Jul 26, 2017

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.

@annevk
Copy link
Member

annevk commented Jul 26, 2017

Exposing "replace all" sounds interesting.

@esprehn
Copy link

esprehn commented Nov 15, 2017

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.

@annevk annevk added needs concrete proposal Moving the issue forward requires someone to figure out a detailed plan needs tests Moving the issue forward requires someone to write tests and removed needs implementer interest Moving the issue forward requires implementers to express interest labels Apr 11, 2019
@annevk
Copy link
Member

annevk commented Apr 11, 2019

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

@jonathantneal
Copy link
Author

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

@jonathantneal
Copy link
Author

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.

@rniwa
Copy link
Collaborator

rniwa commented Jun 5, 2019

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

@snuggs
Copy link
Member

snuggs commented Feb 10, 2020

@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

snuggs added a commit to snuggs/dom that referenced this issue Feb 14, 2020
annevk pushed a commit that referenced this issue Apr 20, 2020
mfreed7 pushed a commit to mfreed7/dom that referenced this issue Apr 27, 2020
@hanguokai
Copy link

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

@domenic
Copy link
Member

domenic commented Apr 5, 2021

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.

@hanguokai
Copy link

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.

@dead-claudia
Copy link

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

@FergoTheGreat
Copy link

FergoTheGreat commented Jun 22, 2024

Is it just me, or is ParentNode.replaceChildren() problematic because of the choice to pass the children as a variable number of arguments? In practice, this limits the number of children you can append to ~120K on current Chrome before you get a RangeError: Maximum call stack size exceeded. Maybe you think 120K is enough children for any parent, but it seems poorly designed for an append function to cause a stack overflow if you try to append an arbitrarily large number of elements. I think it would have been better to take a single array of elements as input. This criticism is the same I have for String.fromCharCode.

@dead-claudia
Copy link

dead-claudia commented Jun 24, 2024

@FergoTheGreat Is there a reason you're not using Canvas?

An empty DOM element in Chrome requires around 1KB empty last I checked, and 120k of just those, without nested children, comes out to 0.1-0.2 GiB.

If you need to constantly replace nodes like that, try Canvas 2D. That gives you an immediate mode API where you can just blank the screen (context.clearRect(0, 0, canvas.width, canvas.height)) and re-draw everything any time you need to update. It's a very simple and intuitive API once you learn it, and it's one I'm surprised hasn't been ported to death in native-targeting languages like some Web APIs (like URL) have.

Edit: you'll get far better performance this way as well. You may notice it suddenly gets buttery smooth with this.

And you aren't the first one I had to advise to do this - once had another trying to use the DOM to manage a live SVG with 200k+ nodes and was starting to run into memory limitations around 250k nodes.

@esprehn
Copy link

esprehn commented Jun 24, 2024

In general we didn't optimize for appending numbers of elements that large because the overhead of creating the elements, computing the style, layout and paint ends up being so far outside what's reasonable for a frame duration on the main thread. To have reasonable web perf metrics you'd need to chunk the append way before the stack overflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs concrete proposal Moving the issue forward requires someone to figure out a detailed plan needs tests Moving the issue forward requires someone to write tests