I am trying to work with the Intersection Observer API. I have a function which works in my first iteration. The basic logic is that if the user scrolls down and adds or removes items from a basket, once the basket is in view again (as it is at the top of the document) then I fire an API call.
The issue is that it will not fire the function before scrolling, I want to trigger it if the item is visible or becomes visible again after scrolling (the second part is working)
Here is original js:
var observerTargets = document.querySelectorAll('[id^="mini-trolley"]');
var observerOptions = {
root: null, // null means root is viewport
rootMargin: '0px',
threshold: 0.01 // trigger callback when 1% of the element is visible
}
var activeClass = 'active';
var trigger = $('button');
var isCartItemClicked = false;
trigger.on('click', function() {
isCartItemClicked = true;
});
function observerCallback(entries, observer) {
entries.forEach(entry => {
if(entry.isIntersecting && isCartItemClicked){
$(observerTargets).removeClass(activeClass);
$(entry.target).addClass(activeClass);
isCartItemClicked = false;
console.log('isCartItemClicked and in view');
// do my api call function here
} else {
$(entry.target).removeClass(activeClass);
}
});
}
var observer = new IntersectionObserver(observerCallback, observerOptions);
[...observerTargets].forEach(target => observer.observe(target));
I have updated this so it now checks if the item is visible. so I have updated:
if(entry.isIntersecting && isCartItemClicked)
to
if((entry.isVisible || entry.isIntersecting) && isCartItemClicked)
The issue as I understand is that the observer is only triggered on scroll, but the entry.isVisible is part of the observer callback function.
I have made a JSFIDDLE here (which has HTML and CSS markup).
Is it possible to modify the code. Weirdly the MDN page does not mention the isVisible property, but it is clearly part of the function.
This one is a little tricky but can be done by creating a someObserverEntriesVisible parameter that is set by the observerCallback. With that in place we can define how the button triggers should be handled separately from the observer callback for each intersecting entry.
const observerTargets = document.querySelectorAll('[id^="mini-trolley"]');
const observerOptions = {
root: null, // null means root is viewport
rootMargin: '0px',
threshold: 0.01 // trigger callback when 1% of the element is visible
};
const activeClass = 'active';
const trigger = $('button');
let isCartItemClicked = false;
let someObserverEntriesVisible = null;
let observerEntries = [];
trigger.on('click', () => {
isCartItemClicked = true;
if (someObserverEntriesVisible) {
console.log('fired from button');
observerCallback(observerEntries, observer, false);
}
});
function observerCallback(entries, observer, resetCartItemClicked = true) {
observerEntries = entries;
someObserverEntriesVisible = false;
entries.forEach(entry => {
someObserverEntriesVisible ||= entry.isIntersecting;
if (entry.isIntersecting && isCartItemClicked) {
$(entry.target).addClass(activeClass);
// add API call here
if (resetCartItemClicked) {
isCartItemClicked = false;
console.log('fired from observer');
}
} else {
$(entry.target).removeClass(activeClass);
}
});
}
const observer = new IntersectionObserver(observerCallback, observerOptions);
[...observerTargets].forEach(target => observer.observe(target));
#content {
height: 500px;
}
.active {
background-color: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="mini-trolley">Observer target1</div>
<button>Top button</button>
<div id="content"></div>
<div id="mini-trolley">Observer target2</div>
<button>Bottom button</button>
Related
I'm appending a DOM element like this:
this.store.state.runtime.UIWrap.appendChild(newElement)
When I immediately try to measure the new element's width I get 2.
I tried:
setTimeout()
double nested window.requestAnimationFrame()
MutationObserver
The above works very unreliably, like 50% of the time. Only when I set a large timeout like 500ms it worked.
This happens only on mobile.
This is the workaround that I'm using, but it's ugly:
function getWidthFromStyle(el) {
return parseFloat(getComputedStyle(el, null).width.replace('px', ''))
}
function getWidthFromBoundingClientRect(el) {
return el.getBoundingClientRect().width
}
console.log(getWidthFromBoundingClientRect(newElement))
while (getWidthFromBoundingClientRect(newElement) < 50) {
await new Promise(r => setTimeout(r, 500))
console.log(getWidthFromBoundingClientRect(newElement))
}
I tried with both functions getWidthFromStyle() and getWidthFromBoundingClientRect() - no difference. The width gets calculated properly after a couple of cycles.
I also tried using MutationObserver without success.
Is there a way to know when the DOM is fully updated and styled before I try to measure an element's width/height?
P.S. I'm not using any framework. this.store.state.runtime... is my own implementation of a Store, similar to Vue.
EDIT: The size of the element depended on an image inside it and I was trying to measure the element before the image had loaded. Silly.
it can done with MutationObserver.
doesn't this method solve your problem?
const div = document.querySelector("div");
const span = document.querySelector("span");
const observer = new MutationObserver(function () {
console.log("new width", size());
});
observer.observe(div, { subtree: true, childList: true });
function addElem() {
setTimeout(() => {
const newSpan = document.createElement("span");
newSpan.innerHTML = "second";
div.appendChild(newSpan);
console.log("element added");
}, 3000);
}
function size() {
return div.getBoundingClientRect().width;
}
console.log("old width", size());
addElem();
div {
display: inline-block;
border: 1px dashed;
}
span {
background: gold;
}
<div>
<span>one</span>
</div>
You can use something like this:
export function waitElement(elementId) {
return new Promise((resolve, reject) => {
const element = document.getElementById(elementId);
if (element) {
resolve(element);
} else {
let tries = 10;
const interval = setInterval(() => {
const element = document.getElementById(elementId);
if (element) {
clearInterval(interval);
resolve(element);
}
if (tries-- < 0) {
clearInterval(interval);
reject(new Error("Element not found"));
}
}, 100);
}
});
}
While I'm observing a web-page there is a button that after I click an element appears.
I already have the id of that element, what I want to do in a single code:
press the button, wait for the specific element to appear (become defined), perform an action.
What I tried to do is this:
btn = document.getElementById("btn");
btn.click();
while(document.getElementById("id") == undefined){
continue;
}
console.log("element is loaded!!");
That code didn't work for me (the browser got stuck).
I thought also to pause the code for specific time that it gets to the element to appear (sleep), but is there a better way?
Again, I don't have access to the code of the web-page, so I can't rais a flag when this element is loaded.
Try using a Promise:
btn = document.getElementById("btn");
btn.click();
new Promise((resolve, reject) => {
while (document.getElementById("id") == undefined) {}
resolve();
}).then(() => {
console.log("element is loaded!!");
});
globally, if you want to check if the variable is set :
if(variable)
{
// Do stuff
}
You could set an interval
btn = document.getElementById("btn");
btn.click();
let intv = setInterval(() => {
if (!!document.getElementById("id")) {
console.log('the element is here');
clearInterval(this);
}}, 200);
}
console.log("element is loaded!!");
Use a MutationObserver to check whether the element exists everytime a change in the DOM occurs:
let observer = new MutationObserver(() => document.getElementById('id') ? console.log("loaded") : '');
observer.observe(document.body, {
childList: true
});
Demo:
let observer = new MutationObserver(() => document.getElementById('id') ? console.log("loaded") : '');
observer.observe(document.body, {
childList: true
});
/* below is simply for demonstration */
document.getElementById("btn").addEventListener('click', () => {
setTimeout(() => {
document.body.appendChild(Object.assign(document.createElement("p"), {id: 'id',innerHTML: 'Hello World!'}));
}, 1000)
})
<button id="btn">Click to add an element in 1 second</button>
Your browser is probably crashing because the code is executed too many times in the while loop.
You could try using MutationObserver to listen for change in DOM.
Or
Add use setInterval instead of the while loop.
let interval;
function handleClick() {
// add the new element after 5 seconds
const divNode = document.createElement('div');
divNode.id = 'newElement';
window.setTimeout(() => {
document.body.append(divNode);
}, 5000);
// every second check for the element
interval = window.setInterval(() => {
checkIfElementIsInDom();
}, 1000);
}
function checkIfElementIsInDom() {
console.log('Checking...');
const newNode = document.getElementById('newElement');
if (newNode) {
console.log('completed!');
// stop the interval
clearInterval(interval);
}
}
const buttonNode = document.getElementById('button');
buttonNode.addEventListener('click', handleClick.bind(this));
<button type="button" id="button">Click</button>
Here is an example of using a MutationObserver
const targetNode = document.querySelector('.test');
// 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.');
// Check for our new item
const itm = document.querySelector('.itm');
if (itm) {
console.log('we found our item');
observer.disconnect();
}
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
document.querySelector('button').addEventListener('click', () => {
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
setTimeout(() => {
// Add an item.
document.querySelector('.test').innerHTML = '<div class="itm">Here it is</div>';
}, 5000);
});
<section>
<div>
<button>Click Me</button>
</div>
</section>
<section class="test">
</section>
I'm building a website and right now I'm having an issue trying to get an if statement to read a change of state variable.
I'm trying to close my navbar menu when I click anywhere in the body, other than the navbar. However, my if statement that contains the event listener for the body will not execute because (I think) that the change of state variable that is being used for the condition (true when menu is open, false when menu is closed) is just not being read.
When I open the nav menu, the change of state variable changes to true, as it is intended to. When I close the menu using the button on the nav bar, the change of state variable changes to false, as it is intended to.
Here's my code:
"use strict";
const btnNav = document.querySelector(".nav-btn--container");
const linkContainer = document.querySelector(".link-container");
const header = document.querySelector(".header");
const sidebar = document.querySelector(".sidebar");
const bodyChildren = header.parentElement.children;
const navBtn = document.querySelector(".nav-btn");
const menu = document.querySelector(".sidebar__menu");
const body = document.querySelector(".container");
/**** change of state variables ****/
let menuIsOpen = false;
/**** functions ****/
const closeMenu = function () {
addRemoveBlur("remove");
navBtn.classList.remove("nav-btn--opened");
sidebar.classList.remove("blur");
navBtn.classList.add("nav-btn--closed");
menu.style.visibility = "hidden";
menu.style.transform = "translateX(-150%)";
menuIsOpen = false;
};
const openMenu = function () {
addRemoveBlur("add");
navBtn.classList.remove("nav-btn--closed");
navBtn.classList.add("nav-btn--opened");
menu.style.visibility = "visible";
menu.style.transform = "translateX(0%)";
menuIsOpen = true;
};
const closeMenuByBody = function (e) {
const click = e.target;
closeMenu();
};
const openCloseMenu = function () {
if (!menuIsOpen) {
openMenu();
return;
}
if (menuIsOpen) {
closeMenu();
return;
}
};
/**** this is the problem ****/
if (menuIsOpen) {
body.addEventListener("click", function (e) {
closeMenuByBody(e);
});
}
The problem is that the event listener is never initialized, as your menuIsOpen variable is always declared false.
You should switch your if statement to be contained within the click event handler, like so:
body.addEventListener("click", function (e) {
if (menuIsOpen) {
closeMenuByBody(e);
}
});
You should also consider the following changes to your openCloseMenu method (here renamed to toggleMenu):
const toggleMenu = function () {
if (menuIsOpen) {
closeMenu();
} else {
openMenu();
}
};
As you are evaluating a boolean value, there is no need to test it in two different if expressions: you check if it's true and do something if it is, or else you do something knowing that it's false.
You could also remove your closeMenuByBody method, and just call the closeMenu method from your event listener, as you aren't doing anything in it appart from that:
body.addEventListener("click", function (e) {
if (menuIsOpen) {
e.preventDefault();
closeMenu();
}
});
So I am currently using the following code to execute my jQuery. However, as you can see I have set some timers on executing the second part of the code. What I would like is to see if #objectPrice exists, and then run the timed code but only once. This way I can replace the timers.
var checkExist = setInterval(function() {
if (jQuery('#objectPrice').length) {
var content = jQuery('#prisen').html();
objectPrice.value = content.replace(/\D/g,'');
}
}, 200); // check every 200ms
setTimeout(function() { jQuery("#downPayment").focus(); }, 2200);
setTimeout(function() { jQuery("#downPayment").blur(); }, 2205);
The right way to do it
You can do this by observing the direct parent of the element you want to check, let's assume that your element will be added to the page in the following div:
<div id="parent-div">
<div>child 1</div>
<div>child 2</div>
</div>
You will need to observe any new child added to the #parent-div and trigger a function when this happens:
const callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
if( mutation.addedNodes.length > 0){
for(let node of mutation.addedNodes) {
let jqueryNode = jQuery(node);
if(jqueryNode.is('#objectPrice')){
var content = jQuery('#prisen').html();
objectPrice.value = content.replace(/\D/g,'');
}
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(jQuery('#parent-div')[0], {childList: true});
But if you don't want to change your current code, you make it recursive and call it the first time, like the following:
var checkingFunction = function () {
if(jQuery('#objectPrice').length){
var content = jQuery('#prisen').html();
objectPrice.value = content.replace(/\D/g,'');
}else{
setTimeout(function() { checkingFunction() }, 200);
}
}
checkingFunction();
Is there a way to detect when the parent of an element changes (namely when changing from null to !null -- i.e., when the element is initially added to the DOM) using a MutationObserver? I can't find any documentation that shows how this could be achieved.
I am programmatically creating elements with document.createElement(). I return the created element from a function, but want to create a listener from within the function to react when the element is eventually added to the DOM, without knowing where or which parent it will be added to.
I'm not quite sure how else to phrase this, honestly.
const elem = document.createElement('div');
let added = false;
elem.addEventListener('added-to-dom', () => { added = true; });
// ^ how do I achieve this?
assert(added == false);
document.body.addChild(elem);
assert(added == true);
I don't see what's so hard about understanding this or why it was closed.
An easy but inelegant way is to monkeypatch Node.prototype.appendChild (and, if necessary, Element.prototype.append, Element.prototype.insertAdjacentElement, and Node.prototype.insertBefore) to watch for when an element is added to the DOM:
const elementsToWatch = new Set();
const { appendChild } = Node.prototype;
Node.prototype.appendChild = function(childToAppend) {
if (elementsToWatch.has(childToAppend)) {
console.log('Watched child appended!');
elementsToWatch.delete(childToAppend);
}
return appendChild.call(this, childToAppend);
};
button.addEventListener('click', () => {
console.log('Element created...');
const div = document.createElement('div');
elementsToWatch.add(div);
setTimeout(() => {
console.log('About to append element...');
container.appendChild(div);
}, 1000);
});
<button id="button">Append something after 1000ms</button>
<div id="container"></div>
Mutating built-in prototypes generally isn't a good idea, though.
Another option would be to use a MutationObserver for the whole document, but this may well result in lots of activated callbacks for a large page with frequent mutations, which may not be desirable:
const elementsToWatch = [];
new MutationObserver(() => {
// instead of the below, another option is to iterate over elements
// observed by the MutationObserver
// which could be more efficient, depending on how often
// other elements are added to the page
const root = document.documentElement; // returns the <html> element
const indexOfElementThatWasJustAdded = elementsToWatch.findIndex(
elm => root.contains(elm)
);
// instead of the above, could also use `elm.isConnected()` on newer browsers
// if an appended node, if it has a parent,
// will always be in the DOM,
// instead of `root.contains(elm)`, can use `elm.parentElement`
if (indexOfElementThatWasJustAdded === -1) {
return;
}
elementsToWatch.splice(indexOfElementThatWasJustAdded, 1);
console.log('Observed an appended element!');
}).observe(document.body, { childList: true, subtree: true });
button.addEventListener('click', () => {
console.log('Element created...');
const div = document.createElement('div');
div.textContent = 'foo';
elementsToWatch.push(div);
setTimeout(() => {
console.log('About to append element...');
container.appendChild(div);
}, 1000);
});
<button id="button">Append something after 1000ms</button>
<div id="container"></div>
You could listen for the DOMNodeInserted-event and compare the elements id.
Notice: This event is marked as Depricated and will probably stop working in modern modern browsers at some point in the near
future.
let container = document.getElementById('container');
let button = document.getElementById('button');
document.body.addEventListener('DOMNodeInserted', function(event) {
if (event.originalTarget.id == button.id) {
console.log('Parent changed to: ' + event.originalTarget.parentElement.id);
}
});
button.addEventListener('click', function(event) {
container.appendChild(button);
});
#container {
width: 140px;
height: 24px;
margin: 10px;
border: 2px dashed #c0a;
}
<div id="container"></div>
<button id="button">append to container</button>