Shadow DOM styling from the outside - javascript

I am searching a way to styling shadow DOM from the outside. For example, I would like to set the color of all text in all 'span.special' elements as RED. Including 'span.special' elements from shadow DOM. How I can do this?
Previously there were ::shadow pseudo-element and /deep/ combinator aka >>> for this purpose. So I could write something like
span.special, *::shadow span.special {
color: red
}
But now ::shadow, /deep/ and >>> are deprecated. So, what do we have as a replacement of them?

I did try many methods, including those described here. Since I'm using an external Web Component lib, I don't have access to modify these components. So, the only solution that worked for me was using JS querySelector, like this:
document.querySelector("the-element.with-shadow-dom")
.shadowRoot.querySelector(".some-selector").setAttribute("style", "color: black");
Not the best solution, not suitable for large stylings, but does work for little enchancements.
#John this was tested with Chrome 83.0.4103.116 (still going to test in Safari) and I did for Ionic (v5) ion-toast component. Here is the (almost) real code I used:
import { toastController } from '#ionic/core';
let toastOpts = {
message: "Some message goes here.",
cssClass: "toast-with-vertical-buttons",
buttons: [
{
text: "Button 1",
side: 'end'
},
{
text: "Button2",
side: 'end'
},
{
icon: "close",
side: "start"
}
]
}
toastController.create(toastOpts).then(async p => {
let toast = await p.present(); // this renders ion-toast component and returns HTMLIonToastElement
toast.shadowRoot.querySelector('div.toast-button-group-end').setAttribute("style", "flex-direction: column");
});

There is still no easy way to pierce through the shadow root, but here are 3 ways you can go about it. Just keep in mind that you will need to make changes inside the web component.
Using variables v1 - You will need to pass the property and consume the variable inside the web component.
Using variables v2 - You will need to consume the variable inside the web component.
Using ::part() - You will need to add a part attribute to the element you want to style in the web component. (Note: this pseudo element is well supported but is still in experimental mode, so make sure you're aware of that before using it in production).
Run code sample below for details.
const elA = document.querySelector('custom-container-a');
const shadowRootA = elA.attachShadow({mode:'open'});
shadowRootA.innerHTML = '<style>:host([border]) {display:block;border: var(--custom-border);}</style>'+
'<p>Shadow content A</p>'
const elB = document.querySelector('custom-container-b');
const shadowRootB = elB.attachShadow({mode:'open'});
shadowRootB.innerHTML = '<style>p {display:block;color: var(--custom-color, blue);}</style>'+
'<p>Shadow content B</p>'
const elC = document.querySelector('custom-container-c');
const shadowRootC = elC.attachShadow({mode:'open'});
shadowRootC.innerHTML = '<p part="paragraph">Shadow content C</p>'
/* Normal way of styling */
p {
color: orange;
}
/* Using variables version 1 */
custom-container-a {
--custom-border: 3px solid gold;
}
/* Using variables version 2 */
custom-container-b {
--custom-color: green;
}
/* Using ::part() */
custom-container-c::part(paragraph) {
color: magenta;
}
<p>Light content</p>
<custom-container-a border></custom-container-a>
<custom-container-b></custom-container-b>
<custom-container-c></custom-container-c>

You could use #import css as explained in this answer to another question on SO.
Include the rule inside the style element in the shadow tree.
<style>
#import url( '/css/external-styles.css' )
</style>
Note that the >>> combinator is still part of the CSS Scoping Module Draft.

Well, #import is not a solution if you are working with library web component that you can't change ...
Finally I found several ways to do it:
1) Cascading. Styles of Shadow DOM's host element affect Shadow DOM elements also. Not an option if you need to style a particular element of the Shadow DOM, not every.
2) Custom properties https://www.polymer-project.org/1.0/docs/devguide/styling
If an author of the web component provided such.
3) In Polymer, the have Custom Mixins also https://www.polymer-project.org/1.0/docs/devguide/styling
4) #import, but only for not-library components
So, there are several possibilities, but all of them are limited. No powerful enough way to outside styling as ::shadow were.

Related

Serialized and deserialize HTML style tag with CSS

What I'm trying to do:
Save/copy HTML snippet in one place, paste it in another place and have it de-serealized into an analogous DOM
How do I do serialization now
Call element.outerHTML on a container element
I've also tried using new XMLSerializer().serializeToString(element) with the same result
The issue
When I serialize style nodes that contain the css like:
.a > .b {
color: red
}
They actually get serialized like
.a > .b {
color: red
}
Which is not a valid CSS and so does not get parsed properly.
The problem with greater sign is the only one I observe, but it makes me wonder about other potential serialization issues.
Question: How do I get serialize the style nodes in a way that does not break CSS in them?
So it seems that this has to do with creating the style node dynamically on a new document.
The easiest reproduction is as follows:
const doc = new Document()
const style = doc.createElement('style')
style.textContent = '.a>.b { color: red; }'
console.log(style.outerHTML)
Which yields <style>.a>.b {color:red}</style>
It seems that being part of actively rendered document/or just generally being added to a document and not just created using it has some side-effect on the style node, which causes it to be serialized properly though. So now I do the following instead as a workaround, which is a bit ugly but works:
const doc = new DOMParser().parseFromString('<html><head></head><body></body></html>', 'text/html')
const style = doc.createElement('style')
style.textContent = '.a>.b { color: red; }'
doc.body.append(style)
console.log(style.outerHTML)
doc.body.removeChild(style)
Which produces an appropriately serialized <style>.a>.b { color: red; }</style>
Now this solves my problem, but I'd still really like to understand what is happening here in more detail (e.g. what is the side-effect exactly and how/when is it triggered), so would appreciate comments on that!

