20

I'm trying to clone a shadow root, so that I may swap instances of <content></content> with their corresponding distributed nodes.

My approach:

var shadowHost = document.createElement('div');
var shadowRoot = shadowHost.createShadowRoot();

var clonedShadowRoot = shadowRoot.cloneNode(true);

does not work, as "ShadowRoot nodes are not clonable."

The motivation for this is that I wish to retrieve the composed shadow tree, so that I may use the rendered HTML markup.

This may not work due to the nature of the Shadow DOM, the reference to the distributed nodes is likely to be broken by the cloning process.

Composing the shadow tree is likely to be a native feature, but having searched through the w3c spec, I was unable to find such a method.

Is there such a native method? Or, failing that, would manual traversal (replicating the tree in the process), work?

benvc
  • 14,448
  • 4
  • 33
  • 54
Vix
  • 1,046
  • 10
  • 16
  • The lack of support in standard threw me off. It was solved somehow, I believe by traversing and replicating the structure, not by a simple clone. Were you to share a gist I would give it a look. Otherwise perhaps try to contact the ecma spec designers? – Vix Mar 30 '19 at 05:33
  • 1
    I don't understand the why you want to do this exactly. Is there any example that you can provide? Is using `shadowRoot.innerHTML` an option? – Dakota Jang Apr 05 '19 at 06:10
  • I suppose you could take the innerHTML and assign it into an element, at that point traverse it as normal. I have not had need to revisit this yet – Vix Apr 11 '19 at 13:28

2 Answers2

5

If you are trying to deep clone a node that may contain one or more nested shadow trees, then you will need to walk the DOM tree from that node and check for shadow roots along the way. See edit history if interested in the previous answer that suggested a flawed approach to shallow cloning.

const deepClone = (host) => {
  const cloneNode = (node, parent) => {
    const walkTree = (nextn, nextp) => {
      while (nextn) {
        cloneNode(nextn, nextp);
        nextn = nextn.nextSibling;
      }
    };
    
    const clone = node.cloneNode();
    parent.appendChild(clone);
    if (node.shadowRoot) {
      walkTree(node.shadowRoot.firstChild, clone.attachShadow({ mode: 'open' }));
    }
  
    walkTree(node.firstChild, clone);
  };
  
  const fragment = document.createDocumentFragment();
  cloneNode(host, fragment);
  return fragment;
};

// Example use of deepClone...

// create shadow host with nested shadow roots for demo
const shadowHost = () => {
  const host = document.createElement('div');
  const nestedhost = document.createElement('p');
  nestedhost.attachShadow({mode: 'open'}).appendChild(document.createElement('span'));
  host.attachShadow({mode: 'open'}).appendChild(nestedhost);
  return host;
};

// return fragment containing deep cloned node
const fragment = deepClone(shadowHost());
// deep cloned node
console.log(fragment.firstChild); 
// shadow tree node
console.log(fragment.firstChild.shadowRoot.firstChild);
// nested shadow tree node
console.log(fragment.firstChild.shadowRoot.firstChild.shadowRoot.firstChild);
benvc
  • 14,448
  • 4
  • 33
  • 54
  • It's worth noting that cloning the innerHTML will require _everything_ to be re-parsed, where Intervalia's answer clones the node itself, not requiring re-parsing. – jhpratt Apr 06 '19 at 06:42
  • 1
    @jhpratt - no question about it, but since the shadow tree node cloning approach requires removing, cloning, and then appending again it is not clear to me how much more efficient it would really be. – benvc Apr 10 '19 at 14:34
  • This is nice, but this is only working with first level shadow dom, what if we have nested shadow dom elements? we need to clone the whole component with all his nested non/shadow dom elements EDIT: or better, what if the component has SLOTS how can we clone also slot contents – Michael Burger Apr 11 '19 at 10:16
  • @MichaelBurger - this will "clone" nested elements, but it will not clone any nested shadow roots / trees if that is what you are asking. I am pretty sure this will clone `` elements as well (as long as they are not nested under a descendant shadow root). Give it a shot and if it doesn't work, post another question. – benvc Apr 11 '19 at 14:08
  • @benvc Yes I intend also with nested shadow roots, is there a way deep clone custom elements with shadow dom and append it to document.body without having to rerender them? – Michael Burger Apr 11 '19 at 14:57
  • I agree with @MichaelBurger - this doesn't seem to clone more than one level down. It seems one shouldn't have to detach a shadow DOM to copy it. Is this a bug or a feature of browsers or the standard? – Ginzorf Apr 22 '20 at 01:37
  • 1
    @Ginzorf - not sure exactly what the spec says about cloning shadow trees. Since shadow trees are intended to create a hidden, separate DOM, something like [Document.importNode](https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode) would seem to be the right approach but it appears that most browsers have intentionally prevented this. Many use cases for cloning shadow trees are better handled with [templates and slots](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots). – benvc Apr 22 '20 at 17:14
  • @benvc - thanks for the reply! I'm still looking into this. It's very strange because cloneNode(true) does do a "deep" copy of all children up to the first web component, but doesn't copy any other web components nested in the first. That seems like a bug, imo. Although I understand the desire to provide design/DOM-level encapsulation, the very fact that you CAN create a shadow root with {mode: 'open'} to allow internal interrogation seems to show the standard plays a little fast and loose with "encapsulation". My use case is popping up a copy of part of the current window for printing. – Ginzorf Apr 22 '20 at 18:19
  • @Ginzorf - since there seem to be a few use cases for deep cloning nodes with nested shadow trees, the answer has been edited with one approach to solving that problem. – benvc Apr 22 '20 at 23:06
1

OK. This is a little crazy, but here is a routine I wrote that will clone the children of a shadowRoot. This conforms to the V1 spec.

function cloneShadow(shadow) {
  const frag = document.createDocumentFragment();

  var nodes = [...shadow.childNodes];
  nodes.forEach(
    node => {
      node.remove();
      frag.appendChild(node.cloneNode(true));
      shadow.appendChild(node);
    }
  );

  return frag;
}

const s1 = document.querySelector('.shadow1');
const s2 = document.querySelector('.shadow2');

s1.attachShadow({mode:'open'}).innerHTML = `<h1>Header</h1>
<p>Content in a paragraph</p><slot></slot>`;

setTimeout(() => {
  s2.attachShadow({mode:'open'}).appendChild(cloneShadow(s1.shadowRoot));}, 1000);
.shadow1 {
  background-color: #F88;
}

.shadow2 {
  background-color: #88F;
}
<div class="shadow1">
  <p>SHADOW 1</p>
</div>
<div class="shadow2">
  <p>SHADOW 2</p>
</div>

I had to remove each node from the shadowDOM and then clone it and then append it back into the shadowRoot.

I even added a setTimeout so you can see that it works at any time.

It even works with slots.

Intervalia
  • 10,248
  • 2
  • 30
  • 60
  • Curious, why remove the nodes and then re-append them? Is it not possible to clone a childNode _inside_ a shadowRoot either? – jhpratt Apr 06 '19 at 06:44
  • 2
    It failed to clone them while still in a shadowRoot. So removing them and then re-appending them seemed to be the only way to get it to work. – Intervalia Apr 06 '19 at 19:12
  • This is only working for one level shadow dom and not wor slots, is there a way to implement also this? – Michael Burger Apr 11 '19 at 10:19
  • You will get a clone of each element. That would indicate that every sub-element should be cloned as well. If you clone an element with shadowDOM then it should be a new copy of the element which means it will have a new shadowDOM and new children. Are you seeing something different? – Intervalia Apr 11 '19 at 14:14