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

Timing of cloning for the <selectedoption> element #10520

Open
josepharhar opened this issue Jul 25, 2024 · 38 comments
Open

Timing of cloning for the <selectedoption> element #10520

josepharhar opened this issue Jul 25, 2024 · 38 comments

Comments

@josepharhar
Copy link
Contributor

What is the issue with the HTML Standard?

The <selectedoption> element is discussed more generally here, and this issue is for a topic which has been split out: w3c/csswg-drafts#10242

When the author modifies content inside an <option> element, we will clone the contents of that <option> into the <selectedoption>, but the timing at which we do this cloning has not yet been decided.

The timing will be web observable. For example, if the author modifies an option and then tries to query the layout of <selectedoption>, they will have to wait until the cloning happens.

Some possible options for cloning timing are:

  1. Synchronously
  2. Microtask timing
  3. CustomElements reaction timing
  4. Something slower?

I think that we should do microtask timing because I am planning on using a MutationObserver internally to implement this, and MutationObserver already uses microtask timing: https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask

If we diverge from microtask timing, we would have to make a special kind of mutationobserver that does different timing, right?

@emilio @smaug----

@domenic
Copy link
Member

domenic commented Jul 25, 2024

MutationObserver kind of uses microtask timing, but it uses a very special sort, with the compound microtasks. I wonder if the difference will be observable. I think it will, as it will set the "mutation observer microtask queued" agent-wide flag, which will impact the behavior of other web developer-created MutationObservers.

@sanketj
Copy link
Member

sanketj commented Jul 26, 2024

+1 to microtask timing to avoid over-cloning

To @domenic 's point, I wonder whether we should use a special type of observer with special microtask timing to minimize observable differences in mutation observers created by web developers. The "clone into selectedoption" microtask could be queued after the regular mutation observer microtask, so that web devs don't see differences in the notify mutation observers step.

The downside is that approach does introduce more complexity, both conceptually and implementation wise. But that might be safer from a web compat perspective.

@smaug----
Copy link

The over-cloning would happen only if one mutates the content of the selected option, no? The normal case is that user selects one option and the contents get cloned once. So CEReaction or even more synchronous cloning might not be too bad in this case.

Microtasks were designed for MutationObserver, and the reason was to improve performance in cases when one does lots of DOM mutation all over the place. That is not quite the case here.

svg:use, at least in Gecko, is re-cloned when needed when layout is flushed, but of course we don't want to expose layout flush timing.

@sanketj
Copy link
Member

sanketj commented Jul 29, 2024

The over-cloning would happen only if one mutates the content of the selected option, no?

I think cloning would also need to happen if the select element APIs, ex. setting selectedIndex, are used to change the selected option. Authors can update the selected option this way several times before rendering happens.

@josepharhar
Copy link
Contributor Author

MutationObserver kind of uses microtask timing, but it uses a very special sort, with the compound microtasks. I wonder if the difference will be observable. I think it will, as it will set the "mutation observer microtask queued" agent-wide flag, which will impact the behavior of other web developer-created MutationObservers.

Chromium has already been using a regular MutationObserver to observe changes to child content in option elements, so I don't see how any behavior could be changed by re-purposing this MutationObserver to be used for <selectedoption>: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/forms/html_option_element.cc;l=58-87;drc=e61a3c997dd7d76fdc7fe44b4f20d210bdbd700e

So CEReaction or even more synchronous cloning might not be too bad in this case.

The prototype uses a synchronous mutation observer right now in chromium, and I think that the performance is quite bad because every dom mutation to any element in the whole tree requires us to start walking the dom tree to figure out if we need to update an option/selectedoption: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/forms/html_selected_option_element.cc;l=27-32;drc=11e2d7ffa05a985a084057676f9c76c6e88018fa

Using CEReaction sounds much better than synchronous, but I'm still inclined to just reuse MutationObserver the way it is.

@domenic
Copy link
Member

domenic commented Jul 31, 2024

