Detect dom element style change using Mutation Observer - javascript

Using MutationObserver I would like to detect a dom element change due to media query in a component.
But the MutationObserver fails to trigger an event when the style changes.
detectDivChanges() {
const div = document.querySelector('.mydiv');
const config = { attributes: true, childList: true, subtree: true };
const observer = new MutationObserver((mutation) => {
console.log("div style changed");
})
observer.observe(div, config);
}
}
<div class="mydiv">test</div>
.mydiv {
height: 40px;
width: 50px;
background-color: red;
}
#media screen and (min-width : 500px) {
.mydiv {
background-color: blue;
}
}
Here is a live version of the code

Mutation Observer can observe changes being made to the DOM tree.
When your CSS MediaQuery changes, the DOM tree is not affected whatsoever, so the MutationObserver won't catch it.
Your confusion comes from the fact that HTMLElements do have a style attribute. This attibute is indeed part of the DOM tree. But this style attribute is not the style that is applied on the element. This attribute does declare a StyleSheet that the CSSOM will parse and use if needed, but the CSSOM and the DOM are two separate things.
So what you want to detect is a CSSOM change not a DOM one (the style attribute doesn't change when you resize your screen), and this, a MutationObserver can't do it.
However, since you are willing to listen for a CSS MediaQuery change, then you can use the MediaQueryList interface and its onchange event handler:
const mediaQuery = window.matchMedia('screen and (min-width : 500px)');
mediaQuery.onchange = e => {
console.log('mediaQuery changed', 'matches:', mediaQuery.matches);
}
.mydiv {
height: 40px;
width: 50px;
background-color: red;
}
#media screen and (min-width : 500px) {
.mydiv {
background-color: blue;
}
}
<div class="mydiv">test</div>

Related

Javascript detect when computed css property changes

Given some text:
<div>text</div>
I would like to detect when the computed CSS property color changes for this div.
There could be a number of css queries that would change its color, like media queries:
div {
color: black;
#media (prefers-color-scheme: dark) {
color: white;
}
#media screen and (max-width: 992px) {
background-color: blue;
}
}
Or perhaps a class applied to a parent:
div {
}
body.black {
color: white;
}
How can I, using Javascript, observe this change of computed style?
I think we can get part way there at least by using mutation observer on the whole html, that will detect things like change to the attributes of anything, which may or may not influence the color on our to be 'observed' element, and by listening for a resize event which will at least catch common media query changes. This snippet just alerts the color but of course in practice you'll want to remember the previous color (or whatever you are interested in) and check to see if it is different rather than alerting it.
const observed = document.querySelector('.observed');
const html = document.querySelector("html");
let style = window.getComputedStyle(observed);
// NOTE: from MDN: The returned style is a live CSSStyleDeclaration object, which updates automatically when the element's styles are changed.
const observer = new MutationObserver(function(mutations) {
alert('a mutation observed');
mutations.forEach(function(mutation) {
alert(style.color);
});
});
function look() {
alert(style.color);
}
observer.observe(html, {
attributes: true,
subtree: true,
childList: true
});
window.onresize = look;
.observed {
width: 50vmin;
height: 50vmin;
color: red;
}
#media (min-width: 1024px) {
.observed {
color: blue;
}
}
#media (max-width: 700px) {
.observed {
color: gold;
}
<div class="observed">See this color changing.<br> Either click a button or resize the viewport.</div>
<button onclick="observed.style.color = 'purple';">Click for purple</button>
<button onclick="observed.style.color = 'magenta';">Click for magenta</button>
<button onclick="observed.style.color = 'cyan';">Click for cyan</button>
What I don't know is how many other things might influence the setting - I see no way of finding out when the thing is print rather than screen for example. Hopefully someone will be able to fill in any gaps.
I wrote a small program that detects change in a certain CSS property, in getComputedStyle.
Note: Using too many observeChange() may cause performance issues.
let div = document.querySelector("div")
function observeChange(elem, prop, callback) {
var styles = convertObject(getComputedStyle(elem));
setInterval(() => {
let newStyle = convertObject(getComputedStyle(elem));
if (styles[prop] !== newStyle[prop]) { //Check if styles are different or not
callback(prop, styles[prop], newStyle[prop]);
styles = newStyle; //Set new styles to previous object
}
},
500); //You can change the delay
}
//Callback function
function callback(prop, old, newVal) {
console.log(prop + " changed from " + old + " to " + newVal);
}
observeChange(div, "color", callback)
//Convert CSS2Properties object to a normal object
function convertObject(obj) {
let object = {}
for (let i of obj) {
object[i] = obj[i]
}
return object
}
div {
color: green;
}
input:checked+div {
color: red;
}
<input type="checkbox">
<div>Hello World</div>

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>

