How can I style elements used to construct custom elements? - javascript

class MyElement extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'open'})
shadow.appendChild(document.createTextNode('Yadda blah'))
const span = document.createElement('span')
span.appendChild(document.createTextNode('Can I style U?'))
shadow.appendChild(span)
}
}
customElements.define('my-element', MyElement)
my-element {
border: 1px solid black;
}
span {
font-weight: bold;
}
<my-element></my-element>
As you can see, my-element is styled, however, the span used within my-element is not. Not even saying my-element span {font-weight: bold;} in the stylesheet makes any styles effective.
Is there any way to apply styles to this span?

That is the expected behavior of shadow dom. Since your HTML looks like <my-element></my-element>, you can target my-element from your css, but the span is not part of the actual dom, so you can't target it.
You can only apply styles to the span from within the shadow dom context.
There are better ways of defining css in this case, but just to make a point, the following apply styles to the span, but not to my-element:
class MyElement extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'open'})
shadow.appendChild(document.createTextNode('Yadda blah'))
const span = document.createElement('span')
span.appendChild(document.createTextNode('Can I style U?'))
shadow.appendChild(span)
const styleTag = document.createElement('style')
styleTag.innerHTML = `
my-element {
border: 1px solid black;
}
span {
font-weight: bold;
}
`
shadow.appendChild(styleTag)
}
}
customElements.define('my-element', MyElement)
<my-element></my-element>

You have to apply the style to the shadow DOM. A common but soon-to-be-outdated technique is to add a <style> element to the shadow DOM and set its text contents as the CSS you want to apply within the custom element.
The snippet below does just that. The style is saved to a variable, but you can place it anywhere as long you're conscious of how often its context is run. Following Intervalia's advice, it's best to apply a style to the custom element using :host.
It's also possible to add a <link> element instead, but that may cause the element to pop-in without a style (FOUC)
const myElementStyle = `
:host {
border: 1px solid black;
}
span {
font-weight: bold;
}
`
class MyElement extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = myElementStyle;
shadow.appendChild(style);
shadow.appendChild(document.createTextNode('Yadda blah'));
const span = document.createElement('span');
span.appendChild(document.createTextNode('Can I style U?'));
shadow.appendChild(span);
}
}
customElements.define('my-element', MyElement)
<my-element></my-element>
Another option is to use Constructable Stylesheets, which should soon be available in modern browsers. These let you create a stylesheet object that can embed styles or import styles from external resources.
I wrote up an answer here, so I'll just post a snippet that answers your question:
const customSheet = new CSSStyleSheet();
customSheet.replaceSync(`
:host {
border: 1px solid black;
}
span {
font-weight: bold;
}
`);
class MyElement extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({
mode: 'open'
})
shadow.adoptedStyleSheets = [customSheet];
shadow.appendChild(document.createTextNode('Yadda blah'))
const span = document.createElement('span')
span.appendChild(document.createTextNode('Can I style U?'))
shadow.appendChild(span)
}
}
customElements.define('my-element', MyElement)
<my-element></my-element>

Related

Combine :host() with :has() - not possible?

I have a web component with a shadow DOM and a default slot.
I need to apply certain styling based on the presence or absence of specific a light DOM descendant. Please note that I don't need a specific workaround for this specific styling, it's just an example and in the real world the example is alot more complex.
I also cannot work with regular DOM CSS like x-y:has(div) since I need to apply styles to an element in the shadow DOM based on the presence of the div in the light DOM.
Please note that the code snippet only works in browsers that support constructable stylesheets (e.g. Safari won't).
const styleStr = `
:host {
display: block;
border: 3px dotted red;
}
:host(:has(div)) {
border-color: green;
}
`;
let css;
try {
css = new CSSStyleSheet;
css.replaceSync(styleStr);
} catch(e) { console.error(e) }
customElements.define('x-y', class extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).adoptedStyleSheets.push(css);
this.shadowRoot.append(document.createElement('slot'))
}
})
<x-y>no div - should have red border</x-y>
<x-y>
<div>div, should have green border</div>
</x-y>
I was trying to find if maybe :host() is not accepting :has(), but was unable to find anything on it, neither in the spec, nor on MDN or caniuse.
Does anyone have definitive knowledge/reference about this, and can point me to some documentation?
You want to style slotted content based on an element inside the slot
Since <slot> are reflected, (deep dive: ::slotted CSS selector for nested children in shadowDOM slot)
you need to style a <slot> in its container element.
If you want that logic to be done from inside the Component,
you could do it from the slotchange Event, which checks if a slotted element contains that DIV
Then creates a <style> element in the container element
Disclaimer: Provided code is a Proof of Concept, not production ready
<my-component>
Hello Web Component
</my-component>
<!-- <my-component> will add a STYLE element here -->
<my-component>
<!-- <my-component> will assign a unique ID to the DIV -->
<div>Web Component with a DIV in the slot</div>
</my-component>
<script>
customElements.define("my-component", class extends HTMLElement {
constructor() {
super().attachShadow({mode: "open"}).innerHTML = `<slot/>`;
let slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (evt) => {
[...slot.assignedNodes()].forEach(el => {
if (el.nodeName == "DIV") {
el.id = "unique" + new Date() / 1;
// inject a <style> before! <my-component>
this.before( Object.assign( document.createElement("STYLE"), {
innerHTML : `#${el.id} { background:lightgreen } `
}));
}
});
});
}
})
</script>
PS. Don't dynamically add any content inside <my-component>, because that slotchange will fire again...