Chromium has already been using a regular MutationObserver to observe changes to child content in option elements, so I don't see how any behavior could be changed by re-purposing this MutationObserver to be used for <selectedoption>:

The fact that Chrome is violating the spec currently by causing web developer mutation observers to fire at times the spec says are not allowed doesn't seem like a good reason to introduce more such violations.

@josepharhar
Copy link
Contributor Author

If we created an alternate set of MutationObserver infrastructure for user-agent MutationObservers, would that resolve this issue?

I'm trying to understand what it would mean for us to do CEReactions timing instead of microtask timing as well. Does it mean that all of the DOM algorithms that append nodes, remove nodes, or modify attributes would have a new step at the end of it to run user-agent MutationObservers?

@domenic
Copy link
Member

domenic commented Jul 31, 2024

If we created an alternate set of MutationObserver infrastructure for user-agent MutationObservers, would that resolve this issue?

Yes.

I'm trying to understand what it would mean for us to do CEReactions timing instead of microtask timing as well. Does it mean that all of the DOM algorithms that append nodes, remove nodes, or modify attributes would have a new step at the end of it to run user-agent MutationObservers?

In terms of observable effects, yes.

In terms of how you would implement it, I think you'd add a step wherever https://html.spec.whatwg.org/#invoke-custom-element-reactions is called. (Click on the definition to see all call sites.)

I'm not sure how much else of the CE reactions infrastructure we'd want to import... you'd probably need some sort of microtask timing backup for cases like contenteditable, similar to the backup element queue.

@josepharhar
Copy link
Contributor Author

Thanks! I will draft something to create an alternate set of MutationObserver infrastructure in the DOM spec.

In terms of how you would implement it, I think you'd add a step wherever https://html.spec.whatwg.org/#invoke-custom-element-reactions is called. (Click on the definition to see all call sites.)

I'm not sure how much else of the CE reactions infrastructure we'd want to import... you'd probably need some sort of microtask timing backup for cases like contenteditable, similar to the backup element queue.

I see, this sounds tricker than going with microtasks.

I think we should go with microtasks instead of CEReactions for the following reasons:

  • MutationObservers already use microtasks, so trying to create an alternate type of CEReactions MutationObserver would be harder to spec and harder to implement.
  • Performance will be better when imperatively building or modifying option elements due to fewer calls to clone all of the options contents into selectedoption elements.
  • As @dandclark said in the call, it will be easier to understand how this works because it matches the author defined API of MutationObserver. I think this also increases the likelihood that this element is polyfillable.

Microtasks were designed for MutationObserver, and the reason was to improve performance in cases when one does lots of DOM mutation all over the place. That is not quite the case here.

This might be true, perhaps authors won't be doing lots of imperative modifications to an option element subtree - although we are enabling all sorts of new use cases with the customizable select proposal. Likewise, perhaps authors won't be needing to synchronously query the state of the updated DOM content in the selectedoption element, and if they need to, they can easily queue a microtask or rAF to do so.

@emilio
Copy link
Contributor

emilio commented Aug 5, 2024

So to be clear the proposal is something like "any mutation to the selected option subtree queues a micro-task to re-clone the subtree, unless one is already queued", presumably?

That seems fair, if we're fine with all the relevant weirdness that it causes. It might indeed be the less-bad option...

@josepharhar
Copy link
Contributor Author

So to be clear the proposal is something like "any mutation to the selected option subtree queues a micro-task to re-clone the subtree, unless one is already queued", presumably?

Yes, that sounds correct

chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Aug 5, 2024
This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Aug 5, 2024
This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
aarongable pushed a commit to chromium/chromium that referenced this issue Aug 5, 2024
This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5758741
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1337462}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Aug 5, 2024
This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5758741
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1337462}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Aug 5, 2024
This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5758741
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1337462}
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Aug 8, 2024
…testonly

Automatic update from web-platform-tests
Improve <selectedoption> performance

This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5758741
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1337462}

--

