Trying to understand a quirk in intersectionObserver API.
If an observed element is partially on screen but has not met the threshold defined in the options I would expect the call back not to fire, but it does. Trying to understand why or how to prevent it on page load.
function handleIntersect(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
// Should only fire at >= 0.8
console.log(entry, entry.intersectionRatio);
entry.target.classList.add('play');
}
});
}
function createObserver(element) {
let observer;
const options = {
threshold: [0.8]
};
observer = new IntersectionObserver(handleIntersect, options);
observer.observe(element);
}
const waypoints = document.querySelectorAll('.section');
waypoints.forEach(waypoint => {
createObserver(waypoint);
});
Reduced test case:
https://codepen.io/WithAnEs/pen/gxZPMK
Notice how the first 2 sections are animated in even though the second section does not meet the 0.8 threshold (console log). The first inclination is to add an intersectionRatio > 0.8 check, but this is a duplicate effort. Also, the ratio reporting is delayed so could be inaccurate enough not to pass this check.
isIntersecting doesn't depend on thresholds. It is true, if target element touches or intersects root element. So it's always true if intersectionRatio > 0.
If an observed element is partially on screen but has not met the
threshold defined in the options I would expect the call back not to
fire, but it does.
Generally, callback is called when condition intersectionRatio >= your_threshold is changed. Therefore it can be called with any intersectionRatio value.
Moreover, it's always called initially.
Also pay attention that 0.8 is just your desired threshold, but observer.thresholds[0] is actual. Without going into details, they could be different a bit, e.g. in Chrome.
Related
In order to get an animation to work, I am trying to update data in a method consecutively, but the updates are happening to fast.
The element in question was previously animated by setting a custom property --int-menu-height to a height, retrieved via a $ref. A transitionend event is then setting that variable to auto. Now to get the same transition into the other direction, I need to first remove the auto and replace it with an interpolatable value, and then set it to zero, so that it animates between those two values. There is another transitionend event waiting, to finish the entire interaction. This is what my code for the closing looks like, right now:
const comp = this;
const menu = this.$refs.menu;
const menuHeight = this.$refs.menuHeight.clientHeight+'px';
// set it too the actual height (from previously 'auto')
this.menuStyles = { '--int-menu-height': menuHeight };
this.$nextTick( () => {
// set it to zero, so that it animates
this.menuStyles = { '--int-menu-height': 0 }
});
// never firing, because no transition
menu.addEventListener('transitionend', function closeMenu() {
comp.isMenuOpen = false;
menu.removeEventListener('transitionend', closeMenu);
});
I'm thought that $nextTick() would do the trick, but it is still happening to fast. How can I update this.menuStyles only after making sure that the previous update has fully rendered through?
Like Radeanu pointed out, I can just use setTimeout(). Because $nextTick() fires after an update in the virtual DOM, not in the real DOM. The code that I use now, that works, looks like this:
setTimeout( () => {
// set it to zero, so that it animates
this.menuStyles = { '--int-menu-height': 0 }
}, 1);
I checked a lot of endless scroll scripts and was not happy with any of them.
What I want to have is in general this:
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
//same HTML document starts again without reloading
}
};
How is it possible to code this?
You can use an IntersectionObserver to get notified when your view is reaching the end to continue loading more content i.e. infinite scroll, what you want to achieve.
let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
let observer = new IntersectionObserver(callback, options);
let target = document.querySelector('#listItem');
observer.observe(target);
To get the complete guide, check out the very detailed documentation
ONLY If you are using React JS or planning to use it and your concern is performance
You can take a look on this react-window library so it will efficiently only render the ones on the window view:
https://codesandbox.io/s/x70ly749rq - if you inspect element the list on this sandbox, you will see that the previous set of list are remove from the dom making your site not slow despite of so much data that you previously loaded.
It goes hand in hand with react-window-infinite-loader
Core question
Suppose you have a DOM Element e, which is disconnected from current document, i.e.
document.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_DISCONNECTED
is true.
How to tap into an event that triggers at the moment (or next tick, it doesn't matter) this node gets connected, i.e.
document.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_DISCONNECTED
starts returning false?
Higher context
I have an angular component whose ngAfterViewInit will compute some values given the current element style (actual dimensions, paddings, margins and so on).
But angular is free to call ngAfterViewInit after its descendants have been created, but not necessarilly connected to its parent. An element disconnected from the document has essentially no style, so every dimension is - reasonably - zero.
I want to postpone this computation until this element gets finally connected to the whole document, CSS rules get applied, HTML/CSS is reflowed and the element's style settles.
The MutationObserver failure
I've tried to use a MutationObserver, this way:
this._ngZone.runOutsideAngular(() => {
new MutationObserver(mutations => {
console.log('got mutations', mutations);
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node === this.element) {
console.log('GOT CONNECTED');
}
})
});
}).observe(document, { childList: true });
});
Not even the first "got mutations" has shown up in console. Maybe I've got something wrong. I'm new to this MutationObserver thing.
I have the following event handler. Due to their frequency with which it occurs, it will be pretty "greedy". onMouseMove, it will draw a "lasso" around the selected area, then perform a throttled geospatial search around the area.
onMouseMove = ({ pageX, pageY }: MouseEvent) => {
const lasso = this.getLasso(pageX, pageY);
this.ctx.setLineDash([6]);
this.ctx.strokeStyle = '#ffffff';
this.ctx.strokeRect(lasso.left, lasso.top, lasso.width, lasso.height);
// this is the problem area.
this.props.onSelection(this.search(lasso));
}
However, I find the callstack is largely just about running this callback. The action to update the "lasso" is only 2ms, but the onSelection callback takes ~40ms. They're both grouped under Event (mousemove) in performance. I think this is making the animation of the lasso look very choppy.
Would there be a way to run this callback independently/asynchronously from the mouse event handler? It doesn't so much matter to the mousemove event when it completes.
Use setTimeout with a delay of 0. It will be added to the JavaScript scheduler queue, and run after the current callstack finishes.
In situ, this becomes:
onMouseMove = ({ pageX, pageY }: MouseEvent) => {
const lasso = this.getLasso(pageX, pageY);
this.ctx.setLineDash([6]);
this.ctx.strokeStyle = '#ffffff';
this.ctx.strokeRect(lasso.left, lasso.top, lasso.width, lasso.height);
// this is the problem area.
setTimeout(function () { this.props.onSelection(this.search(lasso));});
}
Since 0 is the default, you may omit it, as shown in the linked docs:
If this parameter is omitted, a value of 0 is used, meaning execute "immediately", or more accurately, as soon as possible. Note that in either case, the actual delay may be longer than intended; see Reasons for delays longer than specified below.
So I've got a scroll event. It does a load of stuff to work out whether something should be moved on the page. When you scroll down, it fires off. If you wheel down, drag, it fires of bazillions and bazillions of times. As you'd expect, perhaps. Here's some simple dummy code to represent the sequence of events.
function scroller() {
// 1. A really expensive calculation that depends on the scroll position
// 2. Another expensive calculation to work out where should be now
// 3. Stop current animations
// 4. Animate an object to new position based on 1 and 2
}
$(window).on('resize' scroller);
Don't get me wrong, it's usually accurate so there isn't so much a concurrency issue. My animations inside the event call .stop() (as part #3) so the latest version is always* the right one but it's eating up a lot of CPU. I'd like to be a responsible developer here, not expecting every user to have a quad core i7.
So to my question... Can I kill off previous calls to my method from a particular event handler? Is there any way I can interfere with this stack of queued/parallel-running "processes" so that when a new one is added to the stack, the old ones are terminated instantly? I'm sure there's a concurrency-minded way of wording this but I can't think of it.
*At least I think that's the case - if the calculations took longer in an earlier run, their animation could be the last one to be called and could cock up the entire run! Hmm. I hadn't thought about that before thinking about it here. Another reason to stop the previous iterations immediately!
You can ensure the event is fired a maximum of once per x milliseconds. E.g.:
(function ($) {
$.fn.delayEvent = function (event, callback, ms) {
var whichjQuery = parseFloat($().jquery, 10)
, bindMethod = whichjQuery > 1.7 ? "on" : "bind"
, timer = 0;
$(this)[bindMethod](event, function (event) {
clearTimeout (timer);
timer = setTimeout($.proxy(callback, this, event), ms);
});
return $(this);
};
})(jQuery);
$(window).delayEvent("resize", scroller, 1000);
Minimalistic demo: http://jsfiddle.net/karim79/z2Qhz/6/