Animating light dom elements using neon-elements - javascript

So i've been playing around with the neon element examples. Specifically the load example.
I'm able to accomplish what I want but I feel there should be a more elegant way to do it. The way the example loads content is by creating an array inside of a config object and then uses a dom-repeat to stamp them out all from within animated-grid.html
This seems to force very tight coupling between the animations and the content that is being animating. I don't want to encapsulate content in side of my grid I basically want to say Hey I want an animated grid here are the cells/content to be put inside of it and then having the animated grid take care of laying out and animating those children
I set out to create an API that was more declarative like this.
<animated-grid id="grid">
<div style="background-color: #9C27B0"> <span>1</span> </div>
<div style="background-color: #4CAF50"> <span>2</span> </div>
<div style="background-color: #2196F3"> <span>3</span> </div>
<div style="background-color: #673AB7"> <span>4</span> </div>
<div style="background-color: #FF9800"> <span>5</span> </div>
<div style="background-color: #049688"> <span>6</span> </div>
</animated-grid>
I figured I could change the css selectors from .tile to :host::content div.
Then the template to
<template>
<content id='content'></content>
</template>
I then took a stab at rewritting the attached method to get the children in the lightdom instead of queryselecting for .tile
attached: function() {
this.async(function() {
var nodeList = Polymer.dom(this.$.content).getDistributedNodes();
// I only want to animate divs
nodelist = nodeList.filter(function(node){
return (node.nodeName === "DIV");
});
this.animationConfig['entry'][0].nodes = nodeList;
});
},
However, I keep getting a nasty error in my console.
The final code for animated-grid looked like this:
<!--
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../../../polymer/polymer.html">
<link rel="import" href="../../../paper-styles/paper-styles.html">
<link rel="import" href="../../neon-shared-element-animatable-behavior.html">
<dom-module id="animated-grid">
<link rel="import" type="css" href="../shared.css">
<style>
:host {
display: block;
}
:host::content div {
display: inline-block;
float: left;
color: white;
vertical-align: top;
width: calc(100% / 3);
height: calc(100% / 2);
#apply(--paper-font-title);
#apply(--layout-vertical);
#apply(--layout-center-center);
}
:host::content div:hover {
#apply(--shadow-elevation-2dp);
position:relative;
}
</style>
<template>
<content id='content'></content>
</template>
</dom-module>
<script>
Polymer({
is: 'animated-grid',
behaviors: [
Polymer.NeonSharedElementAnimatableBehavior
],
properties: {
animationConfig: {
type: Object,
value: function() {
return {
'entry': [{
name: 'cascaded-animation',
animation: 'transform-animation',
transformFrom: 'translateY(100%)',
transformTo: 'none',
timing: {
delay: 50
}
}]
}
}
}
},
attached: function() {
this.async(function() {
var nodeList = Polymer.dom(this.$.content).getDistributedNodes();
// I only want to animate divs
nodelist = nodeList.filter(function(node){
return (node.nodeName === "DIV");
});
this.animationConfig['entry'][0].nodes = nodeList;
});
},
_computeTileClass: function(color) {
return 'background-color: ' + color + '';
}
});
</script>

UPDATE: A good friend pointed out that while JavaScript is still insane, it's insane for different reasons than erroneously listed here. The error was that the variable being assigned into, nodelist, from the filter, did not have a capital "L", like the var nodeList does. In JS, this means nodelist is now in the global variable object, global.nodelist, which is why nodeList seemingly wasn't filtered or assigned correctly into. So, another solution is to simply capitalize nodelist to nodeList in the filter return, and it should be fine.
Everything you're doing is fine and good (the async in attached is the right way to setup your animationConfig), with the exception of one thing.
The error you're getting is because you're attempting to animate text nodes with the cascade-animation you setup.
You're attempting to animate text nodes because your variable nodeList contains all the original 13 light dom children, which includes the text nodes, instead of just divs.
The reason your filtered array wasn't filtered, is because JavaScript is insane, and assigning into the array doesn't work as expected, like in every other language.
A solution is to use a new variable for the filter results:
var divNodes = nodeList.filter(function(node){
return (node.nodeName === 'DIV');
});
this.animationConfig.entry[0].nodes = divNodes;