wpt-commits: d32c223215aaa166fce5162b4f76d323e9d513e7
wpt-pr: 47467
i3roly pushed a commit to i3roly/firefox-dynasty that referenced this issue Aug 12, 2024
…testonly

Automatic update from web-platform-tests
Improve <selectedoption> performance

This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5758741
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1337462}

--

wpt-commits: d32c223215aaa166fce5162b4f76d323e9d513e7
wpt-pr: 47467
ErichDonGubler pushed a commit to erichdongubler-mozilla/firefox that referenced this issue Aug 19, 2024
…testonly

Automatic update from web-platform-tests
Improve <selectedoption> performance

This patch improves the performance of <selectedoption> by replacing the
SynchronousMutationObserver with the existing async MutationObserver in
HTMLOptionElement.

The change from sync to async impacts some tests. The timing is being
discussed in a standards issue here:
whatwg/html#10520

Fixed: 336844298
Change-Id: I9693de9cf35913e7daaebb364c4923dcd4a2dc39
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5758741
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1337462}

--

wpt-commits: d32c223215aaa166fce5162b4f76d323e9d513e7
wpt-pr: 47467
@josepharhar
Copy link
Contributor Author

@smaug---- do you have any additional feedback? I'd like to get started on this, and I'd still prefer to use microtask timing.

@annevk
Copy link
Member

annevk commented Aug 29, 2024

Are we also guarding the cloning on the element being connected? I could see synchronously working as well. Either way there will be some weirdness as this is novel behavior.

@smaug----
Copy link

Using microtasks for this does feel odd. One couldn't check the value of the select after modifying the selected option.
And innerHTML and what not would give "wrong" serialization. Feels rather error prone.

And I don't understand "the performance is quite bad because every dom mutation to any element in the whole tree requires us to start walking the dom tree to figure out if we need to update an option/selectedoption". That sounds like some limitation in Blink. In Gecko we'd just observe changes at <option> level.

@josepharhar
Copy link
Contributor Author

Are we also guarding the cloning on the element being connected?

Not cloning when the element is disconnected sounds like a good idea to me.

One couldn't check the value of the select after modifying the selected option.

select.value would still reflect the newly selected option synchronously.

And innerHTML and what not would give "wrong" serialization

If you were to serialize the contents of the <selectedoption> element itself, then yeah you'd have to wait a microtask. This doesn't seem like a typical use case to me.

And I don't understand "the performance is quite bad because every dom mutation to any element in the whole tree requires us to start walking the dom tree to figure out if we need to update an option/selectedoption". That sounds like some limitation in Blink.

This was an issue with the initial implementation which did things synchronously. I'll try studying what the web-exposed MutationObserver does to see if there are any optimizations which could be rebuilt into a synchronous mutation observer.

I do prefer doing it synchronously over CEReactions timing because figuring out how to make a new type of MutationObserver which operates at that timing sounds difficult to implement and very difficult to spec, but ideally I'd still use a normal MutationObserver with microtask timing.

@josepharhar josepharhar added the agenda+ To be discussed at a triage meeting label Sep 3, 2024
@josepharhar
Copy link
Contributor Author

I've thought some more about this, and I think I understand how we could leverage CEReactions to only do one clone per script call to a DOM api which performs a mutation.

I could create a special kind of MutationObserver which instead of queueing a microtask looks to see if there is a CE reactions stack present, and tells that CE reactions stack to "notify" this MutationObserver when it is popped. If there is no CE reactions stack present, then just clone synchronously.

I wonder if this will be really slow when parsing though when the parser does one mutation at a time. Presumbaly there is no CE reactions stack present when we are just doing the initial parsing of the HTML for the document.

I also wonder if using CEReactions like this is just an internal optimization to run clones less often and is functionally the same as just synchronously cloning every time, in which case we could make the spec a lot simpler and keep it in the DOM spec. Maybe doing anything with MutationObservers is also just an optimization, and we could just add steps to the insertion/removal/attributechange steps in the HTML spec to do the cloning when appropriate...?

