HTML web component does not use shadow DOM style - javascript

I have created a vanilla web component or HTML element. It just displays two links.
To encapsulate the thing, I use shadow DOM. However it does not seem to be encapsulated. In the DOM tree it's inside #shadow-root which is good.
Why does the web component use the global style instead of the style I provided in the template for my web component?
The text is red and I expected it to be green.
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>

While this question already has an accepted answer, moving a slot's children to the shadowRoot isn't desirable for most use cases.
What you probably want to do is to use the ::slotted() selector.
Just bear in mind that styles applied to a slot's children through the ::slotted() selector only act as "default" styles and can still be overridden by using styles in light DOM.
For example, check this edited version of your snippet:
As you can see, this time my-el tries to apply both a color and a text-decoration style to anchor (<a>) children in any of it's slots.
However, in light dom, we have a a.special selector that overrides the color, so the <a class="special"> will be red, not green
class MyEl extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `
<style>
::slotted(a) {
color: green;
text-decoration: none;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = template;
}
}
window.customElements.define("my-el", MyEl);
a.special {
color: red
}
<my-el>
Item1
<a class="special" href="example.com">Item2</a>
</my-el>

The full, detailed explanation is in: ::slotted CSS selector for nested children in shadowDOM slot
TL;DR
Your links are in lightDOM and thus styled by its DOM (in your code the document DOM)
Moving the nodes from lightDOM to shadowDOM is one "solution"; but you are not using slots then.
FYI, your code can be compacted to:
class MyEl extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" })
.innerHTML = `<style>a{color:green}</style><slot></slot>`;
}
}
window.customElements.define("my-el", MyEl);
More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs

observe this line, you have to move/copy elements to shadow for example with:
this.shadow.innerHTML = this.innerHTML + template;
I've added this to demonstrate that only inline style will be applied to shadow dom elements .. so copied links in SD are using your style :)
so red will be GLOBAL, green will be SHADOW elements
class MyEl extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.shadow = this.attachShadow({ mode: "open" });
const template = `
<style>
a {
color: green;
}
</style>
<slot></slot>`;
this.shadow.innerHTML = this.innerHTML + template;
}
}
window.customElements.define("my-el", MyEl);
a {
color: red
}
<my-el>
Item1
Item2
</my-el>

Related

Create dynamic tags with Styled Components

Is there a way to create a dynamic tag using styled-components?
For example:
const MyComponent = styled(CustomTag)``;
<MyComponent element="h1" />
You can use as prop by default on components created with styled-components. If in your example CustomTag is also a styled-component that styles a native element (e.g.:)
const CustomTag = styled.h1`
color: red;
`;
then you can do
const MyComponent = styled(CustomTag)`
font-size: 64px;
`;
<MyComponent as="span">Something</MyComponent>
and you'll end up with a <span> tag with font-size of 64px and red text color. Of course you can also use as prop on CustomTag so you don't necessarily need MyComponent.
Maybe this will help you:
Add you code in your typescript
class Test extends HTMLElement {
connectedCallback() {
this.innerHTML = `<h1>Hello World...</h1>`;
this.style.color = "red";
}
}
customElements.define('test', Test);
after compiling refer the js file in you HTML and you can use it like <test></test>

Slots does not work on a html web component without shadow dom

I have a html web component without shadow dom and I try to add a slot. For some reason it does not work.
I expected it to switch "Foo bar" to "Hello world" but that does not happen.
Does slots only works with shadow dom and a template?
How can I get it to work?
class HelloWorld extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<div>
<slot name="element-name">Foo Bar</slot>
</div>
`;
}
}
customElements.define("hello-world", HelloWorld);
<hello-world>
<span slot="element-name">Hello World</span>
</hello-world>
Yes, <slot> only works in shadowDOM
Slotted content is reflected lightDOM content
See: ::slotted CSS selector for nested children in shadowDOM slot
A Web Component without shadowDOM only has innerHTML
If you do this.innerHTML= on such a Web Component it replaces the innerHTML, just like on any other HTML tag
with shadowDOM:
<hello-world>
<b slot="none">Mighty</b>
<span slot="title">Web Components</span>
Hello!
</hello-world>
<script>
customElements.define("hello-world", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = `<div><slot></slot><slot name="title">Foo Bar</slot></div>`;
this.onclick = (evt) => this.querySelector('b').slot = "title";
}
});
</script>