shadow dom not attaching on class that inherit from HTMLButtonElement

I am trying to attach shadow dom to this but it does not attach and gives a error DOMExpection operation is not supported but when inherit from HTMLElement or HTMLSpanElement it works!
So question is can i attach shadow dom to this when inherit from HTMLButtonElement if yes then how else not why!
here is my code :-
class Home_Button extends HTMLButtonElement {
constructor() {
super();
this._dom = this.attachShadow({ mode: "open" });
this.createButton();
}
createButton() {
this.classList.add("contents");
const style = document.createElement("style");
style.textContent = `
.sentToHome{
margin: 1%;
padding: 1%;
border-radius: 5px;
border: 1px solid black;
}
.homeLink{
text-decoration: none;
color: green;
}
`;
const anchor = document.createElement("a");
anchor.innerText = "<< Home";
anchor.setAttribute("href", "/");
anchor.setAttribute("class", "homeLink");
const button = document.createElement("button");
button.setAttribute("class", "sentToHome");
button.appendChild(anchor);
this._dom.append(style, button);
}
}
customElements.define("simple-btn", Home_Button, { extends: "button" });
const sentToHomeBtn = new Home_Button();
document.body.appendChild(sentToHomeBtn);
This will never work in Safari, because Apple has, since 2016, stated they will never implement Customized Built-In Elements, they have only implemented extends HTMLElement
Trigger createButton from the connectedCallback; you can't do DOM operations in the constructor (besides shadowDOM content) because the element (this) doesn't exist in the DOM yet.
Also note this._dom isn't required, you get this.shadowRoot for free from attachShadow()
To be clear this.classList is the problem
Well, #danny-365csi-engelman is also right but
I found this answer.
According to the answer we can attach shadow dom on few elements and button does not exits in those elements.
I just inherit from HTMLElement class instead of HTMLButtonElement class.
After that everything works fine!.
If you want to check current source code go here.

Approaches for Styling Individual Letters in a Web Component

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.

Overriding externally-defined styles in a web component

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>

How to select sub-classed Custom Elements with CSS

Consider the following:
class MyElem extends HTMLElement {};
customElements.define('my-element', MyElem);
class MyMoreSpecificElem extends MyElem {};
customElements.define('my-more-specific-element', MyMoreSpecificElem);
In the parlance of object inheritance, instances of the second class have an 'is-a' relationship with the first: a MyMoreSpecificElem is a MyElem.
This relationship is captured in JavaScript:
let subclassInstance = document.querySelector('my-more-specific-element');
let parentClass = document.querySelector('my-element').constructor;
subclassInstance instanceof parentClass; // true
But I can't think of any way to select all MyElems (including the subclass MyMoreSpecificElem) using CSS selectors. A tag selector would only get the superclass or subclass, and all of the relationship description selectors I'm aware of (e.g. ~, >) are about position in the document, not class hierarchy.
I mean, sure, I can add a CSS class in the constructor and select by that, the call to super would ensure that even subclass instances could be selected that way. But that's gnarly. Is there a way to do this in pure CSS?
Why not have the base component class add either a className value or an attribute that will be set for both the base component class and all of the sub-component classes. Then your CSS can be set based on this className value or attribute.
class MyBaseEl extends HTMLElement {
connectedCallback() {
this.classList.add('my-base-el');
this.innerHTML = '<div>Base El</div>';
}
}
customElements.define('my-base-el', MyBaseEl);
class MySubEl extends MyBaseEl {
connectedCallback() {
super.connectedCallback();
this.innerHTML = '<div>Sub El</div>';
}
}
customElements.define('my-sub-el', MySubEl);
.my-base-el {
background-color: #FF0;
display: block;
outline: 1px dashed black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/webcomponents-lite.js"></script>
<my-base-el></my-base-el>
<my-sub-el></my-sub-el>
This uses a className value to allow the CSS to get at all of the elements that are MyBaseEl or a subclass.
Or like this:
class MyBaseEl extends HTMLElement {
connectedCallback() {
this.setAttribute('my-base-el', '');
this.innerHTML = '<div>Base El</div>';
}
}
customElements.define('my-base-el', MyBaseEl);
class MySubEl extends MyBaseEl {
connectedCallback() {
super.connectedCallback();
this.innerHTML = '<div>Sub El</div>';
}
}
customElements.define('my-sub-el', MySubEl);
[my-base-el] {
background-color: #FF0;
display: block;
outline: 1px dashed black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/webcomponents-lite.js"></script>
<my-base-el></my-base-el>
<my-sub-el></my-sub-el>
This uses an attribute to allow the CSS to get at all of the elements that are MyBaseEl or a subclass.
UPDATE
The only other way of setting CSS for multiple element tags is to know what all of them are and make the css work for all of them like this:
my-base-el, my-sub-el, my-other-sub-el, the-third-sub-el {
background: #000;
color: #fff;
}
But, if someone else can create additional sub-classes then you would need to add to the list.
UPDATE 2
Why not just set the :host in the base class and have it use a CSS variable for the styles you want the user to be able to change. Then everything will inherit that :host css and display like you want.

Categories

Resources