@josepharhar
Copy link
Contributor Author

To add some more context to my last comment, here are some notes about how I'd want to implement this in blink:

We have a "CEReactionsScope" class which is stack allocated by generated bindings code every time a method with the [CEReactions] IDL is called from script. It can be globally accessed by anything which wants to enqueue CE reactions stuff. In its destructor, which should be run right before the IDL method returns control back to script, it invokes reactions on a CustomElementReactionStack.

In order to implement this cloning at CEReactions time, I'd want to identify DOM mutations which should trigger a cloning of the selected <option> element into <selectedoption>, and then enqueue a task to do this cloning into the CEReactionsScope destructor or into something inside the CustomElementReactionStack.

I'm not sure if the "CEReactionsScope" class has an equivalent thing that exists in the spec. I'm also not sure if we will always have a context available during HTML parsing. I can see that we create a CEReactionsScope when running CreateElement for a custom element and when the parser inserts elements.

@smaug----
Copy link

I'm not sure if CEReaction is any better (or worse) to synchronous cloning (and I think I'd implement this as synchronous cloning).

Parsing is an interesting case. No matter what timing is used, cloning might need to happen several times, once for the first <option> being parsed/created and once when possibly later <option selected> is parsed/created - those might happen in different tasks.
What should happen if the network packet boundary is right in middle of an <option>? I guess elements could use a blocked-on-parser flag. Having that on <select>certainly wouldn't work since there are cases when select has thousands of options.

Still pondering issues around microtasks:
Use of microtasks for cloning might lead to some other weird cases, like scheduling first some promises, then modify the value of an option and then schedule more promises. Those first promises would still see the old content, but latter promises new. Rather surprising. And that "modify the value" applies to all the changes, removing selected options, or selecting a new one etc.

@josepharhar
Copy link
Contributor Author

Based on the WHATNOT discussion, I'm going to pursue synchronous cloning

@past past removed the agenda+ To be discussed at a triage meeting label Sep 12, 2024
josepharhar added a commit to josepharhar/html that referenced this issue Sep 18, 2024
The `<selectedoption>` element is part of the customizable `<select>`
proposal: whatwg#9799

It allows authors to declaratively clone the contents of the currently
selected `<option>` of a `<select>` and style it independently for use
in a base appearance `<select>`'s button.

The timing of cloning has been discussed here:
whatwg#10520

The selectedoption element has been discussed generally here:
w3c/csswg-drafts#10242
@josepharhar
Copy link
Contributor Author

I created a spec pr for selectedoption which has synchronous timing here: #10633

@annevk
Copy link
Member

annevk commented Sep 19, 2024

https://x.com/ElliottZ/status/1836512040120123593 by @esprehn raises an interesting issue with cloning in general. This also makes me wonder what will happen with <img> or <video>. I suspect it's not going to be doing what you'd want as a web developer.

@jakearchibald
Copy link
Contributor

jakearchibald commented Sep 19, 2024

Also things where the full state isn't represented via tree & attributes, such as <input>, <canvas>, <iframe> etc.

I don't have good answers here. Some thoughts:

  1. Use deep slot assignment, but this means the selected option disappears when the menu is open (since it can only appear in one place at a time).
  2. Enhance the above to allow something to be slotted in two places at once, however that might work.
  3. A texture-copy of the item in the menu (like element()), but this means it can't be styled differently.
  4. A system where the selected option is represented with simple text, although can be enhanced with JS to display rich content, where the JS is responsible for outputting the correct selected item.

To expand on 4:

<style>
  select, ::picker(select) {
    appearance: base-select;
  }
</style>
<select>
  <option></option>
</select>

In the above, the options can have rich content, but the rendering of the button is UA-controlled, and will just be the textContent of the selected input (or whatever).

<style>
  select, ::picker(select) {
    appearance: base-select;
  }
</style>
<select>
+ <button>…</button>
  <option>…</option>
</select>

