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;
}
`);
...
}
Related
class MyElement extends HTMLElement {
constructor() {
super();
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// data
// Shadow DOM
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
display: block;
}
span {color: ${this.color}}
</style>
<p>Notice the console displays three renders: the original, when color changes to blue after 2 secs, and when the array gets values</p>
<p>The color is: <span>${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.render();
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.render();
}
render() {
// Debug only
const props = Object.getOwnPropertyNames(this).map(prop => {
return this[prop]
})
console.log('Parent render; ', JSON.stringify(props));
// end debug
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
<!DOCTYPE html>
<head>
<script type="module" src="./src/my-element.js" type="module"></script>
<!-- <script type="module" src="./src/child-element.js" type="module"></script> -->
</head>
<body>
<p><span>Outside component</span> </p>
<my-element color="green"></my-element>
<script>
setTimeout(() => {
document.querySelector('my-element').color = 'blue';
document.querySelector('my-element').myArray = [1, 2, 3];
}, 2000);
</script>
</body>
I have a native web component whose attributes and properties may change (using getters/setters). When they do, the whole component rerenders, including all children they may have.
I need to rerender only the elements in the template that are affected.
import {ChildElement} from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span>${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
<child-element></child-element>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.render(); // It rerenders the whole component
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.render();
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
Because each setter calls render(), the whole component, including children unaffected by the updated property, rerenders.
Yes, if you go native you have to program all reactivity yourself.
(but you are not loading any dependencies)
Not complex, Your code can be simplified;
and you probably want to introduce static get observedAttributes and the attributeChangedCallback to automatically listen for attribute changes
customElements.define('my-element', class extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" }).innerHTML = `
<style id="STYLE"></style>
<p>The color is: <span id="COLOR"/></p>
<p>The array is: <span id="ARRAY"/></p>`;
}
connectedCallback() {
// runs on the OPENING tag, attributes can be read
this.color = this.getAttribute("color");
this.myArray = this.getAttribute("myArray"); // a STRING!!
}
get color() {
return this._color;
}
set color(value) {
this.setDOM("COLOR" , this._color = value );
this.setDOM("STYLE" , `span { color: ${value} }`);
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this.setDOM("ARRAY" , this._myArray = value );
}
setDOM(id,html){
this.shadowRoot.getElementById(id).innerHTML = html;
}
});
<my-element color="green" myArray="[1,2,3]"></my-element>
<my-element color="red" myArray="['foo','bar']"></my-element>
You are missing a certain level of abstraction. You are trying to emulate the Vue/React way of doing things where whenever the props change, the render() function is repeatedly called. But in these frameworks, render function doesn't do any DOM manipulation. It is simply working on Virtual DOM. Virtual DOM is simply a tree of JS object and thus very fast. And, that's the abstraction we are talking about.
In your case, it is best if you rely on some abstraction like LitElement or Stencil, etc. - any web component creation framework. They would take care of handling such surgical updates. It also takes care of scheduling scenarios where multiple properties are changed in a single event loop but render is still called exactly once.
Having said that, if you want to do this by yourself, one important rule: Never read or write to DOM from the web component constructor function. Access to DOM is after the connectedCallback lifecycle event.
Here is the sample that you can try and expand:
import { ChildElement } from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this.attached = false;
}
connectedCallback() {
// Check to ensure that initialization is done exactly once.
if (this.attached) {
return;
}
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this.render();
this.attached = true;
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span id="span">${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
<child-element></child-element>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.shadowRoot.querySelector('#span').textContent = this._color;
this.shadowRoot.querySelector('style').textContent = `
span { color: ${this._color}; }
`;
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.shadowRoot.querySelector('#span').innerHTML = this._myArray;
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
The above example is a classical vanilla JS with imperative way of updating the DOM. As said earlier to have reactive way of updating the DOM, you should build your own abstraction or rely on library provided abstraction.
I have created this lightbulb exercise that toggles the SRC of the lightbulb, the background color, as well as the color of the text. As an extra challenge, I'm trying to see if I can condense my code using the toggle() method as I have 3 separate functions. Does anyone know if this is possible and how I could accomplish this?
<h1 class="title-text" id="title-text">Click the lightbulb to turn it on or off!</h1>
<img id="lightbulb" onclick="toggleLight();toggleColor();toggleText()" src="/personal-projects/pic_bulbon1.gif">
<script>
let lightbulb = document.getElementById("lightbulb");
let titleText = document.getElementById("title-text");
function toggleLight() {
if (lightbulb.src.match("bulbon1")) {
lightbulb.src = "/personal-projects/pic_bulboff1.gif"
} else {
lightbulb.src = "/personal-projects/pic_bulbon1.gif"
}
}
function toggleColor() {
if (lightbulb.src.match("bulboff1")) {
document.body.style.background = "black";
} else {
document.body.style.background = "#FEDD00";
}
}
function toggleText() {
if (lightbulb.src.match("bulboff1")) {
titleText.style.color = "white";
} else {
titleText.style.color = "black";
}
}
</script>
</body>
</html>
The If loops work fine. I just want to know how I could use toggle if its possible. All the tutorials I find for this type of thing involve jquery.
Using toggle method
You want to first create a class for toggling in your css. Each element you want to toggle should have a default state and a toggled state. Toggling will add/remove a single class.
See snippet below
Snippet
const LightBulb = document.getElementById("lightbulb");
const toggleLight = () => {
LightBulb.classList.toggle("lightBulbOn");
document.body.classList.toggle("bodyLightOn");
};
body {
background-color: black;
}
body,
body .title-text {
color: white;
}
body.bodyLightOn {
background-color: #fedd00;
}
body.bodyLightOn .title-text {
color: black;
}
#lightbulb::before {
content: url("https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Gluehlampe_01_KMJ.png/340px-Gluehlampe_01_KMJ.png");
}
.lightBulbOn::before {
content: url("https://cdn.mos.cms.futurecdn.net/HaPnm6P7TZhPQGGUxtDjAg-320-80.jpg") !important;
}
<h1 class="title-text" id="title-text">Click the lightbulb to turn it on or off!</h1>
<div id="lightbulb" class="" onclick="toggleLight();">
</div>
Codepen
Let's say I want to create a custom element which bolds every other character. For example, <staggered-bold>Hello</staggered-bold> would become "Hello, where the H, l, and o are all bolded.
There's no nth-letter CSS selector, so as far as I know the only way to achieve this effect is to wrap each individual character with a span programmatically. To do that, I have an implementation that clones the text content into the Shadow Dom, so that the child content as specified by the user is not changed.
Unfortunately, by doing so, something like <staggered-bold><span class="red">red</span></staggered-bold> no longer works, because by cloning the content into the Shadow Dom, the class CSS declarations for the wrapped span no longer apply.
Here's a proof-of-concept implementation, showcasing that the red and blue text are in fact not red and blue:
customElements.define('staggered-bold', class extends HTMLElement {
constructor() {
super()
this
.attachShadow({ mode: 'open' })
.appendChild(document.getElementById('staggered-bold').content.cloneNode(true))
}
connectedCallback() {
// this is a shadow dom element
const text = this.shadowRoot.getElementById('text')
this.shadowRoot.querySelector('slot').assignedNodes().forEach(node => {
const content = node.textContent.split('').map((char) => {
return `<span class="char">${char}</span>`
}).join('')
const newNode = node.nodeType === Node.TEXT_NODE ? document.createElement('span') : node.cloneNode(true)
newNode.innerHTML = content
text.appendChild(newNode)
})
}
})
.red { color: red; }
.blue { color: blue; }
<p><staggered-bold>Some text</staggered-bold></p>
<p><staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold></p>
<template id="staggered-bold">
<style>
.hide { display: none; }
.char:nth-child(odd) {
font-weight: bold;
}
</style>
<span class="hide"><slot></slot></span>
<span id="text"></span>
</template>
My question is this: what is a good approach to styling each character in a custom element while preserving characteristics provided in the light dom?
One approach I've considered is to manipulate the light dom directly, but I have been avoiding that since I think of the light dom as being in full control of the usage-site (ie. things get complicated very quickly if external JS is manipulating the child of staggered-bold). I'm open to being convinced otherwise, especially there's no real alternative.
I've also considered cloning the content into a named slot so that the original text is preserved, and yet the content continues to live in the light dom. However, I feel like this is still kind of icky for the same reason as the previous paragraph.
You can't have the cake and eat it
Global CSS does NOT style shadowDOM (unless you use CSS properties)
Easier to not use shadowDOM at all.
With an extra safeguard: store the state so the element is properly redrawn on DOM moves.
Note: The setTimeout is always required,
because the connectedCallback fires early on the opening tag;
there is no parsed (innerHTML) DOM yet at that time.
So you have to wait for that DOM to be there.
If you do need a TEMPLATE and shadowDOM, dump the whole .innerHTML to the shadowRoot; but Global CSS still won't style it. Or <slot> it.
Do read: ::slotted CSS selector for nested children in shadowDOM slot
If you go with <slot> consider the slotchange Event
but be aware for an endless loop; changing lightDOM will trigger the slotchange Event again
<staggered-bold>Some text</staggered-bold>
<staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold>
<style>
staggered-bold { display: block; font: 21px Arial }
staggered-bold .char:nth-child(even) { color: blue }
staggered-bold .char:nth-child(odd) { color: red; font-weight: bold }
</style>
<script>
customElements.define('staggered-bold', class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure innerHTML is all parsed
if (this.saved) this.innerHTML = this.saved;
else this.saved = this.innerHTML;
this.stagger();
})
}
stagger(node=this) {
if (node.children.length) {
[...node.children].forEach( n => this.stagger(n) )
} else {
node.innerHTML = node.textContent
.split('')
.map(ch => `<span class="char">${ch}</span>`)
.join('');
}
}
})
document.body.append(document.querySelector("staggered-bold"));//move in DOM
</script>
In the end I attempted a strategy I'm calling the mirror node. The idea is the custom element actually creates an adjacent node within which the split characters are placed.
The original node remains exactly as specified by the user, but is hidden from view
The mirror node actually displays the staggered bold text
The below implementation is incomplete, but gets the idea across:
class StaggeredBoldMirror extends HTMLElement {
constructor() {
super()
}
}
customElements.define('staggered-bold', class extends HTMLElement {
constructor() {
super()
this
.attachShadow({ mode: 'open' })
.appendChild(document.getElementById('staggered-bold').content.cloneNode(true))
}
connectedCallback() {
setTimeout(() => {
const mirror = new StaggeredBoldMirror()
mirror.innerHTML = this.divideIntoCharacters()
this.parentNode.insertBefore(mirror, this)
})
}
divideIntoCharacters = (node = this) => {
return [...node.childNodes].map(n => {
if (n.nodeType === Node.TEXT_NODE) {
return n.textContent
.split('')
.map(ch => `<span class="char">${ch}</span>`)
.join('')
} else {
const nn = n.cloneNode(false)
nn.innerHTML = this.divideIntoCharacters(n)
return nn.outerHTML
}
}).join('')
}
})
customElements.define('staggered-bold-mirror', StaggeredBoldMirror)
.red {
color: red;
}
.blue {
color: blue;
}
staggered-bold-mirror .char:nth-child(odd) {
font-weight: bold;
}
<p><staggered-bold>Some text</staggered-bold></p>
<p><staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold></p>
<template id="staggered-bold">
<style>
.hide { display: none; }
</style>
<span class="hide"><slot></slot></span>
</template>
The vanilla component can be outfitted with a slotchange listener in order to rebuild its mirror whenever its inner content changes. The disconnectedCallback method can also ensure that when one node is removed, the other is too.
Of course, there are downsides to this approach, such has potentially having to also mirror events and the fact that it still manipulates the light dom.
Depending on the use case, either this or Danny's answer works.
On a site a CSS class is periodically added to an element by JavaScript.
I'd like not to show the visible effect of that class. In other words what I need is an effect similar to switching the class off in the development console of the browser. Let the class exist but without any consequences.
I understand that I can catch events and remove the class when it appears.
But maybe there is just a more simple way in my case?
In other words: there is a CSS class, I would like it to be present but without any visual effects. If it is impossible, that will also be an answer.
You could use something like this. Access the document's stylesheets and apply some sort of regex matching to figure out which rules are associated with the class in question. Then simply unset the styling on the rules. Note that just because the class name is found in the stylesheet rule doesn't mean it is the element being affected by the styles...but this should get you going in the right direction.
function removeClassStyling(clazz) {
var classRegex = new RegExp('\\.'+clazz.toLowerCase()+'\\b','i')
for (var s=0; s<document.styleSheets.length; ++s) {
var sheet = document.styleSheets[s];
for(var r=0; r<sheet.cssRules.length; ++r) {
var rule = sheet.cssRules[r];
if(rule.selectorText && rule.selectorText.match(classRegex)) {
var properties = Object.keys(rule.style);
for(var p=0; p<properties.length; ++p){
if(rule.style[properties[p]]) rule.style[properties[p]] = "";
}
console.log('removed styling for "'+clazz+'"');
}
}
}
}
setTimeout(function(){ removeClassStyling('unwanted-class') }, 1500)
.unwanted-class {
border: 1px solid red;
}
<div class="unwanted-class"> Test </div>
This should do the trick in most circumstances. I imagine there are circumstances that would evade this, but I can't think of them.
Basically you need to iterate document.styleSheets collection, then iterate each rule contained within and compare the CSSStyleRule.selectorText for each rule against a regular expression.
Regular expressions can be faulty, so I've included a check against an element with the supplied class name using the Element#matches() method. That method can also provide false positives in the case where the element matches some other part of the selector, so the two together should reasonably prevent any false positives.
Once you have a list of all the CSS rules that apply to a given class, you can simply delete them all. This can be done in the same step as finding them, but I've done it separately in for example's sake.
function findClassRules(name) {
const element = document.createElement('div')
element.classList.add(name)
const regex = new RegExp(`\\\.${ name }([^\w]|$)`, 'i')
const test = {
rule: rule => {
if('cssRules' in rule) {
return test.sheet(rule)
} else if('selectorText' in rule) {
const selector = rule.selectorText
return selector.match(regex) && element.matches(selector) && rule
}
},
sheet: sheet => {
const rules = Array.from(sheet.cssRules, test.rule).filter(Boolean)
return rules.length && { sheet, rules }
}
}
return Array.from(document.styleSheets, test.sheet).filter(Boolean)
}
function processSheet({ sheet, rules }) {
rules.forEach(rule => {
if('rules' in rule) {
processSheet(rule)
} else {
sheet.deleteRule(rule)
console.log(`Removed: ${ rule.cssText }`)
}
})
}
document.getElementById('clean').addEventListener('click', event => {
findClassRules('test').forEach(processSheet)
}, false)
.test { padding: 5px }
.test2 { padding: 10px }
#media screen {
.test { margin: 15px }
}
<p class="test">Hello world!</p>
<button id="clean">Remove CSS</button>
<style type="text/css">
.test { color: red }
</style>
<style type="text/css">
.test { border: 1px solid red }
</style>
How about you comment the class in your css file
I'm taking my first steps into web components without using any third-party libraries, such as Polymer. One of the main selling points is that web component styles are separated from styles defined elsewhere, allowing the component's shadow-DOM to be styled in a sandbox-like environment.
The issue I'm running into is how styles cascade through slotted elements. Since slotted elements are not part of the shadow DOM, they can only be targed with the ::slotted() selector within the component template. This is great, but it makes it almost impossible to guarantee a web component will display correctly in all contexts, since externally-defined styles also apply with undefeatable specificity* to slotted elements.
*besides !important.
This issue can be distilled down to this:
customElements.define("my-nav",
class extends HTMLElement {
constructor() {
super();
const template = document.querySelector("template#my-nav").content;
this.attachShadow({ mode: "open" })
.appendChild(template.cloneNode(true));
}
}
);
a {
color: red; /* >:( */
}
<template id="my-nav">
<style>
.links-container ::slotted(a) {
color: lime;
font-weight: bold;
margin-right: 20px;
}
</style>
<div class="links-container">
<slot name="links"></slot>
</div>
</template>
<p>I want these links to be green:</p>
<my-nav>
Link 1
Link 2
Link 3
</my-nav>
I'm having a hard time understanding the value of this "feature". I either have to specify my links in some other format and create their nodes with JS, or add !important to my color property - which still doesn't guarantee consistency when it comes to literally any other property I haven't defined.
Has this issue been addressed somewhere, or is this easily solved by changing my light DOM structure? I am not sure how else to get a list of links into a slot.
The <slot> is intentionally designed to allow the outer code to style the content placed into it. This is a great feature when used correctly.
But if you want better control of what shows in the web component then you need to copy cloned copies of the content from this.childNodes into the shadow DOM. Then you have 100% control over the CSS.
OK. You really only have 90% control because the person using your component can still set the style attribute.
customElements.define("my-nav",
class extends HTMLElement {
constructor() {
super();
const template = document.querySelector("template#my-nav").content;
this.attachShadow({ mode: "open" })
.appendChild(template.cloneNode(true));
}
connectedCallback() {
var container = this.shadowRoot.querySelector('.links-container');
var children = this.childNodes;
if (children.length > 0 && container) {
while(container.firstChild) {
container.removeChild(container.firstChild);
}
for (var i = 0; i < children.length; i++) {
container.appendChild(children[i].cloneNode(true));
}
}
}
}
);
a {
color: red;
}
<template id="my-nav">
<style>
.links-container a {
color: lime;
font-weight: bold;
margin-right: 20px;
}
</style>
<div class="links-container">
</div>
</template>
<p>I want these links to be green:</p>
<my-nav>
Link 1
Link 2
Link 3
</my-nav>
As you can see in the example above the third link is still red because we set the style attribute.
If you want to prevent that from happening then you would need to strip the style attribute from the inner content.
customElements.define("my-nav",
class extends HTMLElement {
constructor() {
super();
const template = document.querySelector("template#my-nav").content;
this.attachShadow({ mode: "open" })
.appendChild(template.cloneNode(true));
}
connectedCallback() {
var container = this.shadowRoot.querySelector('.links-container');
var children = this.childNodes;
if (children.length > 0 && container) {
while(container.firstChild) {
container.removeChild(container.firstChild);
}
for (var i = 0; i < children.length; i++) {
container.appendChild(children[i].cloneNode(true));
}
container.querySelectorAll('[style]').forEach(el => el.removeAttribute('style'));
}
}
}
);
a {
color: red;
}
<template id="my-nav">
<style>
.links-container a {
color: lime;
font-weight: bold;
margin-right: 20px;
}
</style>
<div class="links-container">
</div>
</template>
<p>I want these links to be green:</p>
<my-nav>
Link 1
Link 2
Link 3
</my-nav>
I have even created some components that allow unique children that I read in and convert into custom internal nodes.
Think of the <video> tag and its <source> children. Those children don't really render anything, they are just a way of holding data that is used to indicate the source location of the video to be played.
The key here is to understand what <slot> is supposed to be used for and only use it that way without trying to force it to do something it was never intended to do.
BONUS POINTS
Since ConnectedCallback is called every time this node in placed into the DOM you have to be careful to remove anything within the shadow DOM each time or you will duplicate the children over and over.
customElements.define("my-nav",
class extends HTMLElement {
constructor() {
super();
const template = document.querySelector("template#my-nav").content;
this.attachShadow({ mode: "open" })
.appendChild(template.cloneNode(true));
}
connectedCallback() {
var container = this.shadowRoot.querySelector('.links-container');
var children = this.childNodes;
if (children.length > 0 && container) {
for (var i = 0; i < children.length; i++) {
container.appendChild(children[i].cloneNode(true));
}
}
}
}
);
function reInsert() {
var el = document.querySelector('my-nav');
var parent = el.parentNode;
el.remove();
parent.appendChild(el);
}
setTimeout(reInsert, 1000);
setTimeout(reInsert, 2000);
a {
color: red;
}
<template id="my-nav">
<style>
.links-container a {
color: lime;
font-weight: bold;
margin-right: 20px;
}
</style>
<div class="links-container">
</div>
</template>
<p>I want these links to be green:</p>
<my-nav>
Link 1
Link 2
Link 3
</my-nav>
So removing the duplicated nodes is important:
customElements.define("my-nav",
class extends HTMLElement {
constructor() {
super();
const template = document.querySelector("template#my-nav").content;
this.attachShadow({ mode: "open" })
.appendChild(template.cloneNode(true));
}
connectedCallback() {
var container = this.shadowRoot.querySelector('.links-container');
var children = this.childNodes;
if (children.length > 0 && container) {
while(container.firstChild) {
container.removeChild(container.firstChild);
}
for (var i = 0; i < children.length; i++) {
container.appendChild(children[i].cloneNode(true));
}
}
}
}
);
function reInsert() {
var el = document.querySelector('my-nav');
var parent = el.parentNode;
el.remove();
parent.appendChild(el);
}
setTimeout(reInsert, 1000);
setTimeout(reInsert, 2000);
a {
color: red;
}
<template id="my-nav">
<style>
.links-container a {
color: lime;
font-weight: bold;
margin-right: 20px;
}
</style>
<div class="links-container">
</div>
</template>
<p>I want these links to be green:</p>
<my-nav>
Link 1
Link 2
Link 3
</my-nav>
You're right, there's no solution other that using !important for every CSS property.
Instead, I would not use <slot> and copy the nodes you need:
customElements.define("my-nav",
class extends HTMLElement {
constructor() {
super();
const template = document.querySelector("template#my-nav").content;
this.attachShadow({ mode: "open" })
.appendChild(template.cloneNode(true));
}
connectedCallback() {
var links = this.querySelectorAll( 'a[slot]' )
var container = this.shadowRoot.querySelector( '.links-container' )
links.forEach( l => container.appendChild( l ) )
}
}
);
a {
color: red; /* >:( */
}
<template id="my-nav">
<style>
.links-container > a {
color: lime;
font-weight: bold;
margin-right: 20px;
}
</style>
<div class="links-container">
</div>
</template>
<p>I want these links to be green:</p>
<my-nav>
Link 1
Link 2
Link 3
</my-nav>