How and when to unobserve an Intersection Observer - intersection-observer

I am using a few Intersection Observers to simply change some classes when certain elements are in the viewport. While it's working, I thought it would be best practice to unobserve these when the elements are not in the viewport, with regard to performance and memory build-up. But, inspecting JS events shows that there are only events fired when the elements are in the viewport, and honestly I'm not sure if that alone is just fine.
Here is the working code I have, but note that if I uncomment observerInfo.unobserve(infoStatement); it no longer works, which is expected, but I am not sure how to implement that unobserve.
const infoStatement = document.querySelector('.statement-container');
const bodyInfo = document.querySelector('body');
const observerInfo = new IntersectionObserver(
function callBackFunction(entries) {
const [entry] = entries;
if (entry.isIntersecting) {
bodyInfo.classList.add('background-dark');
} else {
bodyInfo.classList.remove('background-dark');
//observerInfo.unobserve(infoStatement);
}
},
{ rootMargin: "-10%", }
)
observerInfo.observe(infoStatement);
FIDDLE is here
I also tried a toggle in the if/else statement, but that only added the class and did not remove it:
if (entry.isIntersecting) {
bodyInfo.classList.toggle('background-dark');
} else {
observerInfo.unobserve(infoStatement);
}
How can I achieve this?

Related

How do I stop the page from rendering every time I scroll?