In this example, the presence of the button means the developer is opting in to controlling the rendering of the button. This includes representing the selected item however they wish, but none of it is automatic.

This avoids the cloning magic, but it means you can't have rich content in the button without JS.

@justinfagnani
Copy link

Apologies if this is piling on to the wrong issue, since this issue is originally about cloning timing, it just happens to be talking about cloning in general now: I have a thought about where the clone is placed as well. This may warrant another issue.

Cloning into the light DOM might lead to problems with libraries that try to keep the DOM in sync with their state of the world. They could remove the cloned content since they didn't create it. Luckily, React 18 seems to work fine, possibly because the content is in a child element so the diff just skips it.

But more generally it strikes me that cloning into a shadow root on <selectedoption> would be a possibly more robust option, except that style encapsulation prevents styling the options and the selected option in the same way as the option.

This is similar to a problem we have in web components where people often want to complete open up shadow roots to styling from the outer scope, and why I proposed "open-styleable" shadow roots in WICG/webcomponents#909

If that proposal were to make progress, would it be better to clone into a shadow root? Would other encapsulation features (like event re-targeting) prevent that, or would this be additional motivation for open-styleable?

@emilio
Copy link
Contributor

emilio commented Sep 20, 2024

I don't think cloning into the shadow root would be viable. It would expose the whole closed shadow tree to author script, and UAs wouldn't be able to rely on its shape for example.

@justinfagnani
Copy link

Does <selectedoption> even have a user-agent shadow root?

@esprehn
Copy link

esprehn commented Sep 20, 2024

@smaug----

Chrome/WebKit at least have a concept of "finished parsing children":

https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/forms/html_select_element.cc;l=1328;drc=48ee6c4ee320c1bcc4f7d01d5c293e6d41ecf648

In Chrome's implementation of <selectedoption> it seems to discard and clone the contents every time the MutationObserver on the children of the <option> delivers.

So during parsing if it stops halfway through parsing the <option> I think it would clone the half done option and show that, and then clone it again on the next set of appended nodes.

I think destroying the whole subtree each time there's a mutation will be surprising for developers, that means every time you append a child to an <option> other things in there like a CSS animation, animated <img> or <video> will jump. <canvas> resets, etc.

Maybe authors will just get used to this quirky behavior though.

@emilio
Copy link
Contributor

emilio commented Sep 20, 2024

Does <selectedoption> even have a user-agent shadow root?

Sorry, I misread and thought you meant cloning it somewhere inside the <select>s shadow tree. But if you clone it inside <selectedoption>s shadow tree you kinda have the same issue don't you? If you make that tree open, the author can still see inconsistencies. If you make it closed, you unexpectedly give the author access to it and you can still see the inconsistent states (though yeah, it's a lot harder to depend on)

@annevk
Copy link
Member

annevk commented Sep 21, 2024

FWIW, we did consider cloning into a shadow tree, duplicating the layout tree representation, and various other alternatives as everyone agrees this is mucky and somewhat unprecedented (although we do have tree mutation based upon user interaction already, albeit limited to attributes at the moment). These other solutions all have their own issues or seemingly insurmountable complexities. I suspect we're stuck with cloning, but we should think a bit about what it means for various elements as we do expect web developers to use img for instance (and therefore likely also media elements and custom elements) and that should not end up sucking.

@jakearchibald
Copy link
Contributor

