This proposal introduces a mechanism to deduplicate DOM subtrees by creating references to elements. This means there's an original element that is then slotted as a child to one or multiple other elements.
In Web Components, entities can be reflected into a slot in one location. Popover can also move items into a top layer. This proposal is akin to such a mechanism, but this removes the restriction where the element can only be in one place at a time. The closest concept in the current standard is the <datalist> which allows an <input> to reference DOM <option> lists. That implementation however is very limited and ad-hoc for one specific use case.
Use Cases
A common scenario is having multiple <select> elements with identical <option> lists. A common example is font selectors with hundreds of options. Picker controls in general can have a lot of similar duplication from being identical or just sharing history lists. Currently users will duplicate nodes or move them around as a picker is opened using JS. (<select multiple> complicates moving around options though as a single instance of that means one needs to clone the options).
Note: Font is used as an example because it has styling (where each option is rendered in a font). While I say <select> I'm more referring to drop down controls in general which can be implemented with custom elements and the Popover API.
Goals
Allow a DOM subtree to exist in multiple places such that changes to the original are automatically reflected to the others.
Non-goals
This does not necessarily have to be optimization focused. That said, having say 500 font options in 20 font drop downs shouldn't have the full cost of cloning the nodes. (The layout is unique, so it's understandable that some memory might be allocated).
The CSS property content would now support a selector defining the content to be duplicated as children. Any content inside would be hidden when this property is used.
Javascript API
Setting content from JS
Popover has popoverTargetElement which is equivelant to setting the popovertarget attribute. If possible it would be useful to be able to assign an HTMLElement or HTMLCollection to content programmatically overriding the styling.
document.getElementsByTagName('select')[0].contentElements = document.getElementById('a').children; // live HTMLCollection
Assigning a single element would use an array: = [element];
Accessing the Original Element vs the Slotted Element
How would one differentiate the original element from the slotted? Properties like offsetWidth and methods like getBoundingClientRect() will return different values based on whether the original element or slotted is being referenced. The path to the slotted element can be used to uniquely identify it, so that seems like a sensible direction.
Proposed Solution
Modify querySelector() to optionally return a SlottedElement<T> (which derives from the element's class T, see note below) that would be a wrapper that includes a path (readonly) and a reference (readonly) to the original element and behaves like the element. Essentially every query like querySelectorAll() could return a mix of Element and SlottedElement. Ideally since SlottedElement appears like a regular element and is derived from the element's class then instanceof would work even in code unaware of the feature.
All instances of SlottedElement with the same path would be the same from queries and inside HTMLCollection so equality works as expected. Also since SlottedElement is functionally identical to the original it can be used wherever an HTMLElement can be used.
Note: SlottedElement<T> is a generic which for this will just be calledSlottedElementT or a randomly generated class name that will be usable when JS gets generics.
Expand for Failed Approach
I'm including this thought experiment because I wanted to see how awkward or broken it would be.
Naively one could allow setting the path on a property ```slotPath``` of the element before calling any element method. By default it would be ```null``` and refer to the original. Below ```composedPath()``` returns the path and would be a useful helper function.
```
element.slotPath = slot.composedPath();
element.getBoundingClientRect();
element.slotPath = null;
```
This has a downside though in that it could be confusing with asynchronous code where another piece of code could change the ```slotPath```.
What about ```querySelector()``` and other selectors?
```js
document.getElementsByTagName('select')[0].querySelector('option');
```
That would return the original element missing any slot context. Accessing ```offsetWidth``` would then calculate the original element's ```offsetWidth```.
In theory ```querySelector()``` could set the ```slotPath``` before returning the element if it's not the original. Any command ran on the element from then on would be ran in the context of the slotted one. This behavior would be confusing especially with asynchronous code running different queries and modifying the ```slotPath```. That said, it does produce generally intuitive results if one understands that ```content``` is being used and the developer tools correctly marks the slotted items. (Slots and #top-layer items already do this with popover).
This would extend to every part of the Javascript API in the ```Node``` base class, ```HTMLElement```, etc. For example, accessing the ```parentNode``` would change depending on ```slotPath```.
**The big issue** is if you run a query like ```document.body.querySelectorAll('*')``` and an element is in two places the ```slotPath``` would be overwritten by the last slotted element or set to null if the original node is after the slots. This is so confusing and would probably break existing libraries.
Events
Events should function with no changes. Their composed path can be used to differentiate them if needed. In many cases though listeners can be registered to parent elements. If you can think of an edge case please describe it.
Background Information on Web Components and Popover
This might not be well known, but the picker is generally placed within the custom element, either in the light or shadow DOM, such that focus remains on the input when interacting with the picker in the Popover top layer. This results in incredibly elegant code such that input controls can be nested with keyboard input (like pressing escape) and such traversing intuitively. So that's why JS would be used to move a picker or the options around to the current picker so that it's within the subtree of the input.
Examples
A further example is a custom element could have a CSS rule that applies content only when the picker is open. This means the CSS is adding/removing potentially large subtrees. (Though this is what a JS approach would already be doing in order to keep the options within the input's subtree as explained before). The potential advantage here is the slotted elements won't exist until they're needed.
Another example would be using CSS to switch the option list on the fly. This could now be done with just CSS if needed. I don't need this, but it sounds interesting.
Privacy & Security Considerations
None
Let’s Discuss
I remember seeing questions relating to such a feature like this I think decades ago, so I know it's come up in the past. If anyone has links to historical discussions that are still relevant that would be useful. Also different approaches, problems, or big concepts I've forgotten to cover that would be appreciated. That elements exist in one place at a time has been fairly fundamental to DOM for a while, so changing it would be a large decision that could ripple to other proposals and how people approach problems. More examples could be useful.
Introduction
This proposal introduces a mechanism to deduplicate DOM subtrees by creating references to elements. This means there's an original element that is then slotted as a child to one or multiple other elements.
In Web Components, entities can be reflected into a slot in one location. Popover can also move items into a top layer. This proposal is akin to such a mechanism, but this removes the restriction where the element can only be in one place at a time. The closest concept in the current standard is the
<datalist>
which allows an<input>
to reference DOM<option>
lists. That implementation however is very limited and ad-hoc for one specific use case.Use Cases
A common scenario is having multiple
<select>
elements with identical<option>
lists. A common example is font selectors with hundreds of options. Picker controls in general can have a lot of similar duplication from being identical or just sharing history lists. Currently users will duplicate nodes or move them around as a picker is opened using JS. (<select multiple>
complicates moving around options though as a single instance of that means one needs to clone the options).Note: Font is used as an example because it has styling (where each option is rendered in a font). While I say
<select>
I'm more referring to drop down controls in general which can be implemented with custom elements and the Popover API.Goals
Allow a DOM subtree to exist in multiple places such that changes to the original are automatically reflected to the others.
Non-goals
This does not necessarily have to be optimization focused. That said, having say 500 font options in 20 font drop downs shouldn't have the full cost of cloning the nodes. (The layout is unique, so it's understandable that some memory might be allocated).
Proposed Solution
A CSS solution would be used to define this:
The CSS property
content
would now support a selector defining the content to be duplicated as children. Any content inside would be hidden when this property is used.Javascript API
Setting
content
from JSPopover has popoverTargetElement which is equivelant to setting the popovertarget attribute. If possible it would be useful to be able to assign an HTMLElement or HTMLCollection to content programmatically overriding the styling.
Assigning a single element would use an array:
= [element];
Accessing the Original Element vs the Slotted Element
How would one differentiate the original element from the slotted? Properties like
offsetWidth
and methods likegetBoundingClientRect()
will return different values based on whether the original element or slotted is being referenced. The path to the slotted element can be used to uniquely identify it, so that seems like a sensible direction.Proposed Solution
Modify
querySelector()
to optionally return aSlottedElement<T>
(which derives from the element's class T, see note below) that would be a wrapper that includes a path (readonly) and a reference (readonly) to the original element and behaves like the element. Essentially every query likequerySelectorAll()
could return a mix ofElement
andSlottedElement
. Ideally sinceSlottedElement
appears like a regular element and is derived from the element's class theninstanceof
would work even in code unaware of the feature.All instances of
SlottedElement
with the same path would be the same from queries and insideHTMLCollection
so equality works as expected. Also sinceSlottedElement
is functionally identical to the original it can be used wherever anHTMLElement
can be used.Note:
SlottedElement<T>
is a generic which for this will just be calledSlottedElementT
or a randomly generated class name that will be usable when JS gets generics.Expand for Failed Approach
I'm including this thought experiment because I wanted to see how awkward or broken it would be. Naively one could allow setting the path on a property ```slotPath``` of the element before calling any element method. By default it would be ```null``` and refer to the original. Below ```composedPath()``` returns the path and would be a useful helper function. ``` element.slotPath = slot.composedPath(); element.getBoundingClientRect(); element.slotPath = null; ``` This has a downside though in that it could be confusing with asynchronous code where another piece of code could change the ```slotPath```. What about ```querySelector()``` and other selectors? ```js document.getElementsByTagName('select')[0].querySelector('option'); ``` That would return the original element missing any slot context. Accessing ```offsetWidth``` would then calculate the original element's ```offsetWidth```. In theory ```querySelector()``` could set the ```slotPath``` before returning the element if it's not the original. Any command ran on the element from then on would be ran in the context of the slotted one. This behavior would be confusing especially with asynchronous code running different queries and modifying the ```slotPath```. That said, it does produce generally intuitive results if one understands that ```content``` is being used and the developer tools correctly marks the slotted items. (Slots and #top-layer items already do this with popover). This would extend to every part of the Javascript API in the ```Node``` base class, ```HTMLElement```, etc. For example, accessing the ```parentNode``` would change depending on ```slotPath```. **The big issue** is if you run a query like ```document.body.querySelectorAll('*')``` and an element is in two places the ```slotPath``` would be overwritten by the last slotted element or set to null if the original node is after the slots. This is so confusing and would probably break existing libraries.Events
Events should function with no changes. Their composed path can be used to differentiate them if needed. In many cases though listeners can be registered to parent elements. If you can think of an edge case please describe it.
Background Information on Web Components and Popover
This might not be well known, but the picker is generally placed within the custom element, either in the light or shadow DOM, such that focus remains on the input when interacting with the picker in the Popover top layer. This results in incredibly elegant code such that input controls can be nested with keyboard input (like pressing escape) and such traversing intuitively. So that's why JS would be used to move a picker or the options around to the current picker so that it's within the subtree of the input.
Examples
A further example is a custom element could have a CSS rule that applies
content
only when the picker is open. This means the CSS is adding/removing potentially large subtrees. (Though this is what a JS approach would already be doing in order to keep the options within the input's subtree as explained before). The potential advantage here is the slotted elements won't exist until they're needed.Another example would be using CSS to switch the option list on the fly. This could now be done with just CSS if needed. I don't need this, but it sounds interesting.
Privacy & Security Considerations
None
Let’s Discuss
I remember seeing questions relating to such a feature like this I think decades ago, so I know it's come up in the past. If anyone has links to historical discussions that are still relevant that would be useful. Also different approaches, problems, or big concepts I've forgotten to cover that would be appreciated. That elements exist in one place at a time has been fairly fundamental to DOM for a while, so changing it would be a large decision that could ripple to other proposals and how people approach problems. More examples could be useful.