Related

Combine :host() with :has() - not possible?

I have a web component with a shadow DOM and a default slot.
I need to apply certain styling based on the presence or absence of specific a light DOM descendant. Please note that I don't need a specific workaround for this specific styling, it's just an example and in the real world the example is alot more complex.
I also cannot work with regular DOM CSS like x-y:has(div) since I need to apply styles to an element in the shadow DOM based on the presence of the div in the light DOM.
Please note that the code snippet only works in browsers that support constructable stylesheets (e.g. Safari won't).
const styleStr = `
:host {
display: block;
border: 3px dotted red;
}
:host(:has(div)) {
border-color: green;
}
`;
let css;
try {
css = new CSSStyleSheet;
css.replaceSync(styleStr);
} catch(e) { console.error(e) }
customElements.define('x-y', class extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).adoptedStyleSheets.push(css);
this.shadowRoot.append(document.createElement('slot'))
}
})
<x-y>no div - should have red border</x-y>
<x-y>
<div>div, should have green border</div>
</x-y>
I was trying to find if maybe :host() is not accepting :has(), but was unable to find anything on it, neither in the spec, nor on MDN or caniuse.
Does anyone have definitive knowledge/reference about this, and can point me to some documentation?
You want to style slotted content based on an element inside the slot
Since <slot> are reflected, (deep dive: ::slotted CSS selector for nested children in shadowDOM slot)
you need to style a <slot> in its container element.
If you want that logic to be done from inside the Component,
you could do it from the slotchange Event, which checks if a slotted element contains that DIV
Then creates a <style> element in the container element
Disclaimer: Provided code is a Proof of Concept, not production ready
<my-component>
Hello Web Component
</my-component>
<!-- <my-component> will add a STYLE element here -->
<my-component>
<!-- <my-component> will assign a unique ID to the DIV -->
<div>Web Component with a DIV in the slot</div>
</my-component>
<script>
customElements.define("my-component", class extends HTMLElement {
constructor() {
super().attachShadow({mode: "open"}).innerHTML = `<slot/>`;
let slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (evt) => {
[...slot.assignedNodes()].forEach(el => {
if (el.nodeName == "DIV") {
el.id = "unique" + new Date() / 1;
// inject a <style> before! <my-component>
this.before( Object.assign( document.createElement("STYLE"), {
innerHTML : `#${el.id} { background:lightgreen } `
}));
}
});
});
}
})
</script>
PS. Don't dynamically add any content inside <my-component>, because that slotchange will fire again...

How to iterate with click event through Array of divs