@justinfagnani I'm interested in how you feel about this given that lit recommends against attribute reflection, which will result in lit components having important state that won't be cloned. Although I feel that lit would be better off with a more HTML-like model where the attribute is the source of truth, and reflection should be the default (although HTML is kinda inconsistent in when it does and doesn't reflect, particularly recently & here when it comes to user interaction).

@annevk what's your worry with img? I think the list of available images will work around fetching issues related to cloning.

@justinfagnani
Copy link

@jakearchibald yeah, this is a real concern. Not just for Lit, or even because we don't recommend that attribute reflection is used sparingly. It's just that not all state can even be represented as attributes in the first place.

Consider a render-prop like pattern - the render prop functions can't be reliably serialized to attributes. Cloning an element that had render props functions passed to it without restoring those functions would break the clone.

Even more commonly, component often have arbitrarily complex state passed to them via properties. Sometimes this can be serialized to JSON, but even that's usually wasteful and not done. Often serialization isn't semantically correct because the passed in state is shared in some way, or based on classes, or contains non-serializable objects.

@jakearchibald
Copy link
Contributor

Am I right in understanding that:

const div = document.createElement('div');
option.append(div);
div.setAttribute('id', 'whatever');
div.classList.add('foo');
div.textContent = 'hello';

Assuming option is selected, the above will result in the entire contents of option being cloned and appended to selectedoption four times?

@annevk
Copy link
Member

annevk commented Oct 2, 2024

@jakearchibald my concern with <img> is mainly that you might get additional events you did not anticipate. Media elements worsen that.

Assuming option is selected

Only if it's already selected and that code runs afterwards.

@jakearchibald
Copy link
Contributor

jakearchibald commented Oct 2, 2024

Hmm, that means if any of the option content is animated by modifying el.style (which is the method most animation libraries use), that will result in the entire content of the option being cloned per frame of the animation, since it's updating the style attribute.

@jakearchibald
Copy link
Contributor

jakearchibald commented Oct 2, 2024

I guess folks don't want the complexity of 'mapping' the DOM changes individually, so changing a single attribute within the <option>, results in a single attribute change within the <selectedoption>?

@esprehn
Copy link

esprehn commented Oct 3, 2024

The Blink implementation uses a MutationObserver for each option (which is kind of silly, it should use one per select) which means the changes are batched and doing the steps from #10520 (comment) should only clone once. I don't think this should clone synchronously.

In theory it could map from the MutationRecords into modifications on the other tree? Spec'ing that is pretty close to adding DOM patch which folks have requested for a long time. Maybe that's too complex as a blocker for this spec though.

@jakearchibald
Copy link
Contributor

Yeah, I think there are two choices here:

  • Batch the cloning by microtask (or even rAF)
  • Or do it synchronously, but mapping the modifications (it doesn't need to be mutation records)

I prefer that latter. It reduces the amount of cloning, and also feels like it makes more sense. Eg, if, as a developer, I understand that the content of the option is 'mirrored' into the selectedoption, it seems really weird that modifying some inner element within the option blows away the entire contents of the selectedoption. I also agree with @esprehn that this mirroring feature would make for a nice primitive.

I get that it would be more prose to write, but it isn't crazy in terms of implementation. All elements in the selected option get an associated clone that's appended to the selectedoption. That reference lives as long as the option is selected. That means, even if an element is moved in the option (which is a remove + insert), the clone in the selectedoption is also 'moved' (rather than re-cloned).

I've written code like this to mirror a virtual DOM in a worker into a real DOM. It's not too bad.

@justinfagnani
Copy link

justinfagnani commented Oct 4, 2024

Mirroring would have to take into account what happens if the mirror is modified.

  • What if a mirrored node is removed? Is it restored on the next modification to the option?
  • What if an attribute is added to a mirrored element? Is it removed?
  • What if a mirrored element is moved into the selected option? It is disconnected from it's source, then mirrored to selectionoption?

This is another reason I think cloning (or mirroring) into a shadow root on <selectedoption> would be good to consider - to prevent or discourage modification of the clone.

@jakearchibald
Copy link
Contributor

  • What if a mirrored node is removed? Is it restored on the next modification to the option?

No, unless the option node is removed and re-added to the option

  • What if an attribute is added to a mirrored element? Is it removed?

No.

  • What if a mirrored element is moved into the selected option? It is disconnected from it's source, then mirrored to selectionoption?

I hadn't thought of this edge case. I think it can stay connected to its source, and become linked to a clone of its own.

The reflection is one way, and only reflects changes from the item in the option to it's clone.

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

No branches or pull requests

10 participants