How to provide a fallback CSS-value within a custom element?

I have a custom web-component which is basically an SVG-Icon:
<custom-icon>
<svg>{svg-stuff}</svg>
</custom-icon>
I want to be able to change it's size by applying CSS like so:
custom-icon {
width: 20px;
}
But I also would like to have a fallback default value when no CSS is applied. However, when I inline some CSS like <custom-icon style="width:15px"> it just overwrites all CSS I apply afterwards. How can I have the default "15px" only apply if there is no custom CSS?
MWE:
class CustomIcon extends HTMLElement {
constructor() {
super();
let size = "100px"
this.style.height = size;
this.style.width = size;
this.style.background = "firebrick"
this.style.display = "block"
}
}
window.customElements.define('custom-icon', CustomIcon);
custom-icon {
--icon-size: 50px;
height: var(--icon-size);
width: var(--icon-size);
}
<custom-icon />
If the content of your custom element is encapsulated in a Shadow DOM, which is a recommended practice, you can use the :host pseudo-class to define a default style.
Then if you define a global style for your custom element it will override the one defined with :host.
customElements.define( 'custom-icon', class extends HTMLElement {
constructor() {
super()
let size = 100
this.attachShadow( { mode: 'open' } )
.innerHTML = `
<style>
:host {
display: inline-block ;
height: ${size}px ;
width: ${size}px ;
background-color: firebrick ;
color: white;
}
</style>
<slot></slot>`
}
} )
custom-icon#i1 {
--icon-size: 50px;
height: var(--icon-size);
width: var(--icon-size);
}
<custom-icon id="i1">sized</custom-icon>
<hr>
<custom-icon>default</custom-icon>
The order is applied according to the cascade.
CSS applied via the style attribute is at the bottom of the cascade. In effect, if you don't specify via the attribute when it falls back to the stylesheet.
So 20px is the fallback for when you don't specify 15px.
You could write your fallback CSS using another rule-set with a less specific selector (although the only thing less specific than a single type selector (like custom-icon) is the universal selector (*) which isn't helpful) so you would need to replace custom-icon with something more specific.
The other option is the take the sledgehammer approach and make every rule in your ruleset !important.
The best option would probably be to fix whatever circumstance might cause your CSS to be missing in the first place.
You can consider data attribute and then use that attribute as a fallback for the custom property.
You can see in the below, that the size will have no effect until we remove the custom property (by setting initial)
class CustomIcon extends HTMLElement {
constructor() {
super();
this.style.height = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.width = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.background = "firebrick"
this.style.display = "block"
}
}
window.customElements.define('custom-icon', CustomIcon);
custom-icon {
--icon-size: 50px;
margin:5px;
}
<custom-icon size="15px"></custom-icon>
<custom-icon size="25px"></custom-icon>
<custom-icon size="2050px"></custom-icon>
<custom-icon size="200px" style="--icon-size:initial"></custom-icon>
Related question to understand the use of initial : CSS custom properties (variables) for box model
Another example where the custom property is not set initially.
class CustomIcon extends HTMLElement {
constructor() {
super();
this.style.height = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.width = `var(--icon-size, ${this.getAttribute('size')})`;
this.style.background = "firebrick"
this.style.display = "block"
}
}
window.customElements.define('custom-icon', CustomIcon);
custom-icon {
margin: 5px;
}
.set {
--icon-size: 50px;
}
<div class="set">
<custom-icon size="15px"></custom-icon>
<custom-icon size="25px"></custom-icon>
<custom-icon size="2050px"></custom-icon>
</div>
<custom-icon size="200px" ></custom-icon>

Lit-Element: which event to use for DOM updates?

The documentation over at github.com/Polymer/lit-element describes the lifecycl, if a property of some lit-element is changed. However, I can not seem to find any documentation about a lifecycle if the DOM content of the element is changed.
So assume I have some nested DOM structure and my outermost element should display something based on the DOM content. For sake of simplicity the example below will just display the number of child-elements of the given type.
Now at some point my application inserts a new nested element (click the test button below). At this point I would like to update the shown count.
From my tests it seems that render() is not called again in that case, neither is updated().
Which event do I need to listen or which function do I need to implement for to recognize such a change?
My only current workaround is to use requestUpdate() manually after the DOM update, but I think such changes should be handled by lit-element itself.
document.querySelector( 'button' )
.addEventListener( 'click', () => {
const el = document.querySelector( 'my-element' );
el.insertAdjacentHTML( 'beforeend', '<my-nested-element>new addition</my-nested-element>' );
})
my-element, my-nested-element {
display: block;
}
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
<!-- Works only on browsers that support Javascript modules like Chrome, Safari, Firefox 60, Edge 17 -->
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
constructor(){
super();
this.number = this.querySelectorAll( 'my-nested-element' ).length;
}
render() {
return html`<p>number of my-nested-element: ${this.number}</p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
class MyNestedElement extends LitElement {
render() {
return html`<slot></slot>`;
}
}
customElements.define('my-nested-element', MyNestedElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>
In order to detect a new element inserted from the Light DOM through a <slot> element, you can listen to slotchange events on the <slot> element, or on the Shadow DOM root itself.
See the running example below:
document.querySelector('button').onclick = () =>
document.querySelector('my-element').insertAdjacentHTML('beforeend', '<my-nested-element>new addition</my-nested-element>');
my-element,
my-nested-element {
display: block;
}
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
firstUpdated() {
var shadow = this.shadowRoot
var nb = shadow.querySelector( 'span#nb' )
shadow.addEventListener( 'slotchange', () =>
nb.textContent = this.querySelectorAll( 'my-nested-element').length
)
}
render() {
return html`<p>number of my-nested-element: <span id="nb"></span></p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>

html custom element, slot content appended after #shadow-root

i try to create custom element with js. this is my custom element
class ImageBackground extends HTMLElement {
createdCallback() {
let src = this.hasAttribute('src') ? this.getAttribute('src') : '/static/images/user.png'
let className = `img-bg ${this.hasAttribute('className') ? this.getAttribute('className') : ''}`
let isLazy = this.getAttribute('lazy') !== false
const slotContent = document.createElement('slot')
slotContent.setAttribute('name', 'slot-content')
const wrapper = document.createElement('div')
wrapper.appendChild(slotContent)
wrapper.style.backgroundImage = `url("${src}")`
wrapper.style.backgroundSize = 'cover'
wrapper.style.backgroundPosition = 'center'
wrapper.style.height = '300px'
wrapper.setAttribute('class', className)
this.createShadowRoot().appendChild(wrapper)
}
}
document.registerElement('img-bg', ImageBackground)
and this is my pug template
img-bg(src="/static/images/email.svg")
p(slot="slot-content") cek
i want to append p element inside the slot. but the p element appended after the #shadow-root.
can anyone solve this... :( sorry for the bad english
<slot> is defined in Shadow DOM v1. Therefore you mus use attachShadow() instead of createShadowRoot():
this.attachShadow({ mode:'open'}).appendChild(wrapper)
The <slot> tag is used to import children of the element into the <slot> and not a place for you to put your own children.
The best you could do is to wrap your <slot> in something else and then place the component's <p> tag just after the <slot>.
Alternatively, and not recommended:
If you want to add any tag, generated by the component, into the slot then you need to place the tag as a child of your element and not into the shadowDOM. As long as it is placed as a proper child, matching the requirements of the <slot> than it should show in the <slot>.
class MyEl extends HTMLElement {
constructor() {
super();
this.attachShadow({mode:'open'}).innerHTML = `
<style>
.wrapper {
border: 1px solid black;
padding: 5px;
}
::slotted(*) {
background-color: #8F8;
margin: 1px;
padding: 5px;
}
</style>
<p>Before Slot</p>
<div class="wrapper"><slot>Hi There</slot></div>
<p>After Slot</p>
`;
}
connectedCallback() {
}
}
customElements.define('my-el', MyEl);
function addChild() {
var el = document.querySelector('my-el');
var p = document.createElement('p');
p.textContent = "New stuff as a child.";
el.appendChild(p);
}
var button = document.querySelector('#one');
button.addEventListener('click', addChild);
p {
font-family: tahoma;
}
my-el p {
font-weight: bold;
}
<p>Before Element</p>
<my-el>
<p>Stuff in the slot</p>
</my-el>
<p>After Element</p>
<hr/>
<button id="one">Add Child</button>
So the only way to add into the slot is to add child elements to the element.
That is probably not a good thing to do since you are altering the content that the user of your component created. And that could mess up their code, CSS or both.

Categories