I want text parts to appear, and disappear, on click.
Before the first click you can only see the banner, and no verses yet; on the first click the first verse appears, on second click the second verse appears in place of the first, and so on.
I am trying to achieve this with hiding the elements, placing them in an array, and let them display when the number of times the function gets called fits the index of the verse.
I am new to JavaScript, and don't understand the exceptions thrown. If i try to inspect my code online, I get this Exception when O try to call the function by clicking on the website:
Uncaught TypeError: Cannot read properties of null (reading 'style')
This is my code so far:
const text = document.querySelector(".banner")
document.addEventListener('click', myFunction);
const verse1 = document.querySelector(".verse1")
const verse2 = document.querySelector(".verse2")
const verse3 = document.querySelector(".verse3")
const verse4 = document.querySelector(".verse4")
const verse5 = document.querySelector(".verse5")
const verses = [verse1, verse2, verse3, verse4, verse5]
let versesLength = verses.length;
function myFunction() {
for (let i = 0; i < versesLength; i++) {
text.innerHTML = verses[i].style.display = 'block';
}
}
<div class="banner">
<script src="main.js"></script>
<img src="files/SomeLogo.jpg" alt="We are still building on our Website:-)">
</div>
<div id="verses">
<div class="verse1" style="display: none">Lorem Ipsum</div>
<div class="verse2" style="display: none">Lorem Ipsum2</div>
<div class="verse3" style="display: none">Lorem Ipsum3</div>
<div class="verse4" style="display: none">Lorem Ipsum4</div>
<div class="verse5" style="display: none">Lorem Ipsum5</div>
</div>
I am stuck, and clicked through similar questions for the last hours. Thanks in advance for any help
without changing anything in the HTML, you can do something like this in javascript
const text = document.querySelector(".banner")
document.addEventListener('click', myFunction);
let verses = document.querySelector("#verses").children
let count = 0
function myFunction() {
Array.from(verses).forEach(el=> el.style.display="none")
if(count < verses.length){
verses[count].style.display = 'block'
count ++
if(count===verses.length) count =0
}
}
You can remove the need for an array by giving all the verse elements the same class: verse. We can grab them with querySelectorAll.
Add a data attribute to each verse to identify them.
In order to limit the number of global variables we can use a closure - in the addEventListener we call the handleClick function which initialises the count, and then returns a function that will be assigned to the listener. This is a closure. It maintains a copy of its outer lexical environment (ie variables) that it can use when it's returned.
// Cache the elements with the verse class
const banner = document.querySelector('.banner');
const verses = document.querySelectorAll('.verse');
// Call `handleClick` and assign the function it
// returns to the listener
document.addEventListener('click', handleClick());
function handleClick() {
// Initialise `count`
let count = 1;
// Return a function that maintains a
// copy of `count`
return function () {
// If the count is 5 or less
if (count < verses.length + 1) {
// Remove the banner
if (count === 1) banner.remove();
// Remove the previous verse
if (count > 1) {
const selector = `[data-id="${count - 1}"]`;
const verse = document.querySelector(selector);
verse.classList.remove('show');
}
// Get the new verse
const selector = `[data-id="${count}"]`;
const verse = document.querySelector(selector);
// And show it
verse.classList.add('show');
// Increase the count
++count;
}
}
}
.verse { display: none; }
.show { display: block; margin-top: 1em; padding: 0.5em; border: 1px solid #787878; }
[data-id="1"] { background-color: #efefef; }
[data-id="2"] { background-color: #dfdfdf; }
[data-id="3"] { background-color: #cfcfcf; }
[data-id="4"] { background-color: #bfbfbf; }
[data-id="5"] { background-color: #afafaf; }
<div class="banner">
<img src="https://dummyimage.com/400x75/404082/ffffff&text=We+are+still+building+our+website" alt="We are still building on our Website:-)">
</div>
<div>
<div data-id="1" class="verse">Lorem Ipsum 1</div>
<div data-id="2" class="verse">Lorem Ipsum 2</div>
<div data-id="3" class="verse">Lorem Ipsum 3</div>
<div data-id="4" class="verse">Lorem Ipsum 4</div>
<div data-id="5" class="verse">Lorem Ipsum 5</div>
</div>
Additional documentation
Template/string literals
classList
This should make it:
const verses = document.querySelectorAll('.verse');
const banner = document.querySelector('.banner');
const length = verses.length;
let counter = 0;
document.onclick = () => {
if (counter === 0) banner.classList.add('hide');
if (counter >= length) return;
verses[counter].classList.add('show');
if (verses[counter - 1]) verses[counter - 1].classList.remove('show');
counter++;
};
body {
background: orange;
}
.hide {
display: none !important;
}
.show {
display: block !important;
}
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
<div class="banner">
<img
src="files/SomeLogo.jpg"
alt="We are still building on our Website:-)"
/>
</div>
<div id="verses">
<div class="verse1 verse hide">Lorem Ipsum</div>
<div class="verse2 verse hide">Lorem Ipsum2</div>
<div class="verse3 verse hide">Lorem Ipsum3</div>
<div class="verse4 verse hide">Lorem Ipsum4</div>
<div class="verse5 verse hide">Lorem Ipsum5</div>
</div>
<script src="main.js"></script>
</body>
</html>
Fixing the error
The "Cannot read properties of null" error occurs because you try to access the properties of null. Your array holds nulls because you queried for the elements before the browser has inserted them into its DOM.
The browser parses the HTML the same way you would read it: From left to right, and top to bottom.
If the browser encounters a regular <script> element, it halts parsing and first executes the JavaScript. Naturally, some elements may not yet be available in the DOM.
There are multiple ways to defer script execution:
Add attribute defer to <script>: Will execute once the DOM is fully built.
Add attribute type="module" to <script>: Similar to defer, but will also make your code be treated as a JS module. This will also make your code run in strict mode.
Use JS event DOMContentLoaded: Similar to defer, but encapsulated in your JS-file.
Use JS event load: Similar to DOMContentLoaded, but will additionally wait until all resources (e.g. images, videos) have loaded. Prefer DOMContentLoaded if applicable.
Move <script> to the bottom of the HTML: Effectively like defer. Scripts with defer will still load after scripts at the bottom.
The simplest solution would be to use defer, as with it you wouldn't have to change your JS code:
<script src="main.js" defer></script>
By the way: Don't be fooled by the StackOverflow snippets; when using the on-site snippets, the <script> for the code is moved to the bottom of the HTML!
The feature!
Variable lifetimes
Variables in JS only persist for as long as they are used.
Using a variable that is declared outside a function will create a closure around your function and variable. That means, that variable will persist for as long as the function exists that uses it:
let someVariable = 0;
function someFunction() {
// `someVariable` is used, so it will persist across calls!
return someVariable;
}
This means, to have the variable for "keeping track of what verse to show" persist, it has to be declared outside your function.
Show and hide!
By calculating the previous verse's index with the index of the next-to-show verse, we only have to keep one counter. With two counters, they might get out of sync if we don't handle them correctly.
let nextToShow = 0;
function showNextVerse() {
const previousIndex = nextToShow - 1;
// ...
++nextToShow; // Increase counter for next call
}
In our case, the user (or rather, their clicks) will play the role of the loop. They will cause our click handler (the function) to run occasionally, at which point we have to swap the verses.
Swapping the verses can be done in many ways, but we'll stick to your "inline style" way: (Final code)
document.addEventListener("click", showNextVerse);
const banner = document.querySelector(".banner");
const verses = document.getElementById("verses").children; // More on this later
let nextToShow = 0;
function showNextVerse() {
const previousIndex = nextToShow - 1;
// On every call, hide the banner
banner.style.display = "none"; // Use `.style` instead of `.innerHTML` to preserve its HTML!
// Hide previous if valid index
if (previousIndex >= 0 && previousIndex < verses.length) {
verses[previousIndex].style.display = "none";
}
// Show next if valid index
if (nextToShow >= 0 && nextToShow < verses.length) {
verses[nextToShow].style.display = "block";
}
++nextToShow;
}
<div class="banner">
<script src="main.js" defer></script>
<img alt="We are still building on our Website:-)">
</div>
<div id="verses">
<div style="display:none">Lorem Ipsum1</div>
<div style="display:none">Lorem Ipsum2</div>
<div style="display:none">Lorem Ipsum3</div>
<div style="display:none">Lorem Ipsum4</div>
<div style="display:none">Lorem Ipsum5</div>
</div>
Improvements?!
There is no need for the variable versesLength; you can directly replace each of its occurences with verses.length. It doesn't improve on the original name, and is one more potential source for bugs if not synchronized with the original variable.
Correctly use class and id
Currently, your verses use class as if it was id; they each use a different class. This is not wrong, but semantically I would use id for this purpose.
To use the class attribute effectively, you should give each verse the class verse. This way, you can select them more easily via JS (see next section).
Easier getting of elements
As with everything in the coding world, there are many solutions to a problem. You solved getting the elements in a rather tedious way, but there are alternatives: (Non-exhaustive list)
Use document.querySelectorAll().
Rename verses to use same class, and use document.getElementsByClassName().
Use Element.children.
You may have already noticed how I get all the verses. In fact, verses (in the final code) doesn't even reference an array, but an HTMLCollection. It is very similar to an array, with the exception of it updating live to changes:
const elementsWrapper = document.getElementById("elements");
const collection = elementsWrapper.children;
const array = Array.from(elementsWrapper.children);
document.getElementById("bt-add").addEventListener("click", function() {
const newElement = document.createElement("div");
newElement.textContent = "Added later";
elementsWrapper.appendChild(newElement);
});
document.getElementById("bt-log").addEventListener("click", function() {
console.log("collection.length:", collection.length);
console.log("array.length:", array.length);
});
<button id="bt-add">Add new element</button>
<button id="bt-log">Log <code>.length</code></button>
<p>
Try logging first! Then try to add elements, and log again! See the difference?
</p>
<div>Elements:</div>
<div id="elements">
<div>Initially existent</div>
<div>Initially existent</div>
</div>
Alternative way of hiding
Here are ways of hiding the elements:
Use inline styling (this is what you did!).
Use CSS classes.
Use the HTML attribute hidden.
For small style changes I too would use inline styling. But for only hiding elements I would use the hidden attribute.
Also, there are multiple CSS ways of hiding elements:
Using display: none: Will hide the element as if it doesn't exist.
Using opacity: 0: Will hide the element by making it invisible; it still takes up space, and should still be part of the accessibility tree (opinionated).
Moving it off-site with position: fixed and top, left, etc. properties: (Please don't.)
Will move the element off-site, visually. It will still be part of the accessibility tree, and will only work for languages with the intended writing direction (e.g. it won't work for right-to-left languages).
Setting width, height, margin, padding and border to 0: Will hide the element only visually; it will still be part of the accessibility tree, and will stop margin collapse. Screen-reader only classes use this for non-visual elements, very useful.

Conditional styling on class in Svelte

I'm trying to use Svelte to do some conditional styling and highlighting to equations. While I've been successful at applying a global static style to a class, I cannot figure out how to do this when an event occurs (like one instance of the class is hovered over).
Do I need to create a stored value (i.e. some boolean that gets set to true when a class is hovered over) to use conditional styling? Or can I write a function as in the example below that will target all instances of the class? I'm a bit unclear why targeting a class in styling requires the :global(classname) format.
App.svelte
<script>
// import Component
import Katex from "./Katex.svelte"
// math equations
const math1 = "a\\htmlClass{test}{x}^2+bx+c=0";
const math2 = "x=-\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}";
const math3 = "V=\\frac{1}{3}\\pi r^2 h";
// set up array and index for reactivity and initialize
const mathArray = [math1, math2, math3];
let index = 0;
$: math = mathArray[index];
// changeMath function for button click
function changeMath() {
// increase index
index = (index+1)%3;
}
function hoverByClass(classname,colorover,colorout="transparent")
{
var elms=document.getElementsByClassName(classname);
console.log(elms);
for(var i=0;i<elms.length;i++)
{
elms[i].onmouseover = function()
{
for(var k=0;k<elms.length;k++)
{
elms[k].style.backgroundColor=colorover;
}
};
elms[i].onmouseout = function()
{
for(var k=0;k<elms.length;k++)
{
elms[k].style.backgroundColor=colorout;
}
};
}
}
hoverByClass("test","pink");
</script>
<h1>KaTeX svelte component demo</h1>
<h2>Inline math</h2>
Our math equation: <Katex {math}/> and it is inline.
<h2>Displayed math</h2>
Our math equation: <Katex {math} displayMode/> and it is displayed.
<h2>Reactivity</h2>
<button on:click={changeMath}>
Displaying equation {index}
</button>
<h2>Static math expression within HTML</h2>
<Katex math={"V=\\pi\\textrm{ m}^3"}/>
<style>
:global(.test) {
color: red
}
</style>
Katex.svelte
<script>
import katex from "katex";
export let math;
export let displayMode = false;
const options = {
displayMode: displayMode,
throwOnError: false,
trust: true
}
$: katexString = katex.renderToString(math, options);
</script>
<svelte:head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex#0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
</svelte:head>
{#html katexString}
If I understand it correctly you have a DOM structure with arbitrary nested elements and you would want to highlight parts of the structure that share the same class.
So you would have a structure like this:
<div>
<p>This is some text <span class="a">highlight</span></p>
<span class="a">Another highlight</span>
<ul>
<li>Some listitem</li>
<li class="a">Some listitem</li>
<li class="b">Some listitem</li>
<li class="b">Some listitem</li>
</ul>
</div>
And if you select an element with class="a" all elements should be highlighted regardles where they are in the document. This arbitrary placement makes using the sibling selector in css not possible.
There is no easy solution to this, but I will give you my attempt:
This is the full code with some explanation
<script>
import { onMount } from 'svelte'
let hash = {}
let wrapper
onMount(() => {
[...wrapper.querySelectorAll('[class]')].forEach(el => {
if (hash[el.className]) return
else hash[el.className] = [...wrapper.querySelectorAll(`[class="${el.className}"]`)]
})
Object.values(hash).forEach(nodes => {
nodes.forEach(node => {
node.addEventListener('mouseover', () => nodes.forEach(n => n.classList.add('hovered')))
node.addEventListener('mouseout', () => nodes.forEach(n => n.classList.remove('hovered')))
})
})
})
</script>
<div bind:this={wrapper}>
<p>
Blablabla <span class="a">AAA</span>
</p>
<span class="a">BBBB</span>
<ul>
<li>BBB</li>
<li class="a b">BBB</li>
<li class="b">BBB</li>
<li class="b">BBB</li>
</ul>
</div>
<style>
div :global(.hovered) {
background-color: red;
}
</style>
The first thing I did was use bind:this to get the wrapping element (in your case you would put this around the {#html katexString}, this will make that the highlight is only applied to this specific subtree.
Doing a querySelector is a complex operation, so we will gather all the related nodes in a sort of hashtable during onMount (this kind of assumes the content will never change, but since it's rendered with #html I believe it's safe to do so).
As you can see in onMount, I am using the wrapper element to restrict the selector to this section of the page, which is a lot faster than checking the entire document and is probably what you want anyway.
I wasn't entirely sure what you want to do, but for simplicity I am just grabbing every descendant that has a class and make a hash section for each class. If you only want certain classes you could write out a bunch of selectors here instead:
hash['selector-1'] = wrapper.querySelectorAll('.selector-1');
hash['selector-2'] = wrapper.querySelectorAll('.selector-2')];
hash['selector-3'] = wrapper.querySelectorAll('.selector-3');
Once this hashtable is created, we can loop over each selector, and attach two event listeners to all of the elements for that selector. One mouseover event that will then again apply a new class to each of it's mates. And a mouseout that removes this class again.
This still means you have to add hovered class. Since the class is not used in the markup it will be removed by Svelte unless you use :global() as you found out yourself. It is indeed not that good to have global classes because you might have unintended effect elsewhere in your code, but you can however scope it as I did in the code above.
The line
div > :global(.hovered) { background-color: red; }
will be processed into
div.svelte-12345 .hovered { background-color: red; }
So the red background will only be applied to .hovered elements that are inside this specific div, without leaking all over the codebase.
Demo on REPL
Here is the same adapted to use your code and to use a document-wide querySelector instead (you could probably still restrict if wanted by having the bind one level higher and pass this node into the component)
Other demo on REPL

How to add/update scoped CSS variables via JavaScript

Is there a native API which can update the CSS variables scoped under a particular CSS class(or even any other complex CSS selector)predefined in a stylesheet? The question can be generalized for not just CSS variables but other CSS properties as well, i.e whether class specific CSS properties can be updated without targeting a specific HTML element, but by targeting the class definition itself.
Please find below the code snippets which demonstrates an example scenario. You can also find comments in the code to as to what I believe is happening/I am doing on specific lines.
var toggle = true;
function changeColor() {
document.documentElement.style.setProperty('--bg-color', toggle ? 'green' : 'red');
// this works for the "outer" div since there we receive global value(value defined in :root) of --bg-color
toggle = !toggle;
// here I want to also change the scoped value of --bg-color for "inner-primary" and "inner-secondary"
// currently I can do this by doing:
document.querySelectorAll('.inner-primary').forEach(ele => ele.style.setProperty('--bg-color', toggle ? 'blue' : 'yellow'))
document.querySelectorAll('.inner-secondary').forEach(ele => ele.style.setProperty('--bg-color', toggle ? 'yellow' : 'blue' ))
// another way I can see is: we dynamically insert a style tag, but this feels very awkward and can quickly get out of hand on multiple iterations
}
:root {
--bg-color: red;
}
body {
margin: 0;
}
.outer {
width: 100vw;
height: auto;
min-height: 100vh;
text-align: center;
background-color: var(--bg-color); /* receives value from :root */
}
.inner-primary,
.inner-secondary {
width: 100px;
height: 100px;
/* received scoped value from .inner-primary or .inner-secondary defined below*/
background-color: var(--bg-color);
border: 1px solid black;
margin: auto;
margin-bottom: 10px;
}
.inner-secondary {
--bg-color: yellow;
}
.inner-primary {
--bg-color: blue;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Testing</title>
</head>
<body>
<div class="outer">
<div class="inner-primary"></div>
<div class="inner-secondary"></div>
<div class="inner-primary"></div>
<div class="inner-secondary"></div>
<button onclick="changeColor()">Change Color</button>
</body>
</html>
Please try running this to get a full idea of intended effect. You can click "Change Color" button at the bottom to see the effects in action.
To get the intended overriding for CSS variable --bg-color for classes inner-primary and inner-secondary, I had to use querySelectorAll with the required CSS selector(in this case just a class name) and iteratively set the CSS variable for each individual element found.
By nature of how CSS gets read by the browser, feels like the other solution to this is to dynamically insert a style element tag into the DOM, with the required CSS variable update, scoped under the required class name(or any other required selector)..
But this feels awkward and can quickly get out of hand if we don't implement some system to reuse the same style tag and not insert new ones during each toggle.
Is there any other way to do this? Any native API which can solve this without having to access individual elements or without inserting style tags dynamically..?
As suggested by A Haworth and referring Change CSS of class in Javascript? I was able update changeColor function to use CSSStyleSheet(MDN link) instead. Please find the updated function below, which uses this API:
var toggle = true;
function changeColor() {
document.documentElement.style.setProperty('--bg-color', toggle ? 'green' : 'red');
// solution using document stylesheets
const styleSheet = document.styleSheets[0]
const cssRules = Array.from(styleSheet.cssRules);
const primaryClassIndex = cssRules.findIndex(cssRule => cssRule.selectorText === '.inner-primary');
const secondaryClassIndex = cssRules.findIndex(cssRule => cssRule.selectorText === '.inner-secondary');
//update primary:
styleSheet.deleteRule(primaryClassIndex);
styleSheet.insertRule(`.inner-primary {--bg-color: ${toggle ? 'yellow' : 'blue'};}`, primaryClassIndex)
//update secondary:
styleSheet.deleteRule(secondaryClassIndex);
styleSheet.insertRule(`.inner-secondary {--bg-color: ${toggle ? 'blue' : 'yellow'};}`, secondaryClassIndex)
//toggle
toggle = !toggle;
}
This is still some concern here since it seems like we can only overwrite the entire cssRule(which may also include other CSS properties) for a particular selector, and not just one required property. But this may arguably be better than updating each individual element style or inserting style tags as mentioned in the question.
Can check the full working codepen at => https://codepen.io/yadus/pen/mdWZmXX

Strange behavior in Polymer data-binding to an attribute

Using Polymer 1.0 I'm trying to bind to an attribute of a custom element, and just display it.
The custom element is in fact an <iron-input> list, that has an add and a delete button. I'd like to reflect any change in that list to the host. It also has a minItemSize attribute meaning it has at least this many elements. So I added a check to the observer, adding extra elements in case it goes under this number.
But when I bind to the attribute that holds the list, things get out of sync, and I can delete all of the inputs from the ui.
I have two <dyn-inputlist> elements. In one of them I don't bind to the data
attribute, in the other I do.
The first one behaves as expected: adds and removes on the button click.
The other doesn't work, because you can remove all input boxes. Even though the data itself is updated, and filled with extra items, for some reason the UI doesn't reflect this. (Checking the data property of the element does show that it has the correct number of items)
I also expect that if I set data={{myData}} on both dyn-inputlist element, they always display the same thing. But pressing add/remove buttons randomly on either component gets them out of sync.
Am I missing something?
Thanks in advance.
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="bower_components/webcomponentsjs/webcomponents.js"></script>
<link rel="import" href="components/dyn-inputlist.html"/>
</head>
<body>
<template is="dom-bind">
<dyn-inputlist min-item-size="4"></dyn-inputlist>
<div>{{mydata}}</div>
<dyn-inputlist min-item-size="4" data="{{mydata}}"></dyn-inputlist>
</template>
</body>
</html>
dyn-inputlist.html:
<link rel="import" href="../../polymer/polymer.html">
<link rel="import" href="../../iron-input/iron-input.html">
<dom-module id="dyn-inputlist">
<template>
<button on-click="removeItem">x</button>
<button on-click="addItem">+</button>
<template is="dom-repeat" items="{{data}}">
<div>
<span>{{index}}</span>
<input is="iron-input" bind-value="{{item.content}}">
</div>
</template>
</template>
<script>
Polymer({
is: 'dyn-inputlist',
properties: {
minItemSize: {
type: Number,
notify: true,
value: 1
},
data: {
type: Array,
reflectToAttribute: true,
notify: true,
value: function () {
return []
}
}
},
observers: ['_dataChanged(data.*)'],
addItem: function (e) {
this.unshift('data', {content: ""});
this.reflectPropertyToAttribute('data')
},
removeItem: function (e) {
this.shift('data');
this.reflectPropertyToAttribute('data')
},
_dataChanged: function (e) {
if (this.data != null) {
while (this.data.length < this.minItemSize) {
this.push('data', {content: ""})
}
} else {
this.data = [{content: ""}];
}
this.reflectPropertyToAttribute('data');
}
});
</script>
</dom-module>
EDIT:
This is the live code: http://jsbin.com/poquke/1/edit?html,output
I have played around a bit with your code and I noticed that it will work if you wrap the code in your changed handler in an async function. This fixed both issues that you described.
_dataChanged: function (e) {
this.async(function(){
if (this.data != null) {
while (this.data.length < this.minItemSize) {
this.push('data', {content: ""})
}
} else {
this.data = [{content: ""}];
}
});
}
I don't have a perfect explanation for this behaviour. I assume it is related somehow to the way Polymer handles the observation for changes. Each time you push to the data array in the changed handler, this in fact changes data and should in turn trigger the handler again.
No async is required if you simplify.
Here is the simplified code, this removes the repeated calls to _dataChanged when you push the minimum values, and allows polymer's built-in eventing system to take care of updating and notifying the other elements. A function: _createNewItem() is for creating an object. This simplifies where item object creation is handled.
http://jsbin.com/vemita/6/edit?html,output
The link and URL references have changed from the sample code in the question above to conform to the polymer element and demo page standards to be used with polyserve.
I've commented on your original code for why each line should or shouldn't be there. this includes the reason for the changes to _dataChanged
http://jsbin.com/ponafoxade/1/edit?html,output

Categories

Resources