Is it possible to position nested shadow DOMs on top of each other?
With normal DOM, nested divs just stack by default:
.d1 {
width: 150px;
height: 150px;
background-color: lightseagreen;
}
.d2 {
width: 100px;
height: 100px;
background-color: darkslateblue;
}
.d3 {
width: 50px;
height: 50px;
background-color: lightgray;
}
<div class="container">
<div class="d1">
<div class="d2">
<div class="d3">
</div>
</div>
</div>
</div>
But with shadow DOM, how can this be done?
id = 0;
depth = 3;
customElements.define('box-component', class extends HTMLElement {
constructor() {
super();
if (id > depth)
return;
this.id = id++;
var template = document.getElementById("box-template");
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = template.innerHTML;
// The only goal of all of the following boilerplate is to set 'exportparts' attribute,
// such that each parent exports its *descendants* (not just *children*) parts.
// This is done by accessing the shadow dom of the child from its parent.
// (note: the outermost component doesn't need to export its parts)
if (id == 0)
return;
const parts = [...this.shadowRoot.children]
.filter(elem => elem.getAttribute('part'))
let thisExportParts = null;
let childExportParts = null;
thisExportParts = parts.map(elem => [
[elem.getAttribute('part'), `${elem.getAttribute('part')}${this.id}`].join(':')
].join(','));
if (this.shadowRoot && this.shadowRoot.children) {
const childShadowRoot =
[...this.shadowRoot.children].filter(elem => elem.shadowRoot)[0];
if (childShadowRoot) {
const fullChildExportParts = childShadowRoot.getAttribute('exportparts');
if (fullChildExportParts) {
childExportParts = fullChildExportParts.split(',').map(part => {
return part.includes(':') ? part.split(':')[1] : part;
}).join(',');
}
}
}
this.setAttribute('exportparts', [thisExportParts, childExportParts].filter(_ => _).join(','));
}
});
box-component::part(box1) {
width: 150px;
height: 150px;
background-color: lightseagreen;
}
box-component::part(box2) {
width: 100px;
height: 100px;
background-color: darkslateblue;
}
box-component::part(box3) {
width: 50px;
height: 50px;
background-color: lightgray;
}
<template id="box-template">
<style>
</style>
<div part="box"></div>
<box-component></box-component>
</template>
<box-component class="container"></box-component>
It's possible. In the shadow DOM, the following should be added:
:host {
position: absolute;
top: 0;
}
This :host
rule styles all instances of the custom component element (the shadow host) in the document to be absolutely positioned, while also making the positioning relative to each positioned parent (that's why top: 0
is needed).
In fact, this is similar to changing the normal DOM code in the question to use positioning (note that without positioning, just setting top: 0
won't work (further read)):
.container {
position: relative;
}
.container div {
position: absolute;
top: 0;
}
.d1 {
width: 150px;
height: 150px;
background-color: lightseagreen;
}
.d2 {
width: 100px;
height: 100px;
background-color: darkslateblue;
}
.d3 {
width: 50px;
height: 50px;
background-color: lightgray;
}
<div class="container">
<div class="d1">
<div class="d2">
<div class="d3">
</div>
</div>
</div>
</div>
So, here's the full code:
id = 0;
depth = 3;
customElements.define('box-component', class extends HTMLElement {
constructor() {
super();
if (id > depth)
return;
this.id = id++;
var template = document.getElementById("box-template");
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = template.innerHTML;
// The only goal of all of the following boilerplate is to set 'exportparts' attribute,
// such that each parent exports its *descendants* (not just *children*) parts.
// This is done by accessing the shadow dom of the child from its parent.
// (note: the outermost component doesn't need to export its parts)
if (id == 0)
return;
const parts = [...this.shadowRoot.children]
.filter(elem => elem.getAttribute('part'))
let thisExportParts = null;
let childExportParts = null;
thisExportParts = parts.map(elem => [
[elem.getAttribute('part'), `${elem.getAttribute('part')}${this.id}`].join(':')
].join(','));
if (this.shadowRoot && this.shadowRoot.children) {
const childShadowRoot =
[...this.shadowRoot.children].filter(elem => elem.shadowRoot)[0];
if (childShadowRoot) {
const fullChildExportParts = childShadowRoot.getAttribute('exportparts');
if (fullChildExportParts) {
childExportParts = fullChildExportParts.split(',').map(part => {
return part.includes(':') ? part.split(':')[1] : part;
}).join(',');
}
}
}
this.setAttribute('exportparts', [thisExportParts, childExportParts].filter(_ => _).join(','));
}
});
box-component::part(box1) {
width: 150px;
height: 150px;
background-color: lightseagreen;
}
box-component::part(box2) {
width: 100px;
height: 100px;
background-color: darkslateblue;
}
box-component::part(box3) {
width: 50px;
height: 50px;
background-color: lightgray;
}
<template id="box-template">
<style>
:host {
position: absolute;
top: 0px;
}
</style>
<div part="box"></div>
<box-component></box-component>
</template>
<box-component class="container"></box-component>