I'm exploring imports, templates, shadow DOM and custom elements in Chrome Canary (33.0.1712.3). In a grid layout I have a particular content element (region of the display) that will display different web components or cloned light DOM fragments imported from files.

However, I'm unable to redisplay ordinary HTML DOM once a shadow DOM has been added because I don't know how to remove the shadow root. Once created, the shadow root remains and interferes with the rendering of ordinary DOM. (I've looked at various W3C specs such as intro to web components, shadow DOM, templates, Bidelman's articles on HTML5 Rocks, etc.) I've isolated the problem in a simple example below:

Click "show plain old div"; click "show shadowed template"; click "show plain old div". Inspect in devtools after each click. After the third click, there is no output below the buttons and in devtools I am seeing:

<div id="content">
  #document-fragment
  <div id="plaindiv">Plain old div</div>
</div>

What do I need to add to removeShadow() to remove the shadow root and fully reset the content element to its initial state?

removing_shadows.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>

  <template id="shadowedTemplateComponent">
    <style>
      div { background: lightgray; }
      #t { color: red; }
    </style>

    <div id="t">template</div>

    <script>console.log("Activated the shadowed template component.");</script>
  </template>

  <template id="plainDiv">
    <div id="plaindiv">Plain old div</div>
  </template>
</head>

<body>
<div>
  <input type="button" value="show plain old div" onclick="showPlainOldDiv()"/>
  <input type="button" value="show shadowed template" onclick="showShadowTemplate()"/>
  <div id="content"></div>
</div>

<script>
  function removeChildren(elt) {
    console.log('removing children: %s', elt);
    while (elt.firstChild) {
      elt.removeChild(elt.firstChild);
    }
  }
  function removeShadow(elt) {
    if (elt.shadowRoot) {
      console.log('removing shadow: %s', elt);
      removeChildren(elt.shadowRoot); // Leaves the shadow root property.
      // elt.shadowRoot = null; doesn't work
      // delete elt.shadowRoot; doesn't work
      // What goes here to delete the shadow root (#document-fragment in devtools)?
    }
  }

  function showPlainOldDiv() {
    console.log('adding a plain old div');
    var host = document.querySelector('#content');
    removeChildren(host);
    removeShadow(host);
    var template = document.querySelector('#plainDiv');
    host.appendChild(template.content.cloneNode(true));
  }

  function showShadowTemplate() {
    console.log('adding shadowed template component');
    var host = document.querySelector('#content');
    removeChildren(host);
    removeShadow(host);
    var template = document.querySelector('#shadowedTemplateComponent');
    var root = host.shadowRoot || host.webkitCreateShadowRoot();
    root.appendChild(template.content.cloneNode(true));
  }
</script>
</body>
</html>
有帮助吗?

解决方案 2

You can't remove a shadow root once you add it. However, you can replace it with a newer one.

As mentioned here, http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-301/, the newest shadow root "wins" and becomes the rendered root.

You can replace your shadow root with a new shadow root that only contains the <content> pseudo-element to insert everything from the light DOM back into the shadow DOM. At that point, as far as I know it will be functionally equivalent to having no shadow DOM at all.

其他提示

The spec of Shadow DOM moved from v0 to v1.

One of the changes is that in v1 there is no way to create shadow root on itself and the host element may contain only one shadow root.

So it seems like the answer of replacing the shadow root with a new blank shadow root is not valid anymore.

Solution paths:

  • if the host element self (div in your example) has no special value beside holding that Shadow DOM, one can just replace the host element as a whole
  • if one still likes to preserve the host, clearing the Shadow DOM with something like e.shadowRoot.innerHTML = '' might be sufficient

rmcclellan is correct that you cannot truely "remove" a ShadowRoot v2. But, you can fake it.

The OuterHTML PARTIAL Solution

elementWithShadowDOMv2.outerHTML = elementWithShadowDOMv2.outerHTML;

HOWEVER, there is a major caveat: although there is no visual change, elementWithShadowDOMv2 still refers to the destroyed element with the ShadowDOMv2 as if elementWithShadowDOMv2.parentNode.removeChild( elementWithShadowDOMv2 ) were called. This also "removes" event listeners on the element. Observe the demo below.

var addShadowHere = document.getElementById("add-shadow-here");

addShadowHere.addEventListener("mouseenter", function() {
  addShadowHere.style.border = '2em solid blue';
});
addShadowHere.addEventListener("mouseleave", function() {
  addShadowHere.style.border = '';
});

