Javascript detect when computed css property changes - javascript

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>

Related

Vuejs - Use variable for background-image in style template

I have got a question regarding using a variable within the background-image of the style template. Is this possible at all?
I would like to achieve something like this:
<script>
export default {
name: 'setCss',
computed: {
cssVars() {
var ua = navigator.userAgent.toLowerCase();
var isAndroid = ua.indexOf("android") > -1; //&& ua.indexOf("mobile");
var path = '';
if(isAndroid) {
path = 'img/samsung/';
} else{
path = 'img/iphone/';
}
return {
'--bg-path': path,
}
}
}
};
</script>
and then with this CSS:
<style scoped>
div {
background-image: var(--bg-path) + '/1366x400#2x.jpg';
background-size: 1366px 400px;
}
#media not all,
not all,
only screen and (max-width: 480px) {
div {
background-image: var(--bg-path) + '/480x288.jpg';
background-size: 480px 288px;
}
}
</style>
This will not work but I am wondering if there is a way to do this?
For this to work, you will simply need to pass the computed property cssVars to the component as dynamic style attribute like:
<button :style="cssVars">My button</button>
Here is a working demo:
(here I am passing a hardcoded value for bgColor, but you can assume that being passed as a prop to a component also)
new Vue({
el: "#myApp",
data: {
bgColor: "green"
},
computed: {
cssVars() {
return {
'--bg-color': this.bgColor,
}
}
}
})
button {
background-color: var(--bg-color);
padding:10px 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<button :style="cssVars">My button</button>
</div>
You will need to use class/style binding.
In your case, binding style would look something like the following. First, you would need to bind a style with the computed background image (the rest of the styles don't need to be bound, they can remain in your style tag).
<template>
<div :style="{backgroundImage: computedBgImage}"></div>
</template>
...
computed:{
computedBgImage() {
let path;
if (isAndroid) {
path = 'img/samsung/';
} else{
path = 'img/iphone/';
}
return path + '/480x288.jpg'
}
}
...
If you need to use the same image in multiple places, consider defining a class for each image, and binding the class, instead of the style, as explained in the above link.

Detect dom element style change using Mutation Observer

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>

It's it possible to get React to move an element rather than re-create it when it changes its place in the DOM?

It's it possible to get React to move an element rather than re-create it when it changes its place in the DOM?
Let's imagine I'm making a 2 pane component and I want to be able to hide/unhide one pane. Let's also imagine the panes themselves are very heavy. In my case the each pane has over 2000 elements.
In my actual code I'm using a splitter when there are 2 panes. In order to show just one pane I need to remove the splitter and replace it with a div.
The code below simulates this. If there's one pane it uses a div to contain the pane. If there's 2 panes it uses pre to contain them. In my case it would be div with 1 pain and a splitter with 2.
So, instrumenting document.createElement I see that not only are the containers created but the elements inside are recreated. In other words, in my code when go from splitter->div the 2000+ element pane will get entirely recreated which is slow.
Is there a way to tell React effectively. "Hey, don't recreate this component, just move it?"
class TwoPanes extends React.Component {
constructor(props) {
super(props);
}
render() {
const panes = this.renderPanes();
if (panes.length === 2) {
return React.createElement('pre', {className: "panes"}, panes);
} else {
return React.createElement('div', {className: "panes"}, panes);
}
}
renderPanes() {
return this.props.panes.map(pane => {
return React.createElement('div', {className: "pane"}, pane);
});
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
panes: [
"pane one",
"pane two",
],
};
}
render() {
const panes = React.createElement(TwoPanes, {panes: this.state.panes}, null);
const button = React.createElement('button', {
onClick: () => {
const panes = this.state.panes.slice();
if (panes.length === 1) {
panes.splice(0, 0, "pane one"); // insert pane 1
} else {
panes.splice(0, 1); // remove pane 1
}
this.setState({panes: panes});
},
}, "toggle pane one");
return React.createElement('div', {className: "outer"}, [panes, button]);
}
}
// wrap document.createElement so we can see if new elements are created
// vs reused
document.createElement = (function(oldFn) {
let count = 0;
let inside = false;
return function(type, ...args) {
if (!inside) { // needed because SO's console wrapper calls createElement
inside = true;
console.log(++count, "created:", type);
inside = false;
}
return oldFn.call(this, type, ...args);
}
}(document.createElement));
ReactDOM.render(
React.createElement(App, {}, null),
document.getElementById('root')
);
html { box-sizing: border-box; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; }
#root { width: 100vw; height: 100vh; }
.outer {
width: 100%;
height: 100%;
}
.panes {
width: 100%;
height: 100%;
display: flex;
flow-direction: row;
justify-content: space-between;
}
.pane {
flex: 1 1 auto;
border: 1px solid black;
}
button {
position: absolute;
left: 10px;
top: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I don't think there is a way to just move around DOM trees, even if there was - it will be pretty expensive, as
React's diff algorithm still needs to compare different trees and if it stumbles upon a subtree / node with different structure, it will immediately discard the old one. This is one of the assumptions that the diff algorithm makes in order to run in O(n)
Two elements of different types will produce different trees
In order to move around a cached DOM, you will first need detach it from the tree, which implies that it still needs to be reapplied later and that is a bottleneck. Inserting HTML into the DOM is very expensive, even if cached / prerendered.
My suggestion is to use CSS, because display: none / display: block is a much faster than reapplying cached DOM.
class TwoPanes extends React.Component {
render() {
return (
<div>
<Pane1 />
<Pane2 style={this.state.panes.length === 2 ? {} : {display: 'none'} } />
</div>
);
}
}

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.

Styling Polymer Element Dynamically

I am trying to make an paper-card element change colors based on the status of the customers data on Fire base, but for some reason the color only updates on the second click of the customer. Right now I have the paper cards ID set to the firebase data in order to make it change colors. Here's my elements style code:
<style is="custom-style">
:host {
display: block;
}
#cards {
#apply(--layout-vertical);
#apply(--center-justified);
}
.row {
padding: 20px;
margin-left: 10px;
}
paper-card {
padding: 20px;
}
#check {
float: right;
bottom: 15px;
--paper-card
}
#Done {
--paper-card-header: {
background: var(--paper-green-500);
};
--paper-card-content: {
background: var(--paper-green-300);
};
}
#Default {
/*Apply Default Style*/
/*--paper-card-content: {*/
/* background: var(--paper-red-500);*/
/*};*/
}
paper-icon-button.check{
color: var(--paper-green-500);
}
paper-icon-button.check:hover{
background: var(--paper-green-50);
border-radius: 50%;
}
#check::shadow #ripple {
color: green;
opacity: 100%;
}
.iron-selected{
color: green;
}
And here is the template:
<template>
<firebase-collection
location="https://calllistmanager.firebaseio.com/Wilson"
data="{{wilsonData}}"></firebase-collection>
<div id="cards">
<template id="cards" is="dom-repeat" items="{{wilsonData}}" as="customer">
<paper-card id="{{customer.status}}" class="{{customer.status}}" heading="[[customer.__firebaseKey__]]">
<div class="card-content">
<span>Phone: </span><span>[[customer.number]]</span>
<span>Status: </span><span>[[customer.status]]</span>
<paper-icon-button style="color: green" id="check" on-tap="checktap" icon="check">
</paper-icon-button>
</div>
</paper-card>
</template>
</div>
Here is my script:
<script>
(function() {
Polymer({
is: 'list-display',
properties: {
wilsonData: {
type: Object,
observer: '_dataObserver'
}
},
ready: function() {
var listRef = new Firebase("https://calllistmanager.firebaseio.com/Wilson");
},
checktap: function(e){
// e.model.customer.status = "Done";
console.log("Starting Status: " + e.model.customer.status);
ref = new Firebase("https://calllistmanager.firebaseio.com/Wilson")
var stat;
var store = ref.child(e.model.customer.__firebaseKey__);
store.on("value", function(snapshot){
stat = snapshot.child("status").val();
});
if(stat == "Done"){
store.update({
"status": "Default"
});
e.model.customer.status = "Default";
}
else {
store.update({
"status": "Done"
});
e.model.customer.status = "Done";
}
console.log("Ending Status: " + e.model.customer.status);
this.updateStyles()
}
});
})();
at first I thought the problem may be that the function runs updateStyles(); faster than firebase can update but it always works fine on the second click...any suggestions?
I think the problem could be caused by the call to firebase. store.on("value", is not a synchronous function. However, later in your code you assume that you already have a value, that will be set later on whenever the value event fires. You could try adding the rest of your code in the event handler. Like this:
checktap: function(e){
// e.model.customer.status = "Done";
console.log("Starting Status: " + e.model.customer.status);
ref = new Firebase("https://calllistmanager.firebaseio.com/Wilson")
var store = ref.child(e.model.customer.__firebaseKey__);
store.once("value", function(snapshot){
var stat = snapshot.child("status").val();
if(stat == "Done"){
store.update({
"status": "Default"
});
e.model.set("customer.status", "Default");
}
else {
store.update({
"status": "Done"
});
e.model.set("customer.status", "Done");
}
console.log("Ending Status: " + e.model.customer.status);
this.updateStyles();
}.bind(this));
}
Essentially, you wait until the stat variable has been set to do the rest of your tasks. Also note, the bind(this) at the end, which will allow you to update the the styles from the event handler.
Update
There are a couple of more issues. First it's better to uses classes for changing the styles and not IDs. IDs should not change. Then, to bind to the class attribute, use the $ sign. When you update the model, you should use the set API.
Have a look at this plunker. It is a small working example (only works in Chrome) that changes styles when you click the checkmark. It does not use Firebase, however.
Here's how you could to the style with classes.
.Done {
--paper-card-header: {
background: var(--paper-green-500);
};
--paper-card-content: {
background: var(--paper-green-300);
};
}
And in your template:
<paper-card class$="{{customer.status}}" heading="[[customer.__firebaseKey__]]">

Categories