I'm trying to write a simple HTML Custom Element for putting trees on pages, using simple code like:
<html-tree title="root">
<tree-node title="child1">
<tree-node title="leaf1"></tree-node>
<tree-node title="leaf2"></tree-node>
</tree-node>
<tree-node title="child2">
<tree-node title="leaf3"></tree-node>
<tree-node title="leaf4"></tree-node>
</tree-node>
</html-tree>
Each element is basically a shadow dom with a single unnamed <slot></slot>
for putting children in, so I'd expect a nice, nested structure. Instead, if I assign the standard debug style :host>* { border:1px red solid; }
to these custom elements, each element shows up on its own line, with a border around it, rather than showing them as being nested.
How do I preserve the markup-specified nesting in a way that CSS plays nice?
A snippet:
/**
* Main tree node class
*/
class GenericNode extends HTMLElement {
constructor() {
super();
this._shadow = enrich(this.attachShadow({mode: `open`}));
this._shadow.addSlot = () => this._shadow.add(create(`slot`));
if (!this.get) {
this.get = e => this.getAttribute(e);
}
this.setupDOM(this._shadow);
}
setupDOM(shadow) {
this.setStyle(`:host>* { border:1px red solid; }`)
if (this.leadIn) this.leadIn(shadow);
shadow.addSlot();
if (this.leadOut) this.leadOut(shadow);
}
setStyle(text) {
if (!this._style) {
this._style = create(`style`, text);
this._shadow.add(this._style);
} else {
this._style.textContent = text;
}
}
}
/**
* "not the root" element
*/
class Node extends GenericNode {
constructor() {
super();
}
leadIn(shadow) {
shadow.add(create(`p`, this.get(`title`)));
}
}
// register the component
customElements.define(`tree-node`, Node);
/**
* "the root" element, identical to Node, of course.
*/
class Tree extends Node {
constructor() {
super();
}
}
// register the component
customElements.define(`html-tree`, Tree);
/**
* utility functions
*/
function enrich(x) {
x.add = e => x.appendChild(e);
x.remove = e => {
if (e) x.removeChild(e);
else e.parentNode.removeChild(e);
};
x.get = e => x.getAttribute(x);
return x;
}
function find(qs) {
return Array.from(
document.querySelectorAll(qs).map(e => enrich(e))
);
}
function create(e,c) {
let x = enrich(document.createElement(e));
x.textContent = c;
return x;
};
<html-tree title="root">
<tree-node title="child1">
<tree-node title="leaf1"></tree-node>
<tree-node title="leaf2"></tree-node>
</tree-node>
<tree-node title="child2">
<tree-node title="leaf3"></tree-node>
<tree-node title="leaf4"></tree-node>
</tree-node>
</html-tree>
Turns out the default styling of a shadow dom and its content is "nothing", so to effect real nesting, you need to force display:block
or be similarly explicit.
In the above code, rather than merely setting a border on the :host>*
, the :host
, and the <slot>
also need to be explicitly marked as blocks:
setupDOM(shadow) {
this.setStyle(`
:host {
display: block;
border: 1px red solid;
}
:host > slot {
display: block;
border: 1px red solid;
margin-left: 1em;
}
`);
...
}