var shadow = addShadowHere.attachShadow({mode:"open"});
var button = shadow.appendChild(document.createElement("button"));

button.textContent = "Click Here to Destroy The ShadowDOMv2";

button.addEventListener("click", function() {
  addShadowHere.outerHTML = addShadowHere.outerHTML;
  
  update();
});

update();

function update() {
  // This just displays the current parent of the addShadowHere element
  document.getElementById("parent-value").value = "" + (
    addShadowHere.parentNode &&
      addShadowHere.parentNode.cloneNode(false).outerHTML
  );
}
<div id="add-shadow-here">Text Hidden By Shadow DOM</div>
addShadowHere.parentNode => <input readonly="" id="parent-value" />

Notice how the blue border stops working after you remove the ShadowDOM. That is because the event listeners are no longer registered on the new element: the event listeners remain registered on the old element that has now been removed from the DOM.

Thus, you must refresh any references to the element and reattach any event listeners. Here is an example of how you could reobtain a reference to the new element.

function removeShadowWithCaveat(elementWithShadow) {
  if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);

  var parent = elementWithShadow.parentNode;
  var prior = elementWithShadow.previousSibling;

  elementWithShadow.outerHTML = elementWithShadow.outerHTML;

  return prior.nextSibling || parent.firstChild;
}

If you need access to the elements which are naturally hidden by the existing shadow root and which will become exposed after the expulsion of the shadow root, then here is an alternative method that will perfectly preserve these nodes.

function removeShadowWithCaveat(elementWithShadow) {
  if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);

  var ref = elementWithShadow.cloneNode(true);
  while (elementWithShadow.lastChild) ref.appendChild( elementWithShadow.lastChild );
  elementWithShadow.parentNode.replaceChild(elementWithShadow, elementWithShadow);

  return ref;
}

Working Solution

var createShadowProp = (
  "createShadowRoot" in Element.prototype ? "createShadowRoot" : "webkitCreateShadowRoot"
);

function removeChildren(elt) {
  console.log('removing children: %s', elt);
  while (elt.firstChild) {
    elt.removeChild(elt.firstChild);
  }
}
function removeShadowWithCaveat(elementWithShadow) {
  if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);
  
  var ref = elementWithShadow.cloneNode(true);
  while (elementWithShadow.lastChild) ref.appendChild( elementWithShadow.lastChild );
  elementWithShadow.parentNode.replaceChild(elementWithShadow, elementWithShadow);
  
  return ref;
}

function showPlainOldDiv() {
  console.log('adding a plain old div');
  var host = document.querySelector('#content');
  removeChildren(host);
  
  // Remove the shadow
  host = removeShadowWithCaveat(host);
  
  var template = document.querySelector('#plainDiv');
  host.appendChild(template.content.cloneNode(true));
}

function showShadowTemplate() {
  console.log('adding shadowed template component');
  var host = document.querySelector('#content');
  removeChildren(host);

  // Remove the shadow
  host = removeShadowWithCaveat(host);
  
  var template = document.querySelector('#shadowedTemplateComponent');
  var root = host.shadowRoot || host[createShadowProp]({
    "open": true
  });
  root.appendChild(template.content.cloneNode(true));
}
<div>
  <input type="button" value="show plain old div" onclick="showPlainOldDiv()"/>
  <input type="button" value="show shadowed template" onclick="showShadowTemplate()"/>
  <div id="content"></div>
</div>

<template id="shadowedTemplateComponent" style="display:none">
  <style>
    div { background: lightgray; }
    #t { color: red; }
  </style>

  <div id="t">template</div>

  <script>console.log("Activated the shadowed template component.");</script>
</template>

<template id="plainDiv" style="display:none">
  <div id="plaindiv">Plain old div</div>
</template>

Also note the misuse of vendor prefixes (a problem that far too many developers have issues with). You are correct that, at the time that this question was asked, there was only the prefixed version of createShadowRoot (which was webkitCreateShadowRoot). Nevertheless, you must ALWAYS check to see if the unprefixed createShadowRoot version is available in case if browsers standardize the API in the future (which is now the case). It might be nice to have your code working today, but it's awesome to have your code working several years from now.

In Chrome:

  1. Press F12, DevTool will open
  2. Click gear icon in DevTool
  3. Uncheck "show user agent shadow DOM" checkbox

Enjoy !

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top