I have the following simple component:
Usage:
<style>
my-element {
--my-bg: green;
--my-text: red;
}
</style>
<my-element myStyling>
<p>Test</p>
</my-element>
Component:
const template = document.createElement('template');
template.innerHTML = `
<style>
:host([myStyling]), :host([myStyling]) {
background-color: var(--my-bg);
color: var(--my-text);
}
</style>
<slot></slot>
Static
`;
class MyElement extends HTMLElement {
constructor() {
super();
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
The code outputs the following result:
Why the color: green applies on the static text and the shadow DOM both, while the background color style applies only on the static text?
Default value for CCS property color is inherit.
Default style for CSS property background-color is transparent (won't inherit from its parent element).
Default custom element display property is inline (= phrasing content) and therefore won't settle background properties to its children.
In your code, the "Test" text is in a <p> element, that won't inherit from the :host background color, but will be transparent and therefore will display the background color of the main page, which is white.
See the live example below for a complete use case.
const template = document.createElement('template')
template.innerHTML = `
<style>
:host {
background-color: var(--my-bg);
color: var(--my-text);
}
</style>
<slot></slot>
<hr>
Text in Shadow DOM root
<p>Text in Paragraph in Shadow DOM <span>and child Span</span></p>
<span>Text in Span in Shadow DOM <p>and child Paragraph</p></span>`
class MyElement extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
.appendChild(template.content.cloneNode(true))
}
}
window.customElements.define('my-element', MyElement)
body {
background-color: lightblue;
}
my-element {
--my-bg: green;
--my-text: red;
}
<my-element myStyling>
Text in Light DOM root
<p>Text in Paragraph in Light DOM <span>and Child Span</span></p>
<span>Text in Span in Light DOM <p>and child Paragraph</p></span>
</my-element>
If you want the background-color to be applied to all the child elements inside the Shadow DOM, you must apply the css rule to the * selector too:
:host, * {
background-color: ...
}
If you want the background-color to be applied to all the light DOM elements inserted with <slot>, you must add a ::slotted(*) pseudo-element rule:
:host, *, ::slotted(*) {
background-color: ...
}
Alternate approach
If you want the background-color to be applied between the different parts of text, don't forger to define the display property as inline-block or block (= flux content).
As a consequence all children will display the root block background-color.
Here is the complete <style> definition for the Shadow DOM:
:host {
display: inline-block ;
color: var(--my-text);
background-color: var(--my-bg);
}
Related
I've built a web component and I need to set the body of the component when I construct it.
My web component is:
import {html, PolymerElement} from '#polymer/polymer/polymer-element.js';
class TextBlock extends PolymerElement {
static get template() {
var body;
return html`
{body}
`;
}
}
window.customElements.define('text-block', TextBlock)
The page that contains the component is dynamically generated. What I want to do is insert a text-block element into the page as:
<body>
<text-block>
<H1>Title of TextBlock</H1>
</text-block>
</body>
My problem is that I don't understand how to get the web component to take the content between the text-block start/end tags and return it from the call to template()
Whilst not shown, I'm using a webcomponent as I need to style the text using shadow dom.
Don't use Polymer for new Web Components; Google deprecated it years ago.
See: https://polymer-library.polymer-project.org/
Google can't even keep that page up to date; they renamed Lit-Element/html to just Lit.
See: https://lit.dev
Or its 60+ alternatives: https://webcomponents.dev/blog/all-the-ways-to-make-a-web-component/
You can write Vanilla JavaScript Web Components without the soup-starter Lit
Here is a more extended example to cover your next questions:
<style> /* GLOBAL CSS */
text-block { /* inheritable styles trickle down into shadowDOM */
font: 20px Arial;
color: green;
}
div[slot] { /* lightDOM styles are REFLECTED to <slot> in shadowDOM */
width: 200px;
border: 2px solid green;
}
</style>
<text-block prefix="Hello!">
<!-- lightDOM, hidden when shadowDOM is used, then REFLECTED to <slot> -->
<div slot="username">Brett</div>
</text-block>
<text-block prefix="Hi,">
<!-- lightDOM, hidden when shadowDOM is used, then REFLECTED to <slot> -->
<div slot="username">Danny</div>
</text-block>
<template id="TEXT-BLOCK">
<style> /* styling shadowDOM */
div { /* styles do NOT style content inside <slot>, as <slot> are REFLECTED content */
padding: 1em;
background:gold;
}
::slotted(*) { /* slotted can only style the lightDOM 'skin' */
background: lightgreen;
}
</style>
<div>
<slot name="username"></slot>
</div>
</template>
<script>
customElements.define('text-block', class extends HTMLElement {
constructor() {
// see mum! You can use code *before* super() The documentation is wrong
let colors = ["blue","red"];
let template = id => document.getElementById(id).content.cloneNode(true);
super().attachShadow({mode: 'open'})
.append(template(this.nodeName)); // get template TEXT-BLOCK
this.onclick = (evt) => this.style.color = (colors=colors.reverse())[0];
}
connectedCallback(){
this.shadowRoot.querySelector("div").prepend( this.getAttribute("prefix") );
}
});
</script>
For (very) long answer on slots see: ::slotted CSS selector for nested children in shadowDOM slot
How can I style parts which have been slotted into a web component?
My goal is to create 'functional' components which only renders some parts based on the state that they are in (using Redux). These components are then rendered in a container.
This containers knows which kinds of children it can expect and should style the parts from the children accordingly.
An example is a feed of posts, in which all posts are the same web component which only render some parts.
Then there is a grid feed component and renders these posts in a grid.
Another chronological feed might simple render the posts underneath each other.
In both cases the post itself is not aware in the context that it is in.
I want the container component to be responsible for the layout/styling.
I kinda want the inverse of host-context.
With host-context the post component knowns in which containers it can be in and style themself accordingly. But I would like to keep my post component purely functional without any styling. (and host-context is not well supported)
This snippet shows my attempts to make the title of the post red when it is in a my-grid element. But none of my attempts work.
customElements.define('my-grid', class extends HTMLElement {
constructor() {
super().attachShadow({
mode: 'open'
})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
});
customElements.define('my-post', class extends HTMLElement {
constructor() {
super().attachShadow({
mode: 'open'
})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
});
<template id="MY-GRID">
<style>
:host {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
grid-auto-rows: minmax(auto, 100px);
}
::slotted(my-post) {
display: contents;
}
/* Attempts at styling the parts withing the slotted posts */
my-post h1,
::slotted(my-post) h1 {
background-color: red;
}
my-post ::part(test),
::slotted(my-post) ::part(test) {
background-color: red;
}
my-post::part(test),
::slotted(my-post)::part(test) {
background-color: red;
}
</style>
<slot></slot>
</template>
<template id="MY-POST">
<h1 part="title">Title</h1>
</template>
<my-grid>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
</my-grid>
You had multiple issues
Typos: You are mixing title and test for your ::part(x) references
::slotted is a very simple selector, so you can discard all those tries
(per above link) Slotted content remains in lightDOM; so your elements in lightDOM:
<my-grid>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
</my-grid>
must be styled from its container.... in this case the main document DOM
So all global style required is:
my-post::part(title) {
background: red;
}
You can not do this in <my-grid> because <my-post> is not inside <my-grid> lightDOM
<my-grid> can not style its slotted content (only the 'outer' skin with ::slotted)
I added extra styling, slots and a nested <my-post> element to make things clear
<script>
class GridElements extends HTMLElement {
constructor() { super().attachShadow({mode: 'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true)) }}
customElements.define('my-grid', class extends GridElements {});
customElements.define('my-post', class extends GridElements {});
</script>
<template id="MY-GRID">
<style>
:host{display:grid;grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) }
::slotted(my-post) { background: green }
</style>
<slot></slot>
</template>
<style id="GLOBAL_STYLE!!!">
body { font: 14px Arial; color: blue }
my-post::part(title) { background: red }
my-grid > my-post::part(title) { color: gold }
my-post > my-post::part(title) { background:lightcoral }
</style>
<template id="MY-POST">
<h1 part="title"><slot name="title">[Title]</slot></h1>
<slot>[post body]</slot>
</template>
<my-grid>
<my-post><span slot="title">One</span></my-post>
<my-post>Two</my-post>
<my-post></my-post>
<my-post>
<my-post><span slot="title">SUB</span></my-post>
</my-post>
</my-grid>
::part Styling declared from my-grid
I added a my-post to the my-grid lightDOM:
See: https://jsfiddle.net/WebComponents/75xgt3y2/
So if you still want to declare the content in the main DOM, but style from my-grid, you have to move elements from the main DOM to my-grid lightDOM:
connectedCallback(){
this.shadowRoot.append(...this.children);
}
Update #1
Also see exportparts for crossing multiple shadowDOM boundaries:
https://meowni.ca/posts/part-theme-explainer/
https://caniuse.com/mdn-html_global_attributes_exportparts
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/exportparts (jan 2021 - no documentation page yet)
I have a web component that has a template which looks like this...
<template>
<div class="jrg-app-header">
<slot name="jrg-app-header-1"></slot>
<slot name="jrg-app-header-2"></slot>
<slot name="jrg-app-header-3"></slot>
</div>
</template>
I am basically trying to set the contents of the last slot to have flex:1; in style. Is there a CSS query that will do this? I tried something list
::slotted(*):last-child{
flex:1;
}
But it did not work. How do I style the last slotted object?
For long answer on ::slotted see: ::slotted CSS selector for nested children in shadowDOM slot
From the docs: https://developer.mozilla.org/en-US/docs/Web/CSS/::slotted
::slotted( <compound-selector-list> )
The pseudo selector goes inside the brackets: ::slotted(*:last-child)
Note: :slotted(...) takes a simple selector
See (very) long read: ::slotted CSS selector for nested children in shadowDOM slot
customElements.define('my-table', class extends HTMLElement {
constructor() {
let template = (name) => document.getElementById(name)
.content.cloneNode(true);
super()
.attachShadow({ mode: 'open' })
.append( template(this.nodeName) );
}
})
<template id="MY-TABLE">
<style>
:host { display: flex; padding:1em }
::slotted(*:first-child) { background: green }
::slotted(*:last-child) { background: yellow; flex:1 }
::slotted(*:first-of-type) { border: 2px solid red }
::slotted(*:last-of-type) { border: 2px dashed red }
</style>
<slot name="column"></slot>
</template>
<my-table>
<div slot="column">Alpha</div>
<div slot="column">Bravo</div>
<div slot="column">Charlie</div>
</my-table>
<my-table>
<div slot="column">Delta</div>
<div slot="column">Echo</div>
</my-table>
JSFiddle playground:
https://jsfiddle.net/WebComponents/108ey7b2/
More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs
slot is good to make a reusable web component, however, it has a limit so far. What I faced is the style problem. You just can't define the style inside a component, even you know what the inject content's structure would be.
Details found from my post in github here
I write a component, and try to inject content through slot from the outside and try to add style to the specific content in the component's shadow root.
Demo
HTML file
<my-navbar>
<ul>
<li>link1</li>
<li>link2</li>
<li>link3</li>
</ul>
</my-navbar>
JS file
customElements.define('my-navbar', class extends HTMLElement {
constructor () {
super();
const sr = this.attachShadow({ mode: 'open' });
sr.innerHTML = `
<style>
/*worked*/
::slotted(ul)
{
color:green;
}
/*
Suppose I know the outside content is "ul li", and I directly define the
style after they injected into component's slot. However, it just doesn't
work because the slotted selector is just a compound selector. It can only
affect the first layer 'ul'. It can't affect the child dom 'li' */
::slotted(ul li)
{
color:red;
}
</style>
<slot></slot>
`;
}
});
However, it just doesn't work directly because you just can't use a complex selector for ::slot(simple_selector)
Reason
I found an indirect solution, and that's to re-append the outside content into the slots inside the component's shadow root.
Demo
HTML file
<my-navbar>
<!--a dom defined a slot property-->
<ul slot='t'>
<li>link1</li>
<li>link2</li>
<li>link3</li>
</ul>
<!--A dom not define slot property-->
<span>1234</span>
</my-navbar>
JS file
customElements.define('my-navbar', class extends HTMLElement {
constructor () {
super();
const sr = this.attachShadow({ mode: 'open' });
sr.innerHTML = `
<style>
ul li
{
color:red;
}
</style>
<slot name='t'></slot>
<slot ></slot>
`;
// Do something later...
setTimeout(this.appendOutsideSlotContentIntoInsideSlot.bind(this), 1000)
}
appendOutsideSlotContentIntoInsideSlot()
{
// Insert outside dom element which has define slot property into the specify slot inside the shadow root
debugger;
for (let objIndex=0;objIndex<this.children.length;)
{
var obj=this.children[objIndex];
if(obj.slot){
var slot=this.shadowRoot.querySelector('slot[name='+obj.slot+']');
if(slot)
{
slot.appendChild(obj);
continue;
}
}
objIndex++;
}
// Insert the rest dom which has not define slot property values into the anonymous slot
var defaultSlot=Array.prototype.slice.call(this.shadowRoot.querySelectorAll('slot')).filter(function(el){ return !el.name})[0];
debugger;
if(defaultSlot)
{
while (this.children.length>0)
{
defaultSlot.appendChild(this.children[0])
}
}
}
});
Well, it works for the contents which have defined the slot property, but doesn't work again with content that has no slot property.
With the exception of a few inheritable rules the contents of the slot are not supposed to be directly affected by your component's shadow CSS. They are designed to allow the CSS outside of your component to be in control.
That is by design.
This is similar to the protection given to elements within the shadow DOM not being affected by external CSS.
Read the section Styling distributed nodes found here: https://developers.google.com/web/fundamentals/web-components/shadowdom#stylinglightdom
You are only allowed to change CSS rules for the top level elements inside the slot. And you are even limited to what you can do to that. All child elements are controlled by the CSS outside of the shadow DOM.
In the example below you will see that we can change the color and background color of the top level elements, or the <ul> tags:
customElements.define('my-navbar', class extends HTMLElement {
constructor () {
super();
const sr = this.attachShadow({ mode: 'open' });
sr.innerHTML = `
<style>
::slotted(ul)
{
color: blue;
}
::slotted(.bold) {
font-weight: bold;
background-color: #222;
color: #FFF;
}
::slotted(.italic) {
font-style: italic;
background-color: #AAA;
color: #000;
}
::slotted(*)
{
color: red;
}
</style>
<slot></slot>
`;
}
});
<my-navbar>
<ul class="bold">
<li>link1</li>
<li class="italic">link2</li>
<li>link3</li>
</ul>
<ul class="italic">
<li>link1</li>
<li class="bold">link2</li>
<li>link3</li>
</ul>
</my-navbar>
In the example above, the only reason the text is red and not blue is because ::slotted(*) affects just the two <ul>, has the same specificity as ::slotted(ul) and is placed after ::slotted(ul). The color is inherited by the <li> tags because that is how CSS works.
The background colors only affect the <ul> tags based on their classes and not the <li> tags with identical classes.
In the example below, the <li> color and background-color are controlled by the CSS outside the shadow DOM. The external rules act as if they are more specific then the shadow DOM rules even though the shadow DOM rules included both a tag and a class selector (ul.bold).
Again, this is by design.
customElements.define('my-navbar', class extends HTMLElement {
constructor () {
super();
const sr = this.attachShadow({ mode: 'open' });
sr.innerHTML = `
<style>
::slotted(ul)
{
color: blue;
}
::slotted(ul.bold) {
font-weight: bold;
background-color: #222;
color: #FFF;
}
::slotted(ul.italic) {
font-style: italic;
background-color: #AAA;
color: #000;
}
::slotted(*)
{
color: red;
}
</style>
<slot></slot>
`;
}
});
li {
color: #555;
backgroung-color: #ddd;
}
.bold {
font-weight: bold;
background-color: #FF0;
}
.italic {
font-style: italic;
background-color: #0FF;
}
<my-navbar>
<ul class="bold">
<li>link1</li>
<li class="italic">link2</li>
<li>link3</li>
</ul>
<ul class="italic">
<li>link1</li>
<li class="bold">link2</li>
<li>link3</li>
</ul>
</my-navbar>
You will note that the background colors of the <ul> and <li> tags are set based on the external classes of bold and italic.
If you want to use a <slot> the you agree that the developer using your component has the override power for anything that is placed into the slot.
If you don't want the user to have that kind of control then the only way to prevent it is to move the component's children into the component's shadow DOM.
But be careful when you do it.
According to the rules of Web Component constructors you can not access or change the children of a component while in the constructor.
But you have to remember that the connectedCallback is called every time the component is inserted into the DOM. So if the developer removes and then re-appends your component then the connectedCallback will be called again. So you have to add a gate to prevent it from getting called twice.
Also you might want to add a MutationObserver to see when the user changes the children of your components.
What i have understood of host is that if i have a child component inside a parent component and we want to style a child component from the parent component we can use :host .
and :host-context for vice-versa.
Please let me know if this is the right use of host .
https://angular.io/docs/ts/latest/guide/component-styles.html
When i try to do the same in my App it dosent work
App component template
<div class ="top">
<h1>
Home Component
</h1>
<hr>
<app-ngrx></app-ngrx>
<router-outlet></router-outlet>
<div>
ngrx component template
<h3 class="mine">NGRX</h3>
<button (click)="increment()">Increment</button>
<div>Current Count: {{ counter | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
App component CSS
:host(.mine){
color:red;
}
This do not Seem to work Please help i am not able to understand.
I looked at this question But just not able to figure out
Angular 2: How to style host element of the component?
Updated after #Gunter Answer
In my app-ngrx template i have added
<h3 class = "mine">NGRX</h3>
<button (click)="increment()">Increment</button>
<div>Current Count: {{ counter | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
and in the app-ngrx css file i have added
:host(.mine){
color:red;
}
But even without adding mine in app component like
<app-ngrx></app-ngrx>
The h3 is red where as i feel it should be red when <app-ngrx class = "mine"></app-ngrx>
What i have understood of host is that if i have a child component
inside a parent component and we want to style a child component from
the parent component we can use :host . and :host-context for
vice-versa
No, this is not what it used for.
:host selector comes from shadow DOM spec.
...This scoped subtree is called a shadow tree. The element it's
attached to is its shadow host.
In angular world, a component's template is a shadow tree. The component's element is a shadow host. So when you're defining styles for :host selector, the styles are applied to the component's element.
:host
In your example, if you defined styles in my-app component, the styles will be applied to <my-app> DOM element. This particular configuration:
:host(.mine){
color:red;
}
Will be applied to the host element that has .mine class:
<my-app class="active">
If you defined styles in app-ngrx component, the styles will be applied to <app-ngrx> DOM element, NOT <my-app>. This particular configuration:
:host(.mine){
color:red;
}
Will be applied to the host element that has .mine class:
<app-ngrx class="active">
:host-context
Now, :host-context is also applied to the host element, but the function (parenthesis) takes a selector that is checked not against the host element itself, but against all ancestors up to document root. If such element is found, the styles are applied.
For example, this selector
:host(.mine){
color:red;
}
matches such structure:
<my-app class="mine">
whereas, this selector:
:host-context(.mine){
color:red;
}
matches this structure:
<div class="mine">
...
<my-app>
This is useful, if you want to apply styles to components view (shadow root) conditionally. This makes h2 always bold:
h2 {
font-weight: bold;
}
whereas this
:host-context(.make-inner-components-bold) h2 {
font-weight: bold;
}
makes them bold only if your component is inside an element with class .make-inner-components-bold.
:host { ... } selects the component itself
:host(.mine) { ... } selects the component itself when it has class="mine" set
:host-context(.mine) { ... } selects the component itself when one of its ancestors has class="mine" set
See also https://angular.io/docs/ts/latest/guide/component-styles.html
#Component({
selector: 'h3',
styles: [':host(.mine) { color: red; }],
template: '<ng-content></ng-content>'})
class MyH3Component{}
<h3 class="mine">this is red</h3>
<h3>this is black</h3>
or with :host-context
#Component({
selector: 'h3',
styles: [':host-context(.mine) { color: red; }],
template: '<ng-content></ng-content>'})
class MyH3Component{}
<body class="mine">
<my-app><my-app>
<body>
AppComponent
template: '<h3>this is red</h3>'
or with class="mine" set
<body>
<my-app><my-app>
<body>
AppComponent
template: '<h3>this is black</h3>'
update
If you want to style the content of a child component (instead of the child component itself) you can use /deep/
:host child /deep/ h3 {
color: red;
}
update 2
::slotted is now supported by all new browsers and can be used with `ViewEncapsulation.ShadowDom
https://developer.mozilla.org/en-US/docs/Web/CSS/::slotted