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

Allow elements to be slotted into multiple locations in the DOM tree #131

Open
sirisian opened this issue Dec 7, 2023 · 0 comments
Open

Comments

@sirisian
Copy link

sirisian commented Dec 7, 2023

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:

<div id="a">
	<option>abc</option>
</div>
<select></select>
select {
	content: #a > *;
}

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?

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.

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

No branches or pull requests

1 participant