Add class to parent when a descendant class is present

I need to dynamically add styling to an element based on when a descendant has a specific class. I understand that this can only be done with javascript which isn't really my department.
Although I've been looking around for some copy paste solutions I now resort to creating this thread as I feel many answers listed here may be outdated and focused on compatibility.
I am no expert on the subject but i read that this can be done quite easily for modern browsers without using jquery and I only need it to work on modern browsers.
<ul class="the-parent">
<li class="the-descendant"></li>
</ul>
What happens is that a Wordpress plugin is adding/removing class "opened" to "the-descendant" on interaction with the menu but does not provide me a way to style the parent based on this interaction.
For what I read from your question, you'd need to set up a MutationObserver on the child node, then watch for attribute changes on the class attribute:
// Get a reference to the Node you need to observe classList changes on
const targetNode = document.querySelector('.child');
// Set up the configuration for the MutationObserver
// https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit
const config = {
attributes: true,
attributeFilter: ['class'],
};
// Callback function to execute when mutations are observed
const callback = (mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
mutation
.target
.closest('.parent')
.classList
.toggle('orange');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
// observer.disconnect();
.parent {
background-color: grey;
color: white;
padding: 30px;
}
.parent.orange {
background-color: orange;
}
.child {
background-color: white;
color: black;
padding: 20px;
}
.parent::after,
.child::after {
content: '"'attr(class)'"';
}
<div class="parent">
<div class="child" onclick="this.classList.toggle('clicked');">child css class: </div>
parent css class:
</div>
Remember it's important to disconnect the observer when you no longer need it, otherwise it stays active even if the observed node is removed from the DOM.

How would you call specific CSS stylesheet in a React component on a specific site?

So I'm working on a platform built on React and that platform already has global CSS that is already set up for all clients a certain way.
A subset of affiliates under a common group name want a new sticky ad component that has to change the CSS a bit so the footer does not get covered up.
So normally, I'd check what our global variable value is, window.client.group, and if it's the certain group, add css or css stylesheet through javascript in an affiliate js file (our old generic platform).
The new CSS needed is:
#media (max-width:767px){
#ad201 {width:100%;position:fixed;bottom:0;left:0;z-
index:99999;margin:0;border:1px solid rgb(232, 232, 232);background-
color:#fff;margin: 0 !important;}
.Footer .container {padding-bottom:50px;}
}
In React though, what's the best and most proper way to do this?
As you can see it's complicated by needing to be in with a media query.
I have a start using group and matchMedia, but what's the best way to bring in the CSS? a whole stylesheet? (stickyAd.css? some other way? and tips on how to do it?)
const group = (window.client.group).toLowerCase();
console.log(group);
const mq = window.matchMedia("(max-width: 767px)");
if ((mq.matches) && (group === 'premiere')) {
// bring in the new css
} else {
// do nothing
}
Thanks for the advice !
It depends on how much control you have over the ad201 and Footer.
I'm assuming Footer is a component you've created in React. In that case, you could add a class like premiere-footer-fix (you can probably think of a better name) to components being rendered when you detect your group:
render() {
const group = (window.client.group).toLowerCase();
return (
<Footer className={group === 'premiere' ? 'premiere-footer-fix' : ''}/>
)
}
or if you import the very handy classnames package,
import classNames from 'classnames';
// ...
render() {
const group = (window.client.group).toLowerCase();
const classes = classNames({
'premiere-footer-fix': group === 'premiere'
});
return (
<Footer className={classes}/>
)
}
Then wherever you declare CSS for the Footer, you just add something like:
#media (max-width:767px) {
.Footer.premiere-footer-fix .container {
padding-bottom: 50px;
}
}
As for your ad, you'd have to provide more info about how it's being inserted into the page since it's not clear how much control you have over the element. But I would add premiere to your ad's classList and find a place to insert this bit of CSS:
#media (max-width:767px) {
#ad201.premiere {
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 99999;
margin: 0;
border: 1px solid rgb(232, 232, 232);
background-color: #fff;
margin: 0 !important;
}
}

