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

<mirror> element, like <slot>, but not limited to ShadowDOM, elements from anywhere can be assigned to it #6507

Open
trusktr opened this issue Mar 18, 2021 · 6 comments

Comments

@trusktr
Copy link

trusktr commented Mar 18, 2021

Having the ability to render an element relative to other places in the DOM would alleviate overhead from situations where an element should be rendered more than once in different places of a web app (f.e. implementing CSS-based VR, and would provide the machinery for new patterns like what virtual component systems currently do with "portals".

The <mirror> element would be similar to ShadowDOM <slot> elements, except that

  • nodes assigned to a <mirror> can come from anywhere (not restricted to a shadow host element's children like with <slot>s), and no hierarchy requirements
  • the <mirror> element does not have to be anywhere specifically (f.e. not required to be in a ShadowRoot)
  • the nodes that are assigned to a <mirror> are still rendered in their original location plus in the new locations where <mirror> elements are located. This is in contrast to ShadowRoot <slot> elements where distributed nodes no longer render in their original light-tree position.

There may be both an imperative way to assign nodes to a <mirror>, and a declarative way.

A declarative example:

<style>
  h1 span { color: deeppink }
  h2 span { color: yellow }
  h3 span { color: cyan }
</style>
<h1>My name is <mirror name="nametag1"></mirror></h1>
<h2>I am called <mirror name="nametag2"></mirror></h2>
<h3>People call me <span mirror="nametag1 nametag2">Joe</span><h3>

would render as if we had written

<style>
  h1 span { color: deeppink }
  h2 span { color: yellow }
  h3 span { color: cyan }
</style>
<h1>My name is <span>Joe</span></h1>
<h2>I am called <span>Joe</span></h2>
<h3>People call me <span>Joe</span><h3>

Affecting the style of the mirrored <span> element causes the effect to be mirrored to all locations. For example,

<style>
  h1 span { color: deeppink }
  h2 span { color: yellow }
  h3 span { color: cyan }
  .underline { text-decoration: underline }
</style>
<h1>My name is <mirror name="nametag1"></mirror></h1>
<h2>I am called <mirror name="nametag2"></mirror></h2>
<h3>People call me <span mirror="nametag1 nametag2">Joe</span><h3>
<script>
  document.querySelector('span').classList.add('underline')
</script>

would render as if we had written

<style>
  h1 span { color: deeppink }
  h2 span { color: yellow }
  h3 span { color: cyan }
  .underline { text-decoration: underline }
</style>
<h1>My name is <span>Joe</span></h1>
<h2>I am called <span>Joe</span></h2>
<h3>People call me <span>Joe</span><h3>
<script>
  document.querySelectorAll('span').forEach(s => s.classList.add('underline'))
</script>

An imperative example:

<style>
  h1 span { color: deeppink }
  h2 span { color: yellow }
  h3 span { color: cyan }
  .underline { text-decoration: underline }
</style>
<h1>My name is <mirror id="mirror1"></mirror></h1>
<h2>I am called <mirror id="mirror2"></mirror></h2>
<h3>People call me <span>Joe</span><h3>
<script>
  const span = document.querySelector('span')
  span.classList.add('underline')
  document.getElementById('mirror1').assign(span)
  document.getElementById('mirror2').assign(span)
</script>

would render as if we had written

<style>
  h1 span { color: deeppink }
  h2 span { color: yellow }
  h3 span { color: cyan }
  .underline { text-decoration: underline }
</style>
<h1>My name is <span>Joe</span></h1>
<h2>I am called <span>Joe</span></h2>
<h3>People call me <span>Joe</span><h3>
<script>
  document.querySelectorAll('span').forEach(s => s.classList.add('underline'))
</script>

The assign() method name is borrowed from #3534.

Use Case 1: CSS-based VR (f.e. phones inside binoculars)

The content to be displayed in 3D only needs to have a single DOM for manipulation (a single source of truth) so as not to worry about having to copy all modifications from one tree (f.e. left eye) to the other (f.e. right eye).

<div class="left-eye" style="transform: ...left eye offset transformation...">
  <div style="transform: translateZ(-400px) rotateY(20deg)" mirror="rightEye">This is content in 3D space.</div>
</div>
<div class="left-eye" style="transform: ...right eye offset transformation...">
  <mirror name="rightEye" />
</div>

Then the user only ever has to modify the left eye content, and it is mirrored to the right eye with the proper right-eye transform.

Use Case 2: Render useful things in multiple places

This is an e-commerce website:

<style>
  header .shoppingCart {
    /*header-specific style for the cart*/
  }
  .sidebar .shoppingCart {
    /* Perhaps the rendering of the cart in the sidebar has a tweak or two. */
  }
  footer .shoppingCart {
    /* Perhaps the rendering of the cart in the footer has a tweak or two. */
  }
  /* The above CSS applies in a manner similar to nodes distributed into a <slot> where
      the ShadowRoot styles distributed children relative to their shadow tree position.
      In this case the distributed node is styled relative to its mirror locations (but like normal
      in its original location). */
</style>
<header class="nav">
  <div class="shoppingCart" mirror="cart">
    <div>3 items, total: $50</div>
    <button>Checkout</button>
  </div>
</header>
<aside class="sidebar">
  ...
  <mirror name="cart">
  ...
</aside>
<footer>
  ...
  <mirror name="cart">
  ...
</footer>

Bike sheddable: perhaps mirrors can have the same name, and something assigned to that mirror name renders at all locations.

Use Case 3: "portals"

On its own, it is similar to the portal concepts in other virtual component systems like React, Vue, etc.

Here's the Svelte Portal example. The part with <div use:createPortal={'foo'}> is the <mirror>, and <div use:portal={'foo'}> is an element in another component being mounted to that position in the other component.

The difference with those concepts and this new <mirror> concept is that in this <mirror> concept the element being mirrored can be mirrored to multiple locations as well as render in its original location.

A library author could implement their own "portal" concept on top of the <mirror> system in order to enforce certain restrictions like an element mirroring only to one outside location (f.e. using a unique naming, or some form of mirror reference passing), or enforcing that the portaled item does not render in its original location with some CSS styling.

<style>
  .originalLocation > * {
    display: none;
  }
</style>

<!-- ...component's tree... -->
<div class=".originalLocation">
  <div portal="foo"></div>
</div>

<!-- ...other component's portal... -->
<mirror name="uniqueName" />

Most common use case

Probably the most common use case will be duplicating content in various places, like with the shopping cart example. The following are sometimes seen in multiple locations of web apps:

  • shopping cart summaries
  • profile cards or icons
  • blog post lists
  • blog tag lists (f.e. in a sidebar and footer)
  • nav items that appear in a header bar on desktop or a pull-out sidebar on mobile. Although both may not be visible at the same time, only one instance is included in the page and it is rendered either in the original location or in the mobile sidebar location depending on device. It may also appear in the footer.
  • etc

Additional API ideas

  • There may be a mirrorchange event that is similar to the slotchange even that we know.
  • Mirrors may be mirrored just like slots can be slotted
  • mirror.assignedNodes() works very similar to slot.assignedNodes()
  • Something like <mirror name="..." scoped> could perhaps make it scoped to a ShadowRoot, just like a <slot> is, and would allow any child node or grandchild node in a shadow host to be distributed to the mirror (provides the concept of distributing indirect children of a host, mentioned in Imperative shadow DOM distribution API #3534 IIRC), effectively making it behave just like a <slot> but with added benefit of indirect child distribution. Satisfies @joelrich's ask in [Shadow] Slotting indirect children WICG/webcomponents#574

Other benefits:

By having only one source of truth for things that should be rendered in multiple places,

  • we can eliminate surface area for errors in keeping the multiple instances in sync.
  • f.e. a checkbox state would be within the original element, and all other locations would be only visual re-renderings of it, so only one place in source code needs to track that checkbox state.
  • Eliminates the need to fetch data for duplicate items, or eliminates the need to share data across duplicate items: only the source of truth item has data-fetching logic associated with it, and the mirrors are just visual re-renderings of it.
  • lower resource use due to not having to instantiate duplicate DOM trees and duplicate pieces of logic for those duplicates.
@rniwa
Copy link

rniwa commented Mar 18, 2021

This is quite a bit like svg use element. Ironically, WebKit & Blink implement svg use element using shadow roots and replicating DOM tree in each instance of use element. FWIW, this seems like something you can easily implement in the user land. You just need to "mirror" DOM tree for each instance using MutationObserver. That's effectively what WebKit/Blink does except it gets updated whenever style gets updated so the timing is slightly different. Trident/Edge used to implement svg use element at more of rendering layer so they avoided replicating the DOM tree but it leads to many architectural challenges in other engines so that's not really practical.

@trusktr
Copy link
Author

trusktr commented Mar 18, 2021

this seems like something you can easily implement in the user land. You just need to "mirror" DOM tree for each instance using MutationObserver.

It is possible, yes, just like we can polyfill Custom Elements, which comes with its can of worms.

Performance aside (either the browser replicates internal trees or it chooses a fast render-only path), the <mirror> idea would improve developer experience and development simplicity in a standard and powerful way.

If a user writes their own duplicate-DOM-and-mirror-changes-with-MutationObserver system, it can open up the surface area for bugs. It can interfere with the end users DOM representation. For example, imagine the user installs a 3rd-party jQuery plugin, and when the user instantiates the plugin, it accidentally also applies itself onto the duplicated DOM trees and causes unintended side-effects. The user is expecting to manipulate only one DOM tree as the source of truth. But due to the plugin also manipulating the duplicate DOM trees, the MutationObserver mechanism could break in some unexpected way, or the jQuery APIs may return multiple states when there should only be one and it could confuse the user code. Of course there would always be some way to fix such an issue, but it would complicate the application code base.

@rniwa
Copy link

rniwa commented Mar 18, 2021

Performance aside (either the browser replicates internal trees or it chooses a fast render-only path), the <mirror> idea would improve developer experience and development simplicity in a standard and powerful way.

I mean... it's true that all new APIs will improve developer ergonomics for developers who need them but adding a very complex feature like this requires a really good reason to do so.

If a user writes their own duplicate-DOM-and-mirror-changes-with-MutationObserver system, it can open up the surface area for bugs. It can interfere with the end users DOM representation. For example, imagine the user installs a 3rd-party jQuery plugin, and when the user instantiates the plugin, it accidentally also applies itself onto the duplicated DOM trees and causes unintended side-effects. The user is expecting to manipulate only one DOM tree as the source of truth. But due to the plugin also manipulating the duplicate DOM trees, the MutationObserver mechanism could break in some unexpected way, or the jQuery APIs may return multiple states when there should only be one and it could confuse the user code. Of course there would always be some way to fix such an issue, but it would complicate the application code base.

How does jQuery plugin start working on / gain access to a duplicated DOM tree? Just put it inside a closed shadow tree and nobody else will find the duplicated instances.

@trusktr
Copy link
Author

trusktr commented Mar 24, 2021

Just put it inside a closed shadow tree and nobody else will find the duplicated instances.

Yeah, there are workarounds of course. Also using unique selectors would help (f.e. .left-eye > .foo).

Considering performance, isn't crossing JS-native boundaries more expensive than keeping things on the native side (f.e. keeping the duplicates as native trees)?

The complexity is a little more simple than ShadowRoot and <slot>. I imagine various parts of the internal code would be re-used.

@jonathantneal
Copy link
Contributor

jonathantneal commented Oct 4, 2022

This kind of functionality has seemed necessary to me when building custom previews, selects, trees, tables, and multitracks. I suspect it’s applicable to experiences that clone DOM trees for the purpose of mirroring content, or custom elements that require nesting deeper than a single parent and child.

Cloning a DOM tree and mirroring it with a MutationObserver would be insufficient when the source elements contain events or styles. For the web components I’ve been working on, our users plan to slot in their own content with its own functionality.

Without something like this mirror proposal, my currently alternative is to create some kind of proxy-like prototype for everything from EventTarget to HTMLElement for functionality cloning, and/or observers chained to a cached CSSStyleDeclaration from getComputedStyle for style cloning. Normally, I wouldn’t entertain this, but I already have to do something very similar to emulate constructible stylesheets. Still, the idea of messing around with native classes beyond polyfilling gives me MooTools vibes, so I’m hoping some of the linked examples will help illuminate a need for this.

@yinonov
Copy link

yinonov commented Oct 6, 2022

can cross pages be considered in this scope? would it be feasible to mirror from 1 page to another same as GH do with permalinks?

We are committed to providing a friendly, safe and welcoming environment for all. Please read and respect the [WHATWG Code of Conduct](https://whatwg.org/code-of-conduct).

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

4 participants