Why is my Scroll event being called twice here? - javascript

InfiniteScrollFactory:
const scrollingSocial = (e) => {
console.log('scrollingSocial');
// e.stopPropagation();
const reachedBottom = () => socialCol.scrollHeight - socialCol.scrollTop === socialCol.offsetHeight;
const loadMoreItems = () => {
console.log('[ Fire Once ] loadMoreItems...');
$rootScope.$emit("socialmedia.stream.load");
};
if (reachedBottom()) loadMoreItems();
};
const wireSocialScroll = (list) => {
console.log('wireSocialScroll called!');
if (notEmpty(list)) {
socialCol.addEventListener('scroll', scrollingSocial);
}
};
const attachScrollListener = (location, col, list) => {
console.log('attachScrollListener');
console.log(' location', location);
switch (location) {
// case 'tagsPanel' : tagsCol = col; wireTagsScroll(list); break;
// case 'feedPanel' : feedCol = col; wireFeedScroll(list); break;
case 'socialMedia' : socialCol = col; wireSocialScroll(list); break;
}
};
My scrollingSocial function gets called once when I scroll down the mouse once. It takes about 45 'scrolls' to finally trigger my loadMoreItems function. However then it gets called twice. And I see the scroll the 46th time even though I did not scroll a 46th time.
socialMediaDirective:
const getColHeight = (tags) => {
if (notEmpty(tags)) InfiniteScrollFactory.attachScrollListener('socialMedia', socialCol, tags);
};

Scrolling and it's event triggers can be a bit finicky.
Just using this code:
$(document).on('scroll', () => console.log('scroll'));
I get multiple scrolls each time I tick my mouse wheel, no matter how carefully I do so.
It's probably the same sort of issue with what you have. What you'll want to do is simply add a boolean that keeps track of if you've called loadMoreItems, use that boolean to keep it from calling it again.
let loadingMoreItems = false;
const scrollingSocial = (e) => {
console.log('scrollingSocial');
// e.stopPropagation();
const reachedBottom = () => socialCol.scrollHeight - socialCol.scrollTop === socialCol.offsetHeight;
const loadMoreItems = () => {
console.log('[ Fire Once ] loadMoreItems...');
$rootScope.$emit("socialmedia.stream.load");
};
if (!loadingMoreItems && reachedBottom()) {
loadingMoreItems = true;
loadMoreItems();
}
};
Then, at an appropriate time (or times), change that boolean back to false to allow it to call again (scrolling back up, more items loaded, reachedBottom() resulting in false once, etc).

Related

Trying to use p5 to change states after pressing button, but nothing happens after clicking the button