CSS style API for Polymer

I came up with the following to modify the style of a webcomponent through:
custom properties in CSS
declaratively in the HTML tag
programatically by changing the properties
My solution uses properties that automatically modify the CSS custom properties.
It looks as follows:
<link rel="import" href="../../bower_components/polymer/polymer.html"/>
<dom-module id="my-bar">
<template>
<style>
:host {
display: block;
box-sizing: border-box;
height: var(--my-bar-bar-height, 50%);
opacity: var(--my-bar-bar-opacity, 0.8);
border: var(--my-bar-bar-border, 1px solid black);
width: 100%;
}
div {
background-color: var(--my-bar-bar-color, blue);
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
</style>
<div id="bar"></div>
</template>
<script>
Polymer({
is: 'my-bar',
properties: {
barColor: {
type: String,
observer: '_colorChanged'
},
barHeight: {
type: String,
observer: '_heightChanged'
},
barOpacity: {
type: String,
observer: '_opacityChanged'
},
barBorder: {
type: String,
observer: '_borderChanged'
}
},
_heightChanged: function () {
this._styleChanged("barHeight");
},
_colorChanged: function () {
this._styleChanged("barColor");
},
_opacityChanged: function () {
this._styleChanged("barOpacity");
},
_borderChanged: function () {
this._styleChanged("barBorder");
},
_styleChanged: function(name) {
// update the style dynamically, will be something like:
// this.customStyle['--my-bar-bar-color'] = 'red';
this.customStyle[this._getCSSPropertyName(name)] = this[name];
this.updateStyles();
},
_getCSSPropertyName: function(name) {
// retrieves the CSS custom property from the Polymer property name
var ret = "--" + this.is + "-";
var char = "";
for(i = 0; i < name.length; i++)
{
char = name.charAt(i);
if(char >= 'A' && char <= 'Z') {
ret += "-" + char.toLowerCase();
}
else {
ret += char;
}
}
return ret;
}
});
</script>
</dom-module>
Then you can either style in CSS:
my-bar {
--my-bar-bar-color: gray;
}
through HTML:
<my-bar bar-height="20%" bar-opacity="0.1" bar-border="2px solid black"></my-bar>
or JavaScript:
this.$.my-bar.barHeight = "20%;
Adding a new CSS property to the API means adding the following lines:
the property definition
the observer code to pass the property name to _styleChanged()
setting the CSS property to the CSS custom property
I don't think that in Polymer you can specify a variable or constant with the function passed to the observer, so that's why the second point is necessary.
Is there any better way to create a CSS style API for Polymer?
Any improvements or simplifications I could do?
I would recommend against doing this; it may not be compatible with future versions. Hopefully it won't be, since Polymer in many ways is a polyfill until browsers adopt/implement web components on their own.
The purpose of the custom property API is to 'approximate' the CSS variable spec. This is needed since Polymer uses a shady dom, not the real shadow dom which is not widely supported yet.
I recommend sticking to the CSS variable spec for styling.

Categories