I think that I am accidentally refreshing this every time a scroll happens, which is maybe rendering the page on every change.
I think that the issue is that the window.addeventlistener gets called the first time and never unmounts, and then the second time animator is called, it mounts and unmounts it's own event listener, leaving the first in place.
I know useEffect is rendering every time scrollPosition changes, my goal was to stop scrollPosition from changing essentially after the first scroll.
The problem here (aside from any performance issues) is that there are components that have animations, which reset every time the scrollPosition changes.
my question is: How do I stop the rendering after the first time the useEffect runs on the scrollPosition change?
function App() {
const [scrollPosition, setScrollPosition] = useState(0);
const [animating, setAnimating] = useState(false);
const handleScroll = () => {
const position = window.scrollY;
setScrollPosition(position);
};
function animator() {
if (!animating) {
window.addEventListener('scroll', handleScroll, { passive: true });
}
return () => {
window.removeEventListener('scroll', handleScroll);
};
}
useEffect(() => {
animator();
}, [scrollPosition]);
}
I have tried moving the event listener to basically every possible other position, creating new functions to handle it, and used the animating boolean to try and prevent scrollPosition from changing, but alas it has not worked for me yet.
Try using the useMemo hook. This video will explain how you can implement it in your code [https://youtu.be/THL1OPn72vo]

Why can't I use getBoundingClientRect()?

I am trying to write a protractor test to check that a page's footer is at the bottom of the page.
I've looked at sources like these to help me:
How to get an element's top position relative to the browser's viewport?
https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
I want to use the getBoundingClientRect function discussed in these sources to get the footer's position, but I am getting an error saying: footer.getBoundingClientRect is not a function
Here is the relevant portion of my code:
footer = element(by.css('lib-footer > mat-toolbar'));
const viewportOffset = footer.getBoundingClientRect();
const bottom = viewportOffset.bottom;
browser.driver.manage().window().getSize().then((size) => {
expect(bottom).toBeGreaterThan(size.height - 50);
});
I know that the footer element is defined because I have already run other tests on it to check things like its color and size.
Why am I being told that getBoundingClientRect is not a function?
This error occurred because you are trying to call the getBoundingClientRect() function on the ElementFinder type, but the ElementFinder does not have and can not call the getBoundingClientRect function.
The getBoundingClientRect function could be called from the Element object, it is the most general base class from which all objects in a Document inherit. It only has methods and properties common to all kinds of elements. More specific classes inherit from Element.
So in your case, you should use the executeScript to execute JavaScript in the context of the currently selected frame or window.
SOLUTION:
// Promise based
const footer = $('lib-footer > mat-toolbar'); // or element(by.css(''));
let viewportOffset = '';
browser.executeScript('return arguments[0].getBoundingClientRect()', footer).then(value => {
viewportOffset = value;
});
const bottom = viewportOffset.bottom;
browser.driver.manage().window().getSize().then((size) => {
expect(bottom).toBeGreaterThan(size.height - 50);
});
// async/await - Highly recommended. Don't forget to add `async` before the function to use `await`
const footer = $('lib-footer > mat-toolbar');
const viewportOffset = await browser.executeScript('return arguments[0].getBoundingClientRect()', footer);
const bottom = viewportOffset.bottom;
const windowSize = await browser.driver.manage().window().getSize();
expect(bottom).toBeGreaterThan(windowSize.height - 50);

Extending native HTML element in Angular 6

I have recently created a native web component which is working well in all browsers. I moved this web component into an Angular 6 application and all works as expected. I then tried to extend a native HTML element which again worked perfectly except when I brought it into my Angular 6 application.
Using the examples from Mozilla I will try and illustrate my issue. Using the following trying to extend a native 'p' element:
// Create a class for the element
class WordCount extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// count words in element's parent element
var wcParent = this.parentNode;
function countWords(node){
var text = node.innerText || node.textContent
return text.split(/\s+/g).length;
}
var count = 'Words: ' + countWords(wcParent);
// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});
// Create text node and add word count to it
var text = document.createElement('span');
text.textContent = count;
// Append it to the shadow root
shadow.appendChild(text);
// Update count when element content changes
setInterval(function() {
var count = 'Words: ' + countWords(wcParent);
text.textContent = count;
}, 200)
}
}
// Define the new element
customElements.define('word-count', WordCount, { extends: 'p' });
<p is="word-count">This is some text</p>
By taking that same code and putting it into an Angular 6 application, the component never runs. I put console log statements in the constructor and connectedCallback methods and they never trigger. If I remove the {extends: 'p'} object and change the extends HTMLParagraphElement and make it an extend HTMLElement to be an autonomous custom element everything works beautifully. Am I doing something wrong or does Angular 6 not support the customized built-in element extension?
I assume the reason is the way that Angular creates those customized built-in elements when parsing component templates - it probably does not know how to properly do that. Odds are it considers is a regular attribute which is fine to add after creation of the element (which it isn't).
First creating the element and then adding the is-attribute will unfortunately not upgrade the element.
See below example: div#d has a non-working example of that customized input.
customElements.define('my-input', class extends HTMLInputElement {
connectedCallback() {
this.value = this.parentNode.id
this.parentNode.classList.add('connected')
}
}, {
extends: 'input'
})
document.addEventListener('DOMContentLoaded', () => {
b.innerHTML = `<input type="text" is="my-input">`
let el = document.createElement('input', {
is: 'my-input'
})
el.type = 'text'
c.appendChild(el)
// will not work:
let el2 = document.createElement('input')
el2.setAttribute('is', 'my-input')
el2.type = 'text'
d.appendChild(el2)
})
div {
border: 3px dotted #999;
padding: 10px;
}
div::before {
content: "#"attr(id)" ";
}
.connected {
background-color: lime;
}
<div id="a"><input type="text" is="my-input"></div>
<div id="b"></div>
<div id="c"></div>
<div id="d"></div>
So to get it to work with Angular, hook into the lifecycle of your Angular component (e.g. onInit() callback) and pick a working way to create your element there.

AS3 Passing Variable Parameters to a generic Function Menu / SubItems

I'm no code genius, but a fan of action script.
Can you help me on this:
I have a function that depending on the object selected, will call event listeners to a set of 'sub-items' that are already on stage (I want to reuse this subitems with changed parameters upon click, instead of creating several instances and several code).
So for each selected 'case' I have to pass diferent variables to those 'sub-items', like this:
function fooMenu(event:MouseEvent):void {
switch (event.currentTarget.name)
{
case "btUa1" :
trace(event.currentTarget.name);
// a bunch of code goes here
//(just cleaned to easy the view)
/*
HELP HERE <--
here is a way to pass the variables to those subitems
*/
break;
}
}
function fooSub(event:MouseEvent):void
{
trace(event.target.data);
trace(event.currentTarget.name);
// HELP PLEASE <-> How can I access the variables that I need here ?
}
btUa1.addEventListener(MouseEvent.CLICK, fooMenu);
btUa2.addEventListener(MouseEvent.CLICK, fooMenu);
btTextos.addEventListener(MouseEvent.CLICK, fooSub);
btLegislacao.addEventListener(MouseEvent.CLICK, fooSub);
Anyone to help me please?
Thank very much in advance. :)
(I'm not sure I got your question right, and I haven't developed in AS3 for a while.)
If you want to simply create function with parameters which will be called upon a click (or other event) you can simply use this:
btUa1.addEventListener(MouseEvent.CLICK, function() {
fooMenu(parameters);
});
btUa2.addEventListener(MouseEvent.CLICK, function() {
fooMenu(other_parameters)
}):
public function fooMenu(...rest):void {
for(var i:uint = 0; i < rest.length; i++)
{
// creating elements
}
}
If you want to call event listeners assigned to something else you can use DispatchEvent
btnTextos.dispatchEvent(new MouseEvent(MouseEvent.CLICK))
Remember, you can't use btTextos.addEventListener(MouseEvent.CLICK, carregaConteudo("jocasta")); because the 2nd parameter you pass while adding Eventlistener will be considered as function itself - there are two proper ways to use addEventListener:
1:
function doSomething(event:MouseEvent):void
{
// function code
}
element.addEventListener(MouseEvent.CLICK, doSomething); //notice no brackets
2:
element.addEventListener(MouseEvent.CLICK, function() { // function code });
So:
function fooSub(event:MouseEvent, bla:String):void
{
trace(event.currentTarget.name+" - "+bla);
// bla would be a clip name.
}
codebtTextos.addEventListener(MouseEvent.CLICK, function(e:MouseEvent) { fooSub(e, "jocasta") } );
Or try something like this if you want content to be dynamically generated:
btUa1.addEventListener(MouseEvent.CLICK, function() {
createMenu(1);
});
btUa2.addEventListener(MouseEvent.CLICK, function() {
createMenu(2);
});
function createMenu(id):void
{
// Switching submenu elements
switch (id)
{
case 1:
createSubmenu([myFunc1, myFunc2, myFunc3]); // dynamically creating submenus in case you need more of them than u already have
break;
case 2:
createSubmenu([myFunc4, myFunc5, myFunc6, myFunc7]);
break;
default:
[ and so on ..]
}
}
function createSubmenu(...rest):void {
for (var i:uint = 0; i < rest.length; i++)
{
var mc:SubItem = new SubItem(); // Subitem should be an MovieClip in library exported for ActionScript
mc.addEventListener(MouseEvent.CLICK, rest[i] as function)
mc.x = i * 100;
mc.y = 0;
this.addChild(mc);
}
}
Your question is rather vague; what "variables" do you want to "pass"? And what do you mean by "passing the variable to a sub item"? Usually "passing" means invoking a function.
If you can be more specific on what exactly your trying to do that would be helpful. In the meantime, here are three things that may get at what you want:
You can get any member of any object using bracket notation.
var mc:MovieClip = someMovieClip;
var xVal:Number = mc.x; // The obvious way
xVal = mc["x"]; // This works too
var propName:String = "x";
xVal = mc[propName] ; // So does this.
You can refer to functions using variables
function echo(message:String):void {
trace(message);
}
echo("Hello"); // The normal way
var f:Function = echo;
f("Hello"); // This also works
You can call a function with all the arguments in an array using function.apply
// Extending the above example...
var fArgs:Array = ["Hello"];
f.apply(fArgs); // This does the same thing
Between these three things (and the rest parameter noted by another poster) you can write some very flexible code. Dynamic code comes at a performance cost for sure, but as long as the frequency of calls is a few hundred times per second or less you'll never notice the difference.

Mootools reuse same function on multiple instances with something like the each function

I'm using the mootools wall plugin, Its working well in my application, however if I add multiple (image) walls it only works for one wall ::: My understanding of scripting is not good enough to add a each function or similar :::
I need to "bind" the code below to say 2 divs like this :::
My First wall:
<div id="viewport">
<div id="wall">
Second wall:
<div id="viewport">
<div id="wall_02">
Any assistance would be appreciated.
var wallIMAGES = new Wall( "wall", {
"width": scArray[1],
"height": scArray[1],
callOnUpdate: function(items){
items.each(function(e, i){
var el = wall[counterFluid];
if (el) {
var a = new Element("img[width="+scArray[1]+"][height="+scArray[1]+"][src={thumb}]".substitute(el));
a.inject(e.node).set("opacity", 0).fade("in");
e.node.store("tubeObject", el);
}
counterFluid++;
// Reset counter
if( counterFluid >= scArray[10].length) counterFluid = 0;
})
}
});
wallIMAGES.initWall();
Maybe something like this:
var my_wall_ids = ['wall', 'wall_02'];
var myWalls = [];
var baseWallOptions = {
"width": scArray[1],
"height": scArray[1],
callOnUpdate: function(items){
items.each(function(e, i){
var el = wall[counterFluid];
if (el) {
var a = new Element("img[width="+scArray[1]+"][height="+scArray[1]+"][src={thumb}]".substitute(el));
a.inject(e.node).set("opacity", 0).fade("in");
e.node.store("tubeObject", el);
}
counterFluid++;
// Reset counter
if( counterFluid >= scArray[10].length) {counterFluid = 0;}
}); // end items.each
}
}
for (var i=0;i<my_wall_ids.length;i++){
var id = my_wall_ids[i];
var wallOptions = baseWallOptions;
// if your customization was something like changing
// the height , but only on the 'wall' element
if (id === 'wall') {
wallOptions.height = 400;
}
myWalls[i] = new Wall(id, wallOptions);
myWalls[i].initWall();
}
If you read Wall's documentation, you'll notice that, just like most other classes, the first argument it takes is an element id.
So, if your initialization code states
new Wall("wall", { …
…then it will be applied to the element that has the id "wall".
You could simply duplicate your code and use one with "wall", the other one with "wall_02". However, that would be bad practice. Indeed, if you later wanted to change some options, you'd have to do it in two distinct blocks, and they would probably get out of sync.
If your only difference lies in the target id, and the options are to be shared, simply store the options object (second parameter to the Wall class) in a variable and use it twice! That is:
var wallOptions = { width: … };
var wallImages = new Wall("wall", wallOptions),
wallImages2 = new Wall("wall_02", wallOptions);
wallImages.initWall();
wallImages2.initWall();
It could be even better to embed initalization in a function, but this solution is probably the easiest if you simply want to have two Wall instances without learning much more about JS.