I am trying to create a function that starts out with a report button and, if you click it, the page will redirect you to where you can type some words into the text box and send or you have a cancel button. If you type in a comment and press the send button then whatever you typed will show up. However, if you click the cancel button it will return you to a page that just has the report button.
While I am certain my code so far is incorrect, my main question is why doesn't anything happen after I click the report button? I based my code off this post.
Here is my code:
let reportButton;
let cancelButton;
let reporting;
let button;
//let input;
const scenes = {
report: () => {
cancelButton?.remove();
// initialize idle state
reporting = false;
reportButton = createButton("Report");
reportButton.position(5, 5);
reportButton.mousePressed = () => {
scenes.reportComment();
};
draw = () => {
clear();
};
},
reportComment: () => {
reportButton?.remove();
reporting = true;
cancelButton = createButton("Cancel");
cancelButton.position(5, 5);
cancelButton.mousePressed = () => {
scenes.report();
};
draw = () => {
clear();
input = createInput();
input.position(20, 65);
button = createButton('Submit');
button.position(input.x + input.width, 65);
button.mousePressed(sendReport);
question = createElement('h2', 'Report');
question.position(20, 5);
textAlign(CENTER);
textSize(50);
};
},
};
function sendReport() {
const customQ = input.value();
question.html(customQ);
input.value('');
}
function setup() {
createCanvas(500, 100);
textSize(20);
}
function draw() {
scenes.report();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Your code (overwriting p5's mousePressed method with a new, unrelated function):
reportButton.mousePressed = () => {
scenes.reportComment();
};
Correct code (calling p5's mousePressed method, passing in a callback that represents the handler to trigger on press):
reportButton.mousePressed(() => {
scenes.reportComment();
});
It's a subtle difference, but a close look at the example in the createButton docs shows the way.
Manipulating p5.js objects usually involves method calls rather than setting object properties with =, in case this helps you "smell" the pattern easier in the future. You may have been misled by the look of draw = () => {} and mousePressed = () => {}, but these overwrite window variables, replacing function draw() and function mousePressed() to change the main loop that p5 runs and the global mouse press handler, respectively. That's a different case.
A good debugging strategy is to eliminate all unneeded code until all that's left is the following minimal reproduction:
function setup() {
const b = createButton("Report");
b.position(10, 10);
b.mousePressed = () => console.log("worked!");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
After removing all distractions having to do with scenes, it's much easier to figure out that mousePressed is being used incorrectly.
Your other handler, cancelButton.mousePressed, should also be adjusted.
After you fix your handlers, you'll probably notice that you can't type into your input. The reason is that it's being created many times per second because it's in the draw() loop:
draw = () => {
// repeat this code many times a second
clear();
input = createInput();
input.position(20, 65);
// ...
Instead, put your element-creation code outside draw() so it's executed one time. For each scene, that top-level code is basically setup() for that scene. draw() is generally for animation, moving objects around the screen, and occasionally creating and destroying objects as the animation requires. Your current app doesn't need any draw()s at the current time, so I'd remove them to avoid confusion.
clear() clears the canvas, not DOM elements, so it's also not necessary in your app yet. Ditto for textAlign(CENTER); and textSize(50);.
Putting it all together:
const scenes = {
report: () => {
const reportButton = createButton("Report");
reportButton.position(5, 5);
reportButton.mousePressed(() => {
reportButton.remove();
scenes.reportComment();
});
},
reportComment: () => {
const cleanup = () => {
input.remove();
cancelButton.remove();
submitButton.remove();
header.remove();
};
const transitionToReport = () => {
cleanup();
scenes.report();
};
const transitionToSendReport = () => {
const value = input.value();
cleanup();
scenes.sendReport(value);
};
const cancelButton = createButton("Cancel");
cancelButton.position(5, 5);
cancelButton.mousePressed(transitionToReport);
const input = createInput();
input.position(20, 65);
const submitButton = createButton("Submit");
submitButton.position(input.x + input.width, 65);
submitButton.mousePressed(transitionToSendReport);
const header = createElement("h2", "Report");
header.position(20, 5);
},
sendReport: customQ => {
const header = createElement("h2", `report: '${customQ}'`);
header.position(20, 5);
},
};
function setup() {
scenes.report();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Or keeping it to two scenes:
const scenes = {
report: () => {
const reportButton = createButton("Report");
reportButton.position(5, 5);
reportButton.mousePressed(() => {
reportButton.remove();
scenes.reportComment();
});
},
reportComment: () => {
const transitionToReport = () => {
input.remove();
cancelButton.remove();
submitButton.remove();
header.remove();
scenes.report();
};
const cancelButton = createButton("Cancel");
cancelButton.position(5, 5);
cancelButton.mousePressed(transitionToReport);
const input = createInput();
input.position(20, 65);
const submitButton = createButton("Submit");
submitButton.position(input.x + input.width, 65);
const sendReport = () =>
header.elt.textContent = input.value();
submitButton.mousePressed(sendReport);
const header = createElement("h2", "Report");
header.position(20, 5);
},
};
function setup() {
scenes.report();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Note that I'm locally-scoping all variables to each scene. Each scene is responsible for cleaning up its own elements when transitioning to another scene. This helps manage complexity as the app grows.
UX isn't great since the user should be able to press Enter to transition to the next scene, and there should probably be some validation that the input has some content in it, but that's out of scope for this answer.

events with addEventListener

I'm tried to something like : Catch all events in my addEventListeners and then call my function calculateBill. I have a problem my events are lost. It is possible to pass the parameters for each addeventlister separately. I try to do it since few hours and i have no idea what's happening here.
const billInput = document.querySelector('#bill');
const percentageButton = document.querySelectorAll('.tip__values--item');
const numberOfPeople = document.querySelector('#people');
const tipAmount = document.querySelector('.result__amount--price');
const totalBill = document.querySelector('.result__total--price');
const reset = document.querySelector('button');
const catchBill = (e) => {
return e.target.value;
};
const catchPeople = (e) => {
return e.target.value;
};
const handleButtons = (e) => {
return e.target.textContent;
};
const calculateBill = (catchBill, catchPeople, handleButtons) => {
console.log(
'catchBill:',
catchBill,
'catchPeople:',
catchPeople,
'handleButtons:',
handleButtons
);
};
billInput.addEventListener('keyup', function (e) {
calculateBill(catchBill(e), catchPeople, handleButtons);
});
numberOfPeople.addEventListener('keyup', function (e) {
calculateBill(catchBill, catchPeople(e), handleButtons);
});
percentageButton.forEach((btn) => {
btn.addEventListener('click', function (e) {
calculateBill(catchBill, catchPeople, handleButtons(e));
});
});
I know that i can do this FrontedMentor challenge in other way but I'd like to know is this possible to do it that way. I know that problem is with parameters that i call in addeventlistener. How can i get my parameters in my calculateBill function with all events?
You'll need to cache the result of each event somewhere so that you can retrieve them later, or retrieve the value in every input each time any event takes place. It looks like calculateBill expects arguments to be strings, not functions, so passing catchBill, catchPeople, or handleButtons (which are functions) to it doesn't make sense. Consider something like:
// assign default values here if you want
let billValue;
let numPeople;
let percValue;
const handleBillChange = (e) => {
billValue = e.target.valueAsNumber;
calculateBill();
};
const handlePeopleChange = (e) => {
numPeople = e.target.valueAsNumber;
calculateBill();
};
const handlePercentageChange = (e) => {
percValue = e.target.textContent;
calculateBill();
};
billInput.addEventListener('keyup', handleBillChange);
numberOfPeople.addEventListener('keyup', handlePeopleChange);
billInput.addEventListener('keyup', handleBillChange);
// This is a NodeList, not a button. Consider naming the variable more precisely.
// percentageButtons, perhaps?
percentageButton.forEach((btn) => {
btn.addEventListener('click', handlePercentageChange);
});
While it'd be technically possible to store the previous events instead, and then do something like
billInput.addEventListener('keyup', function (e) {
calculateBill(catchBill(e), catchPeople(previousPeopleEvent), handleButtons(previousButtonEvent));
});
for all the inputs and buttons, that'd be extremely strange. Better to store just the values.

Event listeners don't get re-attached to my HTML elements when using ES6 without re-feshing the page

I am printing a simple string to the screen. When clicking on one of its letters, is should be removed from wherever it is the string and added at the end. After I click on one letter and the new string is getting printed to the page, the letters don't preserve their event listeners. This is the JS code and here is all the code https://codesandbox.io/s/reverse-array-forked-fvclg?file=/src/index.js:0-1316:
const appBox = document.getElementById("app");
const convertString = (string) => {
let stringToArray = string.split("");
return stringToArray;
};
let stringToArray = convertString("Hello world!");
const printArrayToPage = (string) => {
string.forEach((element) => {
const textBox = document.createElement("div");
if (element !== " ") {
textBox.classList.add("letter");
} else {
textBox.classList.add("emptySpace");
}
const text = document.createTextNode(element);
textBox.appendChild(text);
appBox.appendChild(textBox);
});
};
window.onload = printArrayToPage(stringToArray);
const moveLetter = (event) => {
const targetLetter = event.target;
const letterToRemove = targetLetter.innerHTML;
targetLetter.classList.add("invisible");
if (stringToArray.includes(letterToRemove)) {
const index = stringToArray.indexOf(letterToRemove);
stringToArray.splice(index, 1);
}
stringToArray.push(letterToRemove);
appBox.innerHTML = "";
printArrayToPage(stringToArray);
};
const allLetters = document.querySelectorAll(".letter");
allLetters.forEach((element) => element.addEventListener("click", moveLetter));
const allSpaces = document.querySelectorAll(".emptySpace");
allSpaces.forEach((element) => element.addEventListener("click", moveLetter));
I tried moving the even assignments (this block)
const allLetters = document.querySelectorAll(".letter");
allLetters.forEach((element) => element.addEventListener("click", moveLetter));
const allSpaces = document.querySelectorAll(".emptySpace");
allSpaces.forEach((element) => element.addEventListener("click", moveLetter));
inside printArrayToPage function but because I am using ES6 syntax, I can't use a function before its definition. If I change to functions created used the function keyword, everything works as expected. How can I fix this issue using ES6 so that after I click on a letter and the divs get re-added to the page, event listeners are re-assigned?
If you want to move the letter node to the end, you can use the Node.parentNode property, and append the child node to the end. You don't need to create the nodes every time an item is clicked:
const moveLetter = (event) => {
const targetLetter = event.target;
const parent = targetLetter.parentNode;
parent.appendChild(targetLetter); // move the element to the end
};
See: https://codesandbox.io/s/reverse-array-forked-lf333?file=/src/index.js
Use event delegation. Here's a simple example snippet:
// adding an event listener to the document
document.addEventListener("click", evt => {
console.clear();
const origin = evt.target;
// ^ where did the event originated?
if (origin.closest("#bttn1")) {
console.log("you clicked button#bttn1");
} else if (origin.closest("#bttn2")) {
console.log("you clicked button#bttn2")
}
});
createElement("h3", "header", "Wait 2 seconds...");
// creating buttons after a while. The earlier created
// event listener will detect clicks for the new buttons
setTimeout( () => {
createElement("button", "bttn1", "click me");
createElement("button", "bttn2", "click me too");
}, 2000);
function createElement(nodetype, id, text) {
document.body.appendChild(
Object.assign(document.createElement(nodetype), {id, textContent: text})
);
}
body {
font: normal 12px/15px verdana, arial;
margin: 2rem;
}
button {
margin: 0 0.6rem 0 0;
}

React Hooks - set last value?

I'm trying to write basic hooks to get currentScroll, lastScroll, scrollSpeed while scrolling.
function useDocScroll() {
const isClient = typeof window === "object"
function getScroll() {
return isClient
? window.pageYOffset || document.documentElement.scrollTop
: undefined
}
const [docScroll, setDocScroll] = useState(getScroll)
const [lastScroll, setLastScroll] = useState(null)
const [scrollSpeed, setScrollSpeed] = useState(Math.abs(docScroll - lastScroll))
useEffect(() => {
if (!isClient) {
return false
}
function handleScroll() {
setDocScroll(getScroll())
setLastScroll(getScroll()) // <-- why is this working?
// setLastScroll(docScroll) // <-- why is this not working?
setScrollSpeed(Math.abs(docScroll - lastScroll)) // <-- why is this not working?
}
window.addEventListener("scroll", handleScroll)
}, [])
return [docScroll, lastScroll, scrollSpeed]
}
It seems like when I do setLastScroll(getScroll()), it saves the last scroll value well.
But I don't understand because when handleScroll() is firing, shouldn't getScroll() value stays the same? I don't get it why setDocScroll(getScroll()) and setLastScroll(getScroll()) have different value.
Also, I thought I could do setLastScroll(docScroll), meaning 'set lastScroll value with current docScroll value', but it just prints '0' while docScroll value changes.
Why is this? I want to understand better.
+) And I can't get scrollSpeed which is calculated by docScroll and lastScroll, but I don't know how to get those values.
I think why your code not working is because of following two reasons:
using docScroll directly after setDocScroll won't work because setState is asynchronous task. There is no guarantee that docScroll is updated before executing next statement
you are getting 0 because of scrolling happening inside some particular element (probably). Since document.documentElement points to html element and there is no scrolling inside it. So you receive 0
Solution:
You don't need multiple useStates. Since scroll events emits too frequently, i think its good idea to use useReducer to reduce number of renders. It is important understand where scrolling happening whether on root level or inside some element.
For below solution i proposed:
if scroll happening on root level (html element) no need to pass element to useDocScroll. If scroll happening inside particular element, you need to pass element reference.
const initState = {
current: 0,
last: 0,
speed: 0,
};
function reducer(state, action) {
switch (action.type) {
case "update":
return {
last: state.current,
current: action.payload,
speed: Math.abs(action.payload - state.current) || 0,
};
default:
return state;
}
}
const isClient = () => typeof window === "object";
function useDocScroll(element = document.documentElement) {
const [{ current, last, speed }, dispatch] = useReducer(reducer, initState);
function getScroll() {
return isClient() ? element.scrollTop : 0;
}
function handleScroll() {
dispatch({ type: "update", payload: getScroll() });
}
useEffect(() => {
if (!isClient()) {
return false;
}
element.addEventListener("scroll", handleScroll);
return () => element.removeEventListener("scroll", handleScroll);
}, []);
return [current, last, speed];
}
Example:
if scroll happening inside window
const {current, last, speed} = useDocScroll()
if scroll happening in particular element
const {current, last, speed} = useDocScroll(document.getElementById("main"))
It not working due to closures:
useEffect(() => {
if (!isClient) {
return false;
}
function handleScroll() {
setDocScroll(getScroll());
// On calling handleScroll the values docScroll & lastScroll
// are always will be of the first mount,
// the listener "remembers" (closures) such values and never gets updated
setLastScroll(docScroll);
setScrollSpeed(Math.abs(docScroll - lastScroll));
// v Gets updated on every call
setLastScroll(getScroll());
}
// v Here you assigning a callback which closes upon the lexical scope of
// docScroll and lastScroll
window.addEventListener('scroll', handleScroll);
}, []);
To fix it, a possible solution can be a combination of a reference (useRef) and functional setState.
For example:
setScrollSpeed(lastScroll => Math.abs(getScroll() - lastScroll))
Not sure why lastScrolled needed:
function useDocScroll() {
const [docScroll, setDocScroll] = useState(getScroll());
const lastScrollRef = useRef(null);
const [scrollSpeed, setScrollSpeed] = useState();
useEffect(() => {
if (!isClient) {
return false;
}
function handleScroll() {
const lastScroll = lastScrollRef.current;
const curr = getScroll();
setScrollSpeed(Math.abs(getScroll() - (lastScroll || 0)));
setDocScroll(curr);
// Update last
lastScrollRef.current = curr;
}
window.addEventListener('scroll', handleScroll);
}, []);
return [docScroll, lastScrollRef.current, scrollSpeed];
}

Can't remove event listener for windows object

I am having a lot of trouble trying to remove an event listener.
I have created a website that relies on JavaScript quite heavily. When you navigate on the website it is basically loading in elements dynamically without a page refresh with template literals.
I have to sometimes load in content and add infinite scroll but also be able to remove that event again.
This is the code I use to handle scroll events:
var start = 30;
var active = true;
function yHandler(elem)
{
var oHeight = selectElems('content_main', 'i').offsetHeight;
var yOffset = window.pageYOffset;
var hLimit = yOffset + window.innerHeight;
if (hLimit >= oHeight - 500 && active === true)
{
active = false;
new requestContent({
page: GET.page,
type: returnContentType(GET.page),
scroll: true,
start: start
}, (results) => {
if(results){
setTimeout(()=>{
active = true;
start = start + 30;;
}, 400);
new ContentActive();
}
});
}
}
var scrollRoute =
{
contentScroll: () =>{
yHandler();
}
};
var scrollHandler = function(options)
{
var func = options.func.name;
var funcOptions = options.func.options;
var elem = options.elem;
var flag = options.flag;
this.events = () => {
addEvent(elem, 'scroll', ()=>{
scrollRoute[func](elem, funcOptions);
}, flag);
}
this.clear = () => {
elem.removeEventListener('scroll', scrollRoute[func](), flag);
}
}
I am using this function to set events
function addEvent(obj, type, fn, flag = false) {
if (obj.addEventListener) {
obj.addEventListener(type, fn, flag);
} else if (obj.attachEvent) {
obj["e" + type + fn] = fn;
obj[type + fn] = function () {
obj["e" + type + fn](window.event);
};
obj.attachEvent("on" + type, obj[type + fn]);
} else {
obj["on" + type] = obj["e" + type + fn];
}
}
I am calling this code from whatever code when I need to set the infinite scroll event
new scrollHandler({
func: {
'name':'contentScroll',
},
elem: window,
flag: true,
}).events();
I am calling this code from whatever code when I need to remove the infinite scroll event but without any luck
new scrollHandler({
func: {
'name':'contentScroll',
},
elem: window,
flag: true,
}).clear();
How do I successfully remove the event listener? I can't just name the instances, that will be so messy in the long run when setting and removing the scroll events from various different places.
Two problems:
You have to pass the same function to removeEventListener as you passed to addEventListener. (Similarly, you have to pass the same function to detachEvent as you passed to attachEvent using Microsoft's proprietary stuff — but unless you really have to support IE8 and earlier, you can ditch all that.) Your code isn't doing that.
When trying to remove the handler, you're calling scrollRoute[func]() and passing its return value into removeEventListener. As far as I can tell, that's passing undefined into removeEventListener, which won't do anything useful.
Here's the code I'm referring to above:
this.events = () => {
addEvent(elem, 'scroll', ()=>{ // *** Arrow function you don't
scrollRoute[func](elem, funcOptions); // *** save anywhere
}, flag); // ***
}
this.clear = () => {
elem.removeEventListener('scroll', scrollRoute[func](), flag);
// Calling rather than passing func −−−^^^^^^^^^^^^^^^^^^^
}
Notice that the function you're passing addEvent (which will pass it to addEventListener) is an anonymous arrow function you don't save anywhere, but the function you're passing removeEventListener is the result of calling scrollRoute[func]().
You'll need to keep a reference to the function you pass addEvent and then pass that same function to a function that will undo what addEvent did (removeEvent, perhaps?). Or, again, ditch all that, don't support IE8, and use addEventListener directly.
So for instance:
var scrollHandler = function(options) {
var func = options.func.name;
var funcOptions = options.func.options;
var elem = options.elem;
var flag = options.flag;
var handler = () => {
scrollRoute[func](elem, funcOptions);
};
this.events = () => {
elem.addEventListener('scroll', handler, flag);
};
this.clear = () => {
elem.removeEventListener('scroll', handler, flag);
};
};
(Notice I added a couple of missing semicolons, since you seem to be using them elsewhere, and consistent curly brace positioning.)
Or using more features of ES2015 (since you're using arrow functions already):
var scrollHandler = function(options) {
const {elem, flag, func: {name, options}} = options;
const handler = () => {
scrollRoute[name](elem, options);
};
this.events = () => {
elem.addEventListener('scroll', handler, flag);
};
this.clear = () => {
elem.removeEventListener('scroll', handler, flag);
};
};

Categories