Consider the following web components Foo and Bar. How does the browser behave in both cases when I have very many instances of Foo and Bar respectively on a page. Which approach is more performant? I suspect that with Bar, the browser loads the CSS only once and then parses it only once. With Foo's approach, it doesn't have to load anything, but it does have to re-parse for each instance of Foo. Is that correct?
window.customElements.define('my-foo', class Foo extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
p {
color: red;
}
// more styles go here
</style>
<p>Foo</p>
`;
}
});
window.customElements.define('my-bar', class Bar extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="path/to/my/cdn/bar.css">
<p>Bar</p>
`;
}
});
Related
I understand that in order to style elements from the ShadowDOM, the shadowDOM itself has to "know"
the element, thus be declared inside of it.
It works well for regular web components but i haven't found an answer to it weather it's the same for an extended
native element.
For example, I wanted to know if the code I wrote is the best way, i.e. the external p acting as container, or am i creating a redundant p element.
JavaScript:
class P_example extends HTMLParagraphElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
p{
background-color: orangered;
width: max-content;
}
</style>
<p><slot>Some default</slot></p>
`;
this.shadowRoot.append();
}
connectedCallback() {
//add styling class to the p element
}
}
customElements.define("omer-p", P_example, { extends: "p" });
HTML:
<p is="omer-p">Some sample text</p>
I have a component that relies on external stylesheets. I'm bringing the stylesheet into the component like this:
Child component
export default class Child extends Component {
render() {
return (
<div>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" />
...my code here...
</div>
);
}
}
But what's happening is this is forcing those styles onto the parent component as well.
Parent Component
export default class Parent extends Component {
render() {
return (
<div>
...code here...
<Child />
... more code here...
</div>
);
}
}
Is anyone aware of a way that I can keep that stylesheet link localized to just that child component so the styles aren't applied to the parent component as well?
Edit 2
Currently trying the shadow dom route, trying to pass down some children. Getting an error after the initial render saying Cannot read properties of undefined (reading 'children'). It does render the this.props.children initially...
import React, { Component } from 'react';
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
${this.props.children}
`;
}
};
export default class Child extends Component {
render() {
return (
<div>
<script>
{!customElements.get("my-component") && customElements.define('my-component', MyComponent)}
</script>
<my-component>
<h1>Hello from shadow</h1>
</my-component>
<h1>Hello</h1>
</div>
);
}
}
You can try CSS Modules. Add :local(.className) to the class you want to use in your code which is in the font-awesome-min.css file. Then import the styles to your component. For example import styles from './font-awesome-min.css' then use the module in your code. The styles will only apply to specific element and won't affect other elements in the document. So let's say you have a class called .usericon in your css you do this in the css file.
CSS
:local(.usericon){
fill: red;
}
React Code
import styles from './font-awesome-min.css'
export default function Profile(){
return (
<i className={styles.usericon}>User Icon</i>
)
}
One way to truly isolate your CSS is with Web Components. Web Components are a browser API that allows defining custom elements with their own "shadow DOM". If a style is defined inside the shadow DOM, it is truly sandboxed with no styles going in or out. You can use whatever selectors you like:
class FancyBox extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.fancy-box {
border: solid 3px darkblue;
background: dodgerblue;
padding: 10px;
color: white;
font: 16px sans-serif;
}
</style>
<div class="fancy-box">
<slot></slot>
</div>
`;
}
}
customElements.define('fancy-box', FancyBox);
.fancy-box {
border: dashed 3px darkred !important;
background: crimson !important;
padding: 10px !important;
color: white !important;
font: 16px sans-serif;
}
<fancy-box>Safe in my shadow DOM</fancy-box>
<div class="fancy-box">I am affected by outside stylesheets</div>
Note the use of <slot></slot>. This is a placeholder for child elements of the component.
If I wanted to use this custom element from React, it needs to be defined separately so it only runs once.
class FancyBox extends HTMLElement { /*...*/ };
customElements.define('fancy-box', FancyBox);
class ReactFancyBox extends React.Component {
constructor() {
super();
this.state = { value: 'hello world!' }
}
handleChange(e) {
this.setState({ value: e.currentTarget.value });
}
render() {
return (
<div>
<fancy-box>
<strong>{this.state.value}</strong>
</fancy-box>
<input value={this.state.value} onChange={e => this.handleChange(e)} />
</div>
);
}
};
I have style sections being added in sub-classes.
Very simplified version:
class MyComponent extends HTMLElement{
constructor(){
super()
this.html = `
<style>
:host {
/* some css in here */
}
</style>
<style>
:host {
/* additional css in here */
}
</style>`
}
//...
}
The second style section seems to be getting ignored.
Should this work OK?
Update: It works if the style section isn't split
class MyComponent extends HTMLElement{
constructor(){
super()
this.html = `
<style>
:host {
/* some css in here */
/* additional css in here */
}
</style>`
}
//...
}
How can e.g. access and set the background-color of the ::after pseudo selector of the element below?
#ViewChild('infobubble', { read: ElementRef }) infoBubbleElement: ElementRef;
ngAfterViewInit(): void {
const elem = this.infoBubbleElement.nativeElement;
elem.style.backgroundColor = 'green';
}
I was presented with the same situation, it's not possible to access to ::before or ::after HTML element from elementRef; my solution was to create another DIV within the HTML element that I need to access, to modify it with a directive and change the background of the div that would be the new :: before
Example my directive:
#Directive({selector: '[appPrimaryColor]'})
export class PrimaryColorDirective implements OnInit {
#Input() backgroundColor = false;
constructor(
private elementRef: ElementRef,
private _renderer: Renderer2,
) { }
ngOnInit() {
// Set BackgroundColor
if (this.backgroundColor) {
this._renderer.setStyle(this.elementRef.nativeElement, 'background-color', COLOR);
}
}
}
And in HTML:
<div class="icon">
<!-- step-ok-effect is the new div-->
<div
appPrimaryColor
[backgroundColor]="true"
class="step-ok-effect"></div>
<span class="icon-{{ step.state ? 'check' : step.icon }}"></span>
</div>
</div>
I have a Native V1 component that is not using shadowDOM so I place my CSS in the <head>. But when someone else uses my component my CSS no longer works.
This only happens if their component does use shadowDOM.
Example Code for my component:
class MyEl extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<div class="spaced"><button class="happy-btn">I'm Happy</button></div>
<div class="spaced"><button class="sad-btn">I'm Sad</button></div>`;
}
}
// Define our web component
customElements.define('my-el', MyEl);
button {
padding: 8px 20px;
}
.happy-btn {
background-color: pink;
}
.sad-btn {
background-color: #007;
color: white;
}
<my-el></my-el>
My CSS is loaded into the <head> tag since I am not using shadowDOM. But once the outer element includes me in their shadowDOM then things fall apart.
If you are creating a component that does NOT use ShadowDOM that you may still need to add your CSS into a shadowRoot. If someone else places your component into their shadowDOM, then you must add your CSS to their shadowRoot. You can do this with the following code:
const myStyle = document.createElement('style');
myStyle.setAttribute('component', 'my-el');
myStyle.textContent = ` button {
padding: 8px 20px;
}
.happy-btn {
background-color: pink;
}
.sad-btn {
background-color: #007;
color: white;
}`;
function addCss(el, selector, styleEl) {
// Check to see if we have been placed into a shadow root.
// If we have then add our CSS into that shadow root.
let doc;
try {
doc = el.getRootNode();
if (doc === document) {
doc = document.head;
}
}
catch(_ex) { doc = document.head; } // Shadow DOM isn't supported.
if (!doc.querySelector(selector)) {
doc.appendChild(styleEl.cloneNode(true));
}
}
class MyEl extends HTMLElement {
constructor() {
super();
addCss(this, 'style[component="my-el"]', myStyle);
}
connectedCallback() {
this.innerHTML = `<div class="spaced"><button class="happy-btn">I'm Happy</button></div>
<div class="spaced"><button class="sad-btn">I'm Sad</button></div>`;
}
}
customElements.define('my-el', MyEl);
class TheirEl extends HTMLElement {
constructor() {
super();
this.attachShadow({mode:'open'});
this.shadowRoot.innerHTML = `<hr/><my-el></my-el><hr/><my-el></my-el><hr/>`;
}
}
customElements.define('their-el', TheirEl);
<their-el></their-el>
The function addCss will place your CSS into the correct shadowRoot, or into document.head if there is no shadowRoot.
You must call addCss within your constructor to place the CSS in the correct location. This routine will also make sure you don't add it twice as long as you have a unique selector to identify your <style> tag.
In mine you see the <style> tag adds an attribute called component with a value of the component name. In my case component="my-el".
Then I use the selector 'style[component="my-el"]' to see if that tag is already in the shadowRoot, or document.head if there is no shadowRoot, and only add the styles if it does not already exist.
You can not assume that your component will not be in shadow DOM just because you are not using it. Use the example above to protect yourself.
Side Note
If you are using shadow DOM then this problem goes away since your have to place your CSS into your own shadowRoot.