gatsby setPageSection makes page jump after images finish loading - javascript

This current piece of code:
<button onClick={() => setPageSection([p.slug+"/", s.slug])}>
makes my page jump to the start of the section when the images finish loading
In this case I have a grid of pictures that are correctly lazy loaded with an initial base64 lowres pre-render but everytime each one of the pictures finishes loading the page scrolls back to the top of the section.
This only happens when I use the setPageSection.
When I click a direct link to an anchor ("page/#section") of the page instead of using the above method of setting the page section the correct behavior happens (images continue lazy loading in the background and the pages doesn't snap to the top of the section everytime when each one finishes loading).
UPDATE:
The following looks like the culprit:
enter useEffect(() => {
if (typeof prevPageSection === 'undefined') return;
const scrollToSection = () => {
const el = document.getElementById(pageSection[1]);
if (scrollContainer.current && el) {
scrollContainer.current.scrollTo({ top: el.offsetTop });
}
};
if (pageSection[0] !== prevPageSection![0] && pageSection[0] !== pageContext.slug) {
navigate(locale + pageSection[0]);
scrollContainer.current?.addEventListener('load', scrollToSection, true);
}
scrollContainer.current?.removeEventListener('load', scrollToSection);
}, [pageSection, prevPageSection, locale, pageContext.slug]);
If I remove these event listeners the page stops jumping after the images load but the links get broken (they become useless when navigating to a section on a different page, without these event listeners one can only jump to the desired section if already on that page, otherwise the browser will navigate to the top of the page)

You have too many side effects in your useEffect hook. Each time one dependency (pageSection or prevPageSection or locale or pageContext.slug) is being changed, it triggers the hook, creating your unwanted effect:
const el = document.getElementById(pageSection[1]);
if (scrollContainer.current && el) {
scrollContainer.current.scrollTo({ top: el.offsetTop });
}
In addition, don't define a function inside the useEffect because it will cause a new scoped function creation each time the effect is triggered, overloading your code and application.
Moreover, don't point directly the DOM using document.getElementById(). It's a high-cost action for the browser and you can achieve the same effect using the useRef hook, you will have the exact same information using the .current property of the useRef hook, in the same way than you do the scrollContainer.current. Indeed, you are creating and manipulating a virtual DOM with React, to avoid pointing the DOM, while your code attacks the real DOM.
Be aware of the use of global objects such as window or document in Gatsby, they are not available during the SSR (Server-Side Rendering) so they may potentially break your code. To summarize, gatsby build compiles the code in your Node server, where there is no window or document because it's not even created yet. The code may work under gatsby develop because it's directly rendered by the browser.
To fix your issue, I would clean the useEffect, you can create as many effects as needed, and you can name them using this notation to make your code more readable:
useEffect(someNamedFunction(){
// your code
}, []).
To bypass it faster, you can create an auxiliary function that forces the page to scroll to certain element without involving the hooks:
const scrollToElement=(el)=>{
scrollContainer.current.scrollTo({ top: el.offsetTop });
}
<button onClick={(el)=> scrollToElement(el)}>

Related

DOM elements manipulation with javascript persists after reload or going to next page in pagination

I am trying to toggle a view between grid and list view mode on my frontend HTML page. I am able to do this fine with dom and HTML classes manipulation by toggling "display: none" between two containers. However, when I go to the next product page(through pagination) or when I reload the page, the default view is the one that appears and not the one that was last toggled. Is there a way to persist the view in case a page reload or product pagination changes? thank you.
here is the dom code that achieves this :
viewList.addEventListener('click', function() {
this.classList.add('view__active');
viewGrid.classList.remove('view__active');
gridItem.classList.add('hidden');
listItem.classList.remove('hidden');
});
viewGrid.addEventListener('click', function() {
this.classList.add('view__active');
viewList.classList.remove('view__active');
gridItem.classList.remove('hidden');
listItem.classList.add('hidden');
});
So far I found that I have to use localStorage to achieve this. but is there a better way to do this?
Essentially what is happening is when you request something from the server, the server responds with an HTML document, and whichever scripts associated with that document is run, So whatever JS executed in the first request is not in context when the second request(paginate or reload) is made.
So you need a way to persist information across these page loads, For that, you have 3 options.
Use sessionStorage.
Use localStorage
Use Cookies.
Of the 3 above the easiest would be to use either option 1 or 2.
Replying to your comment,
Also, If I am using localStorage, What am I using to store the view state?
I'm not quite clear as to what you mean by "What you are using to store the state" If your question is about where your data is stored, you need not worry about it as this is handled by the browser. If your question is about "How" to store it you can go through the MDN docs attached in option 1 or 2. This is simply storing a key-value pair as shown in the docs
localStorage.setItem('preferedView', 'grid'); You can add this to your on click handlers as follows,
viewList.addEventListener('click', function() {
this.classList.add('view__active');
viewGrid.classList.remove('view__active');
gridItem.classList.add('hidden');
listItem.classList.remove('hidden');
localStorage.setItem('preferedView', 'grid');
});
viewGrid.addEventListener('click', function() {
this.classList.add('view__active');
viewList.classList.remove('view__active');
gridItem.classList.remove('hidden');
listItem.classList.add('hidden');
localStorage.setItem('preferedView', 'list');
});
Then when loading a new page at the top of your script you can get the users preferedView(if existing) via const preferedView = localStorage.getItem('preferedView');
Here is a complete example from MDN
In order for anyone to find an answer for a similar task, thanks to #Umendra insight, I was able to solve this by using this :
function viewToggeler(viewBtn1, viewBtn2, view1, view2, viewStord) {
viewBtn2.classList.add('view__active');
viewBtn1.classList.remove('view__active');
view1.classList.add('hidden');
view2.classList.remove('hidden');
sessionStorage.setItem('preferedView', viewStord);
}
viewList.addEventListener('click', () => {
viewToggeler(viewGrid, viewList, gridItem, listItem, 'list');
});
viewGrid.addEventListener('click', () => {
viewToggeler(viewList, viewGrid, listItem, gridItem, 'grid');
});
if (sessionStorage.getItem('preferedView') === 'grid') {
viewToggeler(viewList, viewGrid, listItem, gridItem, 'grid');
} else if (sessionStorage.getItem('preferedView') === 'list') {
viewToggeler(viewGrid, viewList, gridItem, listItem, 'list');
}
I ended up using sessionStorage over localStorage because it empties itself on window/tab closing which might be the most desirable result. localStorage persists even after exiting the browser and opening it back.
Also, at any point someone wants to empty the sessionStorage on exit, I used :
window.addEventListener('onbeforeunload', () => {
sessionStorage.removeItem('preferedView');
});

JSPLUMB: disable nodes drag and drop after a specific user interaction

I'm using the community edition of jsplumb (version 2.15.5) within a React project.
// Outside the component
plumb = jsPlumb.getInstance(mySetting);
...
useEffect(() => {
// Handle endpoints
// Handle connections
plumb.draggable(document.querySelectorAll('.my-node'));
// Handle listeners
plumb.repaintEverything();
return () => {
plumb.current.unbind('connection');
plumb.current.unbind('connectionDetached');
plumb.current.deleteEveryEndpoint();
plumb.current.deleteEveryConnection();
};
}, [deps])
I need to have the nodes draggable and then stop them to be draggable when I click a button. I tried to change the argument of the draggable method using an if statement but when the useEffect is re-executed nothing change. I have to refresh the page to see the changes (i.e. havinvg my node undraggable).
if(buttonIsClicked){
plumb.draggable([]);
}else{
plumb.draggable(document.querySelectorAll('.my-node'));
}
It seem that once the draggable method is executed, jsplumb internally save which are the references of the DOM elements that can be draggable, and after that moment those references can't be overridden.
Do anyone know a way to tell jsplumb to stop the nodes to be draggable ?
Thanks.
You can use setDraggable() to arbitrarily set a draggable property to a jsplumb node, like:
const plumb = jsPlumb.getInstance({});
plumb.setDraggable(document.querySelectorAll('.dag-node'), false);
or switching it ON/OFF with toggleDraggable():
const plumb = jsPlumb.getInstance({});
jsPlumbNode.toggleDraggable(document.querySelectorAll('.dag-node'))
I don't think those functions are documented in the offical library website, however you can find some references in the relevant github repository, here and here.

Scroll to bottom of div automatically on page load

I have a chat application in react and have used npm package react-scroll-to-bottom until now. But it's not maintained and throwing warnings so i thought it should not be too hard to write this myself.
I'm following various answers on stackoverflow but cant seem to get it right.
It should just scroll to the bottom of the container as the page loads.
Any ideas what i'm doing wrong? Thanks in advance!
EDIT: now i have a div at the end of the tree which has the ref because i thought it would be easier to scroll to that instead of trying to tell a div to scroll inside it.
const chatHistoryRef = React.useRef();
React.useEffect(() => {
window.scrollTo(0, chatHistoryRef.current.offsetTop);
}, [chatHistoryRef]);
Your useEffect does not have any dependencies, so if the ref changes (as in, the div gets rendered) the effect will not be run. Dependencies are set in a second parameter as an array, like so:
useEffect(() => {
// do something with chatHistoryRef.current
}, [chatHistoryRef.current])
Found the solution. As i'm working with polling in my app right now i only get chat messages every two seconds. So my function was in fact scrolling, but there just wasnt anything to scroll away from. :)
This is my solution. Thanks everyone for the help. By adding length to message dependency this is only fired if there are new chat messages
const chatHistoryRef = React.useRef();
React.useEffect(() => {
chatHistoryRef.current.scrollIntoView({
behavior: 'smooth',
block: 'end'
});
}, [chatHistoryRef, messages.length]);

Angular sub-component style does not change until next ngAfterViewChecked

Angular version: 5.0.3
I have put my code here.
I have made a custom inkbar module, and imported it to the app. I use
<lib-inkbar [nextEle]="inkbarSubject" [color]="inkbarColor"></lib-inkbar>
...to install it in my app.component.
The inkbar under the navigation menu tabs is expected to automatically go in position where router link is currently active. (And this is done by getting the width and offset left of a given element, and move to it.) For example, at the beginning, "/index" is active, so "Index" tab has a class "active", and inkbar should translate under it.
I have achieved this by putting the code in Angular's ngAfterViewChecked life-cycle. inkbarMove() tells inkbar where to go by telling it the element (i.e. the active nav tab).
ngAfterViewChecked() {
this.navMenus = this.ul.nativeElement.children;
let actMenu = this.getAvtiveMenu();
if (actMenu) {
this.inkbarMove(actMenu);
}
}
inkbarMove = (ele: HTMLBaseElement | MouseEvent) => {
if (ele instanceof MouseEvent) {
ele = ele.target as HTMLBaseElement;
}
this.inkbarSubject.next(ele);
};
However, this works only after next ngAfterViewChecked() fires. When page first loads, the inkbar will stay {width: 0px; left: 0px} style, as initial default value, no matter how many times I tell it to "Move" (I even wrote a for-loop, which made my browser crash, but still didn't work lol). It will only starts to move after I press the button and manually call ngAfterViewChecked() again.
Does someone know what is going on here?

Destroying InfiniteScroll on AJAX page change

We have a website that is running jQuery Infinite Scroll Plugin. The plugin is no longer maintained but it is the only one that really serves our purpose properly. However, the problem I have is that our site is ajax based. On page change the pg-changed trigger is fired against the window, which allows us to check if the Infinite Scroll container is there and enable Infinite Scroll. If the Infinite Scroll container isn't there but $.infscr exists, we will attempt to destroy the previous instance.
The problem I am having is that when changing to another page, it doesn't seem to be getting destroyed properly and sometimes AJAX calls will be made, along with the infscr loading bar displaying. Here is the code I am using to instantiate and destroy the plugin:
$(window).on('pg-changed', function () {
// delete our infinite scroll
if(typeof $.infscr !== 'undefined') {
$('.snap-inner, .infscr').data('infinitescroll', null);
$('.snap-inner, .infscr').infinitescroll('unbind');
$('.snap-inner, .infscr').infinitescroll('destroy');
$('#infscr-loading').remove();
$.infscr.data('infinitescroll', null);
$.infscr.infinitescroll('unbind');
$.infscr.infinitescroll('destroy');
delete $.infscr;
}
// setup our infinite scroll
if($('.infscr').length) {
$.infscr = $('.infscr').infinitescroll({
// define our navigation selectors
navSelector : 'div.infscr-navigation',
nextSelector : 'div.infscr-navigation A:first',
itemSelector : '.infscr-item',
// allow scrolling an overflowed element
behavior : 'local',
bufferPx : 120,
binder : $('.snap-inner'),
dataType : 'html',
loading : {
msg : null,
selector : '.snap-content',
img : 'data:image/gif;base64,TRIMMED',
msgText : '<span class="infscr-loading">Loading...</span>',
}
}, function (arrayOfNewElems) {
// render background images on our new elements
$(this).renderBgImages();
});
}
});
I really hope you can help with this as it has become quite a problem now, firing on scroll, making AJAX calls and displaying the loading bar.
I guess the problem might be that the plugin binds events on elements external to itself. It's hard to say without further debugging (is the plugin deleted or does it fail to do so?). If it does, why would it fail to unbind the events? etc.
When you say, 'on another page', I suppose you are not loading a whole new page (which would 'reset' the javascript.
Though, depending on wether this could work for you, you might want to try unbinding ALL events on the document, and starting fresh.
$(document).add('*').off();
should do so. If it does, you could try to pinpoint what elements/events you need to unbind manually. Let me know if this works or not.

Categories

Resources