css darkmode variables being ignored by storybook

I have the following css which is loaded into my project:
// Default theme (light mode)
:root {
/* Typography */
--col-body-text: #0b0c0c;
--col-body-text-light: #505a5f;
}
// Dark mode theme
:root.dark {
/* Typography */
--col-body-text: #c5c5c5;
--col-body-text-light: #f8f8f8;
}
In my actual app this works as expected, however, in storybook, it ignores the dark mode variables.
I have updated my preview.js file to add '.dark' to the `HTML element when dark mode is selected - which works as expected - indeed all of the other dark mode specific code in the components works fine. It's only those variables that are being ignored.
Is there an issue with using :root in storybook that I'm not aware of or something?
if it helps, here is the code that adds the class to the HTML element:
// get an instance to the communication channel for the manager and preview
const channel = addons.getChannel()
// switch body class for story along with interface theme
channel.on('DARK_MODE', isDark => {
if (isDark) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
})
If you place such stylesheet in the HTML page (e.g. in the <head>), the :root selector refers to <html> (https://developer.mozilla.org/en-US/docs/Web/CSS/:root). If you want to use the :root selector with a class, you need to set the class on the <html> element (rather than on <body> suggested in another answer), so document.documentElement.classList.add('dark') is correct.
There is a playground which uses the Storybook syntax and features where I created a working example for you: https://webcomponents.dev/edit/p8TI3583HotsFNWjBMd8/src/index.stories.js
Not sure if your Storybook is configured in another manner, maybe your stylesheet is not really added or gets overwritten somewhere later. Please also verify if you use the CSS Custom Properties (aka CSS vars) correctly, I hope the working demo helps there too.
try
CSS -> body.dark {
instead of :root.dark {
and
channel.on('DARK_MODE', isDark => document.body.classList.toggle('dark', isDark))

Getting a field from a named CSS style in Javascript

I have an HTML document with a link tag in its head to a particular CSS stylesheet:
<link rel="stylesheet" href="style.css" type="text/css">
This .css file contains a particular class, like so:
.mystyle {
color: #00c;
}
What I'm trying to do is to grab that class's color field, so that I can use it dynamically in another part of the page (for another element's background-color). Is there any way in a JavaScript program to access that information, by the name of the class? Something like this:
var myColor = document.getStyle(".mystyle").color;
Some caveats:
There may or may not be other stylesheets that are also linked from this HTML document.
There may or may not be any particular elements on the page that are styled with this particular class.
I've already tried setting a temporary element to have the given class, and then grabbing its color field. That didn't work: the color field contains the empty string.
Thanks.
You can get all stylesheet information using the StyleSheetList and related objects.
In the example below, I aggregate all the document's styles (i.e., inline styles, an external bootstrap stylesheet and the stylesheet provided by Stackoverflow), and retrieve the color information for the .mystyle class:
const sheets = [...document.styleSheets];
const rules = sheets.reduce((a, v) => [...a, ...v.cssRules || []], []);
const rule = rules.find(r => r.selectorText === '.mystyle');
console.log(rule.style.color);
.mystyle {
color: #00c;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"/>
It's possible to use JavaScript to read the actual CSS files themselves by scraping the DOM and extracting the relevant information. While possible, it's clunky, and I'd advise against that unless absolutely necessary. If it's required, this answer covers it pretty well.
As an alternative to scraping the header information, you could use HTMLElement.style and grab the color value, though note that this will only work for inline styles:
var span1 = document.getElementsByTagName('span')[0];
var span2 = document.getElementsByTagName('span')[1];
// Empty
console.log(span1.style.color);
// Blue
console.log(span2.style.color);
.mystyle {
color: #00c;
}
<span class="mystyle">Text</span>
<span style="color: #00c;">Text</span>
However, a much better solution would be making use of what are known as CSS variables. These are defined in :root with a double hyphen prefix, and can be referenced with var(). This allows you to only set a colour once, and re-use it for both a color property and a background-color property, as can be seen in the following:
:root {
--colour: #00c;
}
.a {
color: var(--colour);
}
.b {
background-color: var(--colour);
}
<span class="a">Text</span>
<span class="b">Text</span>
Hope this helps! :)
Try window.getComputedStyle in combination with getPropertyValue.
var elem = document.getElementsByClassName("mystyle");
var theCSSprop = window.getComputedStyle(elem,null).getPropertyValue("color");
More: https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
For any who might come after me:
One can indeed use window.getComputedStyle(element) on an element. However, creating your own element first (if one doesn't exist) comes with an important caveat. Firefox will properly calculate the computed style. However, Chrome (and possibly Safari too) won't calculate the style of an orphaned element that isn't part of the DOM tree. So if you go that route, be sure to add it to the tree somewhere, possibly as a hidden element.

Create CSS class with kotlinx.html DSL

I'm using Kotlin to Javascript plugin and kotlinx.html library to build sample app:
fun main(args: Array<String>) {
window.onload = {
document.body!!.append.div {
a("#", classes = "red") {
+"Link"
}
}
}
}
And I want to paint a link with "red" CSS class to red color.Now I'm using unsage + raw to do it:
document.head!!.append.style {
unsafe {
raw(".red { background: #f00; }")
}
}
How to create CSS class with kotlinx.html DSL? I didn't find any docs related to css DSL.
You cannot use the HTML DSL for creating CSS. There are two possible ways for using css in your HTML.
1) You create CSS files independently and then use the classes as you proposed.
2) Inline the CSS if this is feasible for your app.
h1("h1Class") {
style = "background-color:red"
+"My header1"
}
This results in:
<h1 class="h1Class" style="background-color:red">My header1</h1>
kotinx-html is a DSL for HTML only. So CSS needs to be built separately. What you need is kotlinx.css but it was quite unpopular so it was discontinued. For sure there are few community libraries targeted to that purpose but not sure if they are still alive.

Proper way to author Jquery plugins with style rules

I have been long battling this, and I would like to know if any others have feedback. I am about to make a customized library for building web apps quickly, and I want to make sure I use the right approach. I WANT to use this method:
$.fn.someSlider = function(){
var coreStyle = '.slider ul { white-space: nowrap; } .slider ul li {display: inline-block}', coreStyleTemplate = '<style><\/style>';
}
But I feel like hard coding the base CSS into the widget is always frowned upon - instead I see SO users recommending the use of CSS style rules instead of this option. I really really really want that 'it just works' feel, and having to force my users to use a separate style sheet just to get my plugins working... well is annoying!
Just to clarify: I would like to include all base style rules needed for the widgets proper/base functionality to be included inside the script. The user would easily modify the base look of the widget by writing a style rule in their own style sheet.
Example:
Instead of having to look through all the base styles trying to find the font color like this... .slider {display: inline-block; color: #000; someotherconfusingrule : blahblah; }
The user simply starts a new rule with the classes name/selector being used - and then just write the changes to make to the default script styles
They would just write
.slider {color: #000};
Thanks for the help in advance SO!
Nice question! Although I'm not sure what the preferred solution to this would be, I was thinking of the following approach:
Use a IIFE to define your jQuery plugin and enable you to define some private, global variables and functions.
$.fn.pluginName = (function() {
return function() {
...your regular plugins code...
};
}();
Define your plugins CSS as a list of style rules in your plugins code
var rules = [
'.box {' +
' width: 100px;' +
' background-color: #f99;' +
' margin: 10px;' +
' padding: 10px;' +
' font-family: Helvetica, Arial;' +
' text-align: center;' +
'}'
];
Create a private variable that remembers if your stylesheet has already been added to the document
var styleSheetExists = false;
Create a private function that creates a stylesheet using the style rules above and that adds it as the first <style> element in the <head> allowing the user to override styles in their own CSS. See http://davidwalsh.name/add-rules-stylesheets for a good tutorial on how to do this properly
var createStyleSheet = function() {
var style = document.createElement("style");
style.appendChild(document.createTextNode(""));
$('head').prepend(style);
for (var i = 0; i < rules.length; i++) {
style.sheet.insertRule(rules[i], i);
}
};
The first time your plugin is applied to an element check if the stylesheet has already been created and if not create the stylesheet.
var $elements = $(this);
if (!styleSheetExists) {
createStyleSheet();
styleSheetExists = true;
}
$elements.each(function() {
$(this).addClass('box');
});
return $elements;
See http://codepen.io/ckuijjer/pen/FkgsJ for this example. It creates a jQuery plugin called box which simply adds the class box to an element. The class box has a default pink background color defined in its stylesheet which gets overridden by a user defined blue background color.
But please do make this configurable in your jQuery plugin. You want to enable developers to bundle all their css, including your plugins, to optimize resource delivery to the client. Plus injecting stylesheets might be a small performance hit.
It may seem annoying but separating the model, view, and controller is the correct way. You're using jQuery so why not consider how jQuery would approach the situation: a jQuery UI widget like the Accordion comes with several stylesheets, the most important being the base stylesheet and a separate 'theme' stylesheet that (if done correctly) is nondestructive and can be modified without risking the integrity of the widget. You may also want to consider how your favorite plugins are authored and what makes them appeal to you. It's my personal opinion CSS should never be present in JavaScript files however if you've made up your mind, the solution #ckuijjer provided is sound. Hope this helps!

Categories