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.
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 1 year ago.
Improve this question
I want to detect an element movement in my page . for example i have a bottom with offsetheight: 200px and offsetleft : 200px and i want to have a listener for detect that element position has been changed (not resized <======)
You can detect whether an attribute on an element has been changed or whether something has been added/subtracted from the DOM by using a MutationObserver.
Here's a simple example where a change anywhere in the body is noted by a console.log. The offset of the button you are interested in can then be read and checked against the original to see if it has moved.
<!doctype html>
<html>
<head>
<title>Observe</title>
<style>
.movedown {
position: relative;
width: 30vmin;
height: 30vmin;
}
.button {
width: 20vmin;
height: 20vmin;
background: pink;
}
</style>
</head>
<body>
<button onclick="this.classList.toggle('movedown');console.log('button.offsetTop = ' + button.offsetTop);">CLICK ME TO EXTEND/SHRINK ME SO THE OTHER BUTTON MOVES</button>
<button class="button">I AM THE BUTTON YOU ARE INTERESTED IN SEEING WHEN I HAVE MOVED</button>
<div></div>
<script>
//This script copied almost complete from MDN
// Select the node that will be observed for mutations
const targetNode = document.body;
const button = document.querySelector('.button');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('A ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// 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);
</script>
</body>
</html>
Code taken from MDN where further info on the observing such mutations can be found.
I've made a mistake. I paired my functionality to .on('click', ...) events. My system installs certain items and each item is categorized. Currently, my categories are [post, image, widgets], each having its own process and they are represented on the front-end as a list. Here's how it looks:
Each one of these, as I said, is paired to a click event. When the user clicks Install a nice loader appears, the <li> itself has stylish changes and so on.
I also happen to have a button which should allow the user to install all the items:
That's neat. Except...there is absolutely no way to do this without emulating user clicks. That's fine, but then, how can I wait for each item to complete (or not) before proceeding with the next?
How can I signal to the outside world that the install process is done?
It feels that if I use new CustomEvent, this will start to become hard to understand.
Here's some code of what I'm trying to achieve:
const installComponent = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve();
}, 1500);
});
};
$('.item').on('click', (event) => {
installComponent().then(() => {
console.log('Done with item!');
});
});
$('#install-all').on('click', (event) => {
const items = $('.item');
items.each((index, element) => {
element.click();
});
});
ul,
ol {
list-style: none;
padding: 0;
margin: 0;
}
.items {
display: flex;
flex-direction: column;
width: 360px;
}
.item {
display: flex;
justify-content: space-between;
width: 100%;
padding: 12px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 0;
}
.item h3 {
width: 80%;
}
.install-component {
width: 20%;
}
#install-all {
width: 360px;
height: 48px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul class="items">
<li class="item" data-component-name="widgets">
<h3>Widgets</h3>
<button class="install-component">Install </button>
</li>
<li class="item" data-component-name="post">
<h3>Posts</h3>
<button class="install-component">Install </button>
</li>
<li class="item" data-component-name="images">
<h3>Images</h3>
<button class="install-component">Install </button>
</li>
</ul>
<button id="install-all">Install All</button>
As you can see, all clicks are launched at the same time. There's no way to wait for whatever a click triggered to finish.
This is simple architectural problems with your application that can be solved by looking into a pattern that falls into MVC, Flux, etc.
I recommend flux a lot because it’s easy to understand and you can solve your issues by separating out your events and UI via a store and Actions.
In this case you would fire an action when clicking any of these buttons. The action could immediately update your store to set the UI into a loading state that disables clicking anything else and show the loader. The action would then process the loader which can be monitored with promises and upon completion the action would finalize by setting the loading state in the store to false and the UI can resolve to being normal again. The cool thing about the proper separation is the actions would be simple JS methods you can invoke to cause all elements to install if you so desire. Essentially, decoupling things now will make your life easier for all things.
This can sound very complicated and verbose for something as simple as click load wait finish but that’s what react, angular, flux, redux, mobx, etc are all trying to solve for you.
In this case I highly recommend examining React and Mobx with modern ECMaScript async/await to quickly make this issue and future design decisions much easier.
What you should do is to declare a variable which will store the installation if it's in progress. And it will be checked when you are trying to install before one installation is complete.
var inProgress = false;
const installComponent = () => {
inProgress = true;
return new Promise((resolve, reject) => {
if(inProgress) return;
else{
setTimeout(() => {
inProgress = false;
return resolve();
}, 1500);
}
});
};
I'd be looking to implement something like this:
let $items = $('.items .item');
let promises = new Array($items.length);
// trigger installation of the i'th component, remembering the state of that
function startInstallOnce(i) {
if (!promises[i]) {
let component = $items.get(i).data('component-name');
promises[i] = installComponent(component);
}
return promises[i];
}
// used when a single item is clicked
$items.on('click', function(ev) {
let i = $(this).index();
startInstallOnce(i);
});
// install all (remaining) components in turn
$('#install-all').on('click', function(ev) {
(function loop(i) { // async pseudo-recursive loop
if (i === components.length) return; // all done
startInstallOnce(i).then(() => loop(i + 1));
})(0);
});
I would like to add and remove 'over' from my class on an element created using a lit-html template triggered by 'dragEnter' and 'dragLeave':
#app {
background-color: #72a2bc;
border: 8px dashed transparent;
transition: background-color 0.2s, border-color 0.2s;
}
#app.over {
background-color: #a2cee0;
border-color: #72a2bc;
}
const filesTemplate = () =>
html`
<button id="app"
#dragover=${??}
#dragleave=${??}
>
Click Me
</button>
`;
In my old system I called these methods in a separate module via an event emitter, but I am hoping I can make it all defined in the template using lit-html.
dragEnter(e) {
this.view.element.className += ' over';
}
dragLeave(e) {
this.view.element.className = element.className.replace(' over', '');
}
It depends what your custom element looks like. With your template you could just put #dragover=${this.dragEnter}. However, if you want this to apply to your entire custom element and not just the button you can do something like this:
connectedCallback() {
super.connectedCallback();
this.addEventListener('dragover', this.dragEnter);
}
If you do not have custom element and just use lit-html by itself you have to put your event handlers dragEnter(e)and dragLeave(e) into the template like so: #dragover=${this.dragEnter}
You need to add the class with classList.add in dragEnter and remove it in dragLeave. In the future you maybe can use classMap directive in lit-html, however there is nothing wrong with just using classList. I would stick with just using classList. In a very distant future css might also have a selector for it: Is there a CSS ":drop-hover" pseudo-class?
I think that, in order to solve the problem in a "lit-html style", the solution has to be something like this:
import { html, render} from 'lit-html';
import { classMap } from 'lit-html/directives/class-map.js';
const myBtnClasses = {
over: false
};
function dragEnter(e) {
myBtnClasses.over = true;
renderFiles();
}
function dragLeave(e) {
myBtnClasses.over = false;
renderFiles();
}
const filesTemplate = (classes) =>
html`
<button id="app" class="${classMap(myBtnClasses)}"
#dragover=${dragEnter} #dragleave=${dragLeave}
>
Click Me
</button>
`;
function renderFiles() {
render(filesTemplate(myBtnClasses), YOUR_CONTAINER);
}
When using lit-html you have to express your UI as a function of your "state" and "rerender" each time your state changes, so the best solution in this little example is to consider your classes as part of your state.
Anyway better than
this.view.element.className += ' over';
is
this.view.element.classList.add('over');
And instead
this.view.element.className = element.className.replace(' over', '');
use
this.view.element.classList.remove('over');
This is better because of allowing to avoid many bugs like adding the same class many times.
I do not know lit-html but try
let sayHello = (name, myClass) => html`<h1 class="${myClass}">Hello ${name}</h1>`;
https://lit-html.polymer-project.org/
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>
I am trying to create a list which can be reordered by dragging the items in it.
When I drag an element for the first time dragstart.trigger="drag($event)" invokes the drag(e). In drag(e) I set data of the element dragged.
On dropping the dragged element drop.trigger="drop($event)" invokes drop(e).
In drop(e) I get the dragged element and remove it from the list/parent element <ul>.
After that I insert the dragged element to the dropped location.
The problem is once a element is dragged. I cannot able to drag it again to different target because the dragstart.trigger="drag($event)" is not invoking the drag(e).
How can I invoke dragstart.trigger="drag($event)"?
<ul id="columns" drop.trigger="drop($event)" dragover.trigger="allowDrop($event)">
<li id="item1" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>A</header></li>
<li id="item2" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>B</header></li>
<li id="item3" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>C</header></li>
<li id="item4" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>D</header></li>
<li id="item5" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>E</header></li>
</ul>
JS :
drag(e) {
console.log('handleDragStart');
// Target element is the source node.
this.dragSrcEl = e.currentTarget;
console.log('dragSrcEl :', this.dragSrcEl);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.currentTarget.outerHTML);
e.currentTarget.classList.add('dragElem');
return true;
}
allowDrop(e) {
console.log('handleDragover');
e.preventDefault();
}
dragend() {
console.log('handleDragEnd');
}
drop(e) {
console.log('handleDrop');
if (e.stopPropagation) {
e.stopPropagation();
}
// Don't do anything if dropping the same column we're dragging.
if (this.dragSrcEl != e.srcElement) {
e.currentTarget.removeChild(this.dragSrcEl);
let dropHTML = e.dataTransfer.getData('text/html');
e.srcElement.parentNode.insertAdjacentHTML('beforebegin',dropHTML)
}
e.currentTarget.classList.remove('over');
return false;
}
The reason dragstart is not invoked after reordering the elements, is because you're not really reordering them. You're actually removing the dragged element and then inserting a new copy of it.
This new copy is not handled by aurelia's composition engine, therefore not compiled, and so any aurelia-specific expressions in the html will not do anything. .trigger is simply a dead tag at that point.
Drag/drop is kind of a special beast and has never been particularly simple to implement in a natural way, especially when there's all kind of custom framework behavior attached to these elements.
You have 3 options here:
Do not use aurelia's trigger and instead just use el.addEventListener both when you first create them, and then when you create new copies.
Use aurelia's ViewEngine to re-compile (parts of) your view whenever you drop an element so that .trigger is processed which, under the hood, really kind of just does el.addEventListener anyway
Turn this into custom element with a repeat.for and let Aurelia handle the html side of things.
Now option 1 would certainly be the quickest way to get it to work, and option 2 would be slightly more robust and tricky to do, but both are quite hacky.
I'm a strong advocate of utilizing the framework rather than hacking around it, because things will be easier to maintain on the longterm and you can more easily add additional fancy behavior as the project evolves.
It may seem much more involved than what you are doing now, but by engaging more of the framework to handle the low-level stuff, you'll have "living" draggable elements with a fully functional Aurelia that you can do much more things with.
So here's just one example of how you might approach option 3:
In app.js, make your columns into a list of javascript objects:
items = [
{ text: "A", id: "item1" },
{ text: "B", id: "item2" },
{ text: "C", id: "item3" },
{ text: "D", id: "item4" },
{ text: "E", id: "item5" }
];
In app.html, pass those items to the columns custom element (to keep the html similar to your example i'll use as-element)
<template>
<require from="./resources/elements/columns"></require>
<ul as-element="columns" items.bind="items"></ul>
</template>
In resources/elements/columns.js, work against individual items viewmodels instead of against the html elements:
import { customElement, children, bindable } from "aurelia-templating";
#customElement("columns")
export class Columns {
// keeps a list of the viewmodels of the direct "li" children
#children("li") children;
// the columns
#bindable() items;
// the currently dragged column
dragColumn;
// the customEvent we dispatch from the child "column" element
handleColDragStart(e) {
// the viewmodel we passed into the customEvent
this.dragColumn = e.detail.column;
}
allowDrop(e) {
console.log("handleDragover");
e.preventDefault();
}
drop(e) {
console.log("handleDrop");
if (e.stopPropagation) {
e.stopPropagation();
}
// source drag index
let dragIdx = this.children.indexOf(this.dragColumn);
// if we can't resolve to a sibling (e.g. dropped on or outside the list),
// naively drop it at index 0 instead
let dropIdx = 0;
// try to find the drop target
let dropTarget = e.srcElement;
while (dropTarget !== document.body) {
let dropTargetVm = dropTarget.au && dropTarget.au.controller && dropTarget.au.controller.viewModel;
if (dropTargetVm) {
dropIdx = this.children.indexOf(dropTargetVm);
break;
} else {
dropTarget = dropTarget.parentElement;
}
}
if (dragIdx !== dropIdx) {
// only modify the order in the array of javascript objects;
// the repeat.for will re-order the html for us
this.items.splice(dropIdx, 0, this.items.splice(dragIdx, 1)[0]);
}
return false;
}
}
In resources/elements/columns.html, just listen for the customEvent we dispatch from the column element and other than that only handle drop:
<template id="columns" drop.trigger="drop($event)" dragover.trigger="allowDrop($event)">
<require from="./column"></require>
<li as-element="column" repeat.for="col of items" column.bind="col" coldragstart.trigger="handleColDragStart($event)">
</li>
</template>
In resource/elements/column.js handle the dragstart and dragend events, then dispatch a customEvent with a reference to the viewModel (so you don't have to deal with the html too much):
import { customElement, bindable } from "aurelia-templating";
import { inject } from "aurelia-dependency-injection";
#customElement("column")
#inject(Element)
export class Column {
el;
constructor(el) {
this.el = el;
}
#bindable() column;
dragstart(e) {
this.el.dispatchEvent(
new CustomEvent("coldragstart", {
bubbles: true,
detail: {
column: this
}
})
);
return true;
}
}
Finally, in resources/elements/column.html just listen for the dragstart event:
<template draggable="true" dragstart.trigger="dragstart($event)">
<header>${column.text}</header>
</template>
The part of this solution that might look a bit strange to you, also the part that I still consider a bit hacky, is where we try to get the ViewModel via el.au.controller.viewModel.
This is something you "just need to know". A custom element / html behavior always has an au property on it that contains a reference to the behavior instance with the controller, view, etc.
This is essentially the easiest (and sometimes the only) way to "get a hold of" aurelia when working directly against the html. With things like drag/drop I don't believe there is any way to avoid this, as there is unfortunately no native aurelia support for it.