Track the elements rendered on Scroll - javascript

Thanks for your time.
I'm developing a page which has a top Calendar component( which show a week) and below that, a Scroll component which shows information for each one of the days of the week.
This is my page so far:
The problem here is that I need to know what day the scroll is showing to mark it at the calendar, like this example where the user is at day 3 and day 3 is marked at the calendar:
I have seen scroll tracking questions where the solutions are linked to ScrollY and ScrollX position, but in this case I need some info of the DOM element, like id or something, and I don't know if it's possible.
I also have tried onScroll method of the react infinite scroll, but it returns the whole document.
This is the code:
And the console print:
Thank you very much!

You could use the useRef() hook to reference the parent container within the onScroll function passed to the InfiniteScroll component. In that function you could use the parent reference to calculate which element is currently visible (closest to the middle of visible container). Then, pass item's date to a higher order component which is also shared with the calendar component.
Note: you would need to set the scrollableTarget property of the InfiniteScroll (because we want our parent element to be responsible for
scrollbars).
function InfiniteScrollContainer({ setFocusedItem }) {
const container = useRef();
function handleScroll() {
// Calculate which item is currently in the middle of the container
const containerMiddle = container.current.scrollTop + container.current.getBoundingClientRect().height / 2;
const infiniteScrollItems = container.current.children[0].children;
let index = 0;
let itemFound = false;
const itemNo = infiniteScrollItems.length;
while (!itemFound && index < itemNo) {
const item = infiniteScrollItems[i];
const itemTopOffset = item.offsetTop;
const itemBottomOffset = item.getBoundingClientRect().height + itemTopOffset;
if (itemTopOffset < containerMiddle && itemBottomOffset > containerMiddle) {
setFocusedItem(item);
itemFound = true;
}
index += 1;
}
}
useEffect(() => {
handleScroll();
}, [])
/* ... */
return (
<div ref={container} id="container-id">
<InfiniteScroll
<!-- ... -->
scrollableTarget={"container-id"}
onScroll={handleScroll}
>
<!-- ... -->
</InfiniteScroll>
</div>
);
}

Related

Changing content dynamically inside react-native-pager-view (or any other)

i'm working on a project where i'm going to be displaying details and information about a certain book page by page inside a pager view as page components, the book contains 500+ pages so i can't just create 500 page components like that and insert them into the pager..what i thought is i can get a specific page, render its component, alongside the previous, and the next page only..and when the user swipes to the next/previous page i would change the component state, and have it re-render with the new 3 pages, the current one, the previous, and the next one. the logic in my head makes perfect sense, but it just won't work when i try to apply it.
can anyone help me, guide me to certain videos that explain this principal more? i feel like i'm missing something.
the code goes like this:
first i have the PagesContainer, here i will create the PagesDetails component(s) based on the current page, and having these pages in react-native-pager-view (you can suggest me a better option). for testing purpose only, i set the swipe end callback (onPageSelected) to increment the current page number state, which would then cause the component to re-render and create the new page component(s), that happens only when the user swipes to new page of course:
function PagesContainer({ currentPageNumber, setCurrentPageNumber }) {
const [pageComponents, setPageComponents] = useState([]);
useEffect(() => {
let compArr = [];
compArr.push(<PageDetails key="current" pageNumber={currentPageNumber} />);
if (currentPageNumber > 1) {
compArr.unshift(<PageDetails key="previous" pageNumber={currentPageNumber - 1} />)
}
if (currentPageNumber <= 500) {
compArr.push(<PageDetails key="next" pageNumber={currentPageNumber + 1} />)
}
setPageComponents(compArr);
}, [currentPageNumber])
return (<PagerView style={{ flex: 1 }}
initialPage={currentPageNumber == 1 ? 0 : 1}
layoutDirection={"rtl"}
onPageSelected={(PageSelectedEvent)=>{setCurrentPageNumber(currentPageNumber + 1)}}
>
{pageComponents.map(page => {
return page;
})}
</PagerView>)
}
and then here i have my PageDeatails component where i simply display texts and details of the page, i take the data from the bookData object which is imported at the very top of the code file:
function PageDetails({ pageNumber }) {
const [pageContent, setPageContent] = useState(null);
useEffect(() => {
setPageContent(bookData[pageNumber]["pageContent"]);
}, []);
return (
<View>
{pageContent && <View>
{pageContent.map(item => {
return (<Text>item</Text>)
})}
</View>
}
</View>
)
}
The logic makes perfect sense in my head but it just doesn't work when i test it..what am i missing? what am i doing wrong?
use the PagerView reference using useRef also store the page index and pass to initialPage get current page index from onPageSelected callback
like:
initialPage={currentPageIndex}

Is there a way to get all classes using document.querySelectorAll in reactjs and manipulate it?

I wanted to allow users to change the theme of the application by picking which theme they want the body's background color changes and all button colors. But the problem is that whenever I use document.querySelectorAll('.btn-theme-1').style.backgroundColor it tells me that it cannot read those properties.
I know of useRef() but in my case I am trying to select all buttons throughout the entire application. Not just one element in the current component. So I would like to know if there is a way to fix what I am attempting or if I am doing this the wrong way.
Here is the code for what I tried. This is my pick theme component:
import ColorThemes from '../data/ColorThemes';
import { useEffect } from 'react';
const PickTheme = () => {
const changeTheme = (c) => {
document.body.style.background = c.default || c.bgColor;
document.body.style.color = c.bodyColor;
document.querySelector('.bi-quote').style.color = c.buttonBg;
document.querySelectorAll('.text-color').forEach(el => el.style.color = c.fontColor)
document.querySelectorAll('.btn-theme-1').forEach(el => {
el.style.color = c.buttonColor;
el.style.backgroundColor = c.buttonBg;
});
};
useEffect(() => {
},[changeTheme]);
return(
ColorThemes.background.map(c => {
if(c.bgColor) {
return(
<button type="button" key={c.bgColor} className="btn btn-light me-2 p-3 rounded-5" onClick={() => changeTheme(c)} style={{backgroundColor: c.bgColor}}></button>
);
} else {
return(
<><br/><button type="button" key={c.default} className="btn btn-light me-2 mt-2 rounded-5" onClick={() => changeTheme(c)}>Default</button></>
);
}
})
);
};
export default PickTheme;
It successfully changes the bodys color and background color but not the other classes. I tried with and without useEffect and still receive the same issue.
If I comment out everything except the last selector, the buttons then change colors. So maybe it is conflicting or cannot change everything at once, for example:
const changeTheme = (c) => {
// document.body.style.background = c.default || c.bgColor;
// document.body.style.color = c.bodyColor;
// document.querySelector('.bi-quote').style.color = c.buttonBg;
// document.querySelectorAll('.text-color').forEach(el => el.style.color = c.fontColor)
document.querySelectorAll('.btn-theme-1').forEach(el => {
el.style.color = c.buttonColor;
el.style.backgroundColor = c.buttonBg;
});
};
This changes the buttons background and color after commenting out the other parts.
I know of useRef() but in my case I am trying to select all buttons throughout the entire application. Not just one element in the current component.
Using .querySelector or any other selector will select only those elements that are currently rendered, actually. So if you e.g. toggle the state and component re-renders with different elements, they will not be affected with your change, which will result in partially toggled theme for different elements.
You should either set a context, wrapping whole App or set a redux variable holding info which theme is currently selected. Then, you will be able to manipulate styles using e.g. theme in styled components: https://styled-components.com/docs/advanced#theming or just toggling classNames with css modules, basing on that variable.
You can use useRef() with a function that runs on each element and add them to it, let me dive deeper into it.
Let's first create a reference containing, for now, an empty array:
const myRef = useRef([])
Great, we now want to populate that.
It's going to be in two parts, first, let's make a function that will populates that array:
const addToMyRef = (element) => {
if (element && !myRef.current.includes(element)) {
myRef.current.push(element);
}
};`
Great, we now have a function that takes an element of the DOM as an argument, verifies that it exists and that it is not yet in our array, then adds it.
But now, when will it get triggered? Simply in the ref= attribute!
<button ref={addToMyRef}></button>
You'll now see that your reference is now a set of them, so you can create a reference per element, or maybe modify the code a little to makes it takes objects to have a all-in-one reference for each element of the dom. (We could imagine it being myRef.buttons/myRef.inputs...)

How to add a class of "active" to individual elements in a functional navigation component in react?

This is something quite simple but somehow resulted in a crazy rabbit hole.
This link shows what I want:
https://www.w3schools.com/howto/howto_js_active_element.asp
Nothing special, now the thing becomes hairy for me when the elements in the navbar are rendered from an array of objects (from the specs). The approach I am following is basically rendering a list of buttons, this list of buttons is the state, since supposedly when you update a state it triggers a re-render, then when a button is clicked it "sets" the active class to false on the entire array-state then activates it only for the clicked one. So far it works.
The problem is that the active class is rendered two steps behind. One for the moment when the class in the array-state's elements are set to false, the other when the clicked element gets updated.
As far as I understand useState and setState are queues, hence those are applied asynchronously on each render, in order to avoid that and get the renders to show the current state, useEffect is utilized.
Now the thing is that I am not sure how to apply useEffect in order to achieve the immediate render of the "active" class.
This is the code I have:
import { options } from 'somewhere...'
export default function SideMenu(props){
let auxArr = []
let targetName
const [stateOptions, setStateOptions] = useState([...options])
const [currentOption, SetCurrentOption] = useState({})
function activeOption(e){
// this helps with event bubbling
if (e.target.tagName == "P" || e.target.tagName == "SPAN"){
targetName = e.target.parentElement.id
} else if (e.target.tagName == "IMG"){
targetName = e.target.parentElement.parentElement.id
} else {
targetName = e.target.id
}
// since the main state is an array of objects I am updating it
// in three steps, first the current object is "activated"
// then the main array-state gets "inactivated" to erase all
// the previous "active" classes, finally the activated object
// replaces the corresponding inactive object in the main state.
let targetElement = stateOptions.filter(e => e.id==targetName)[0]
SetCurrentOption({
id: targetElement.id,
activity:true,
img: targetElement.img,
name: targetElement.name
})
// first the "classes" are set to false, then the
// "activated" object replaces the corresponding one
// in the main object, from here comes the two
// steps delay.
auxArr = [...stateOptions]
auxArr.forEach(e => e.activity=false)
setStateOptions(auxArr)
const newOptions = stateOptions.map(e =>
e.id==currentOption.id ? currentOption : e
)
setStateOptions(newOptions)
}
return(
<aside className={styles.sideDiv}>
<nav>
{stateOptions.map(({id, img, name, activity, link}) => {
return(
<button key={id} id={id} onClick={activeOption} className={activity?styles.active:""}>
<Image src={img}/>
<p className={timeColor.theme}> {name} </p>
</button>
)
})}
</nav>
</aside>
)
}
Thanks in advance for any help you can provide.

How to get the element ID into scroll view to mark category in navbar

I have a navbar with a list of categories, and I wanted to set the selected category according to what the user is looking at, but for that I need to get the element ID, but these IDs are dynamically set. Currently I already have a function to set the selected category, but it sets the selected category as the click, and I would also like to mark the selected one as the user scrolls.
My code
function onHandleClick(props){
setCurrentCat(props)
document.getElementById(props).scrollIntoView();
}
return (
<div className = {Position ? 'navbarContainer navbarContainerScroll':'navbarContainer'}>
{Categorias.map(item =>(
<ul>
<li className = {item.Cod === currentCat ? 'navbarContainer liSelect' : 'navbarContainer liNormal'}
onClick = {() => onHandleClick(item.Cod)
}>{item.Nome}</li>
</ul>
))}
</div>
)
I currently move the screen to the category are clicked on the navigation bar, how can I set the selected category when the screen focus is on top of it
I have tried to answer your problem as I understand it. I have filled in some extra pieces to help give a full solution. I hope bits and pieces of this are relevant to your issue!
So if I understand you correctly you want to:
be able to automatically update the active tab in your navbar when a user scrolls the page. Also, I assume they are scrolling vertically for my answer.
As per the problem and code you provided, I infer you have a setup somewhat like this:
A Section or Category component containing content for a given item
A Page component that would render a number of Cateogory components and NavBar
Given these two things, I have put the following code together that could help you achieve the functionality you are looking for:
import React, { useEffect, useRef, useState } from "react";
/** Check if an HTML element is within the main focus region of the page */
function isElementInMainFocusArea(
element,
verticalConstrainFactor = 0,
horizontalConstrainFactor = 0
) {
const elementRect = element.getBoundingClientRect();
const documentHeight =
window.innerHeight || document.documentElement.clientHeight;
const documentWidth =
window.innerWidth || document.documentElement.clientWidth;
// Vertical focus region
const topFocusPos = documentHeight * verticalConstrainFactor;
const bottomFocusPos = documentHeight * (1 - verticalConstrainFactor);
// Horizontal focus region
const leftFocusPos = documentWidth * horizontalConstrainFactor;
const rightFocusPos = documentWidth * (1 - horizontalConstrainFactor);
return (
elementRect.top >= topFocusPos &&
elementRect.bottom <= bottomFocusPos &&
elementRect.left >= leftFocusPos &&
elementRect.right <= rightFocusPos
);
}
/** Navigation bar component which will taken in a list of refs.
Each ref must be assigned to or given as a prop to some other section on the page that we want to scroll to. */
function NavBar({ pageSectionRefs, categories }) {
const [currentCat, setCurrentCat] = useState();
/** Set up the scroll event listener to update the currentCat whenever we scroll*/
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
/** Check all the refs we are to watch to see which one of them is in focus */
function handleScroll() {
for (ref in pageSectionRefs.reverse()) {
if (isElementInMainFocusArea(ref.current)) {
setCurrentCat(index);
return; // if two elements are in focus, only the first will be chosen. Could cause a glitch
}
}
}
// Returns what you were previously returning for this component
return (
<div
className={
Position ? "navbarContainer navbarContainerScroll" : "navbarContainer"
}>
{categories.map((item) => (
<ul>
<li
className={
item.Cod === currentCat
? "navbarContainer liSelect"
: "navbarContainer liNormal"
}
onClick={() => onHandleClick(item.Cod)}>
{item.Nome}
</li>
</ul>
))}
</div>
);
}
/** The top level component that will be rendering the NavBar and page sections. */
function MyPage() {
const categories = [];
const pageSectionRefs = categories.map(() => useRef());
return (
<div>
<NavBar pageSectionRefs={pageSectionRefs} categories={categories} />
{categories.map((cat, idx) => {
return (
<div key={idx} ref={pageSectionRefs[idx]}>
Section for category {idx}
</div>
);
})}
</div>
);
}
In case I don't have a full picture of your problem, kindly comment things I may have missed so I update my answer!
References:
The isElementInFocusArea function was adapted from here: How can I tell if a DOM element is visible in the current viewport?
Inspiration for handling list of refs from here: How can I use multiple refs for an array of elements with hooks?

Scroll to bottom of div with Vue.js

I have a Vue.js component with several elements in it. I want to automatically scroll to the bottom of that element when a method in the component is called.
Basically, do the same as this. However, I haven't found a way to get the element within my component and modify scrollTop
I'm currently using Vue.js 2.0.8.
2022 easy, readable, smooth scrolling ability, & won't hurt your brain... use el.scrollIntoView()
scrollIntoView() has options you can pass it like scrollIntoView({behavior: 'smooth'}) to get smooth scrolling out of the box and does not require any external libraries.
Here is a fiddle.
methods: {
scrollToElement() {
const el = this.$refs.scrollToMe;
if (el) {
// Use el.scrollIntoView() to instantly scroll to the element
el.scrollIntoView({behavior: 'smooth'});
}
}
}
Then if you wanted to scroll to this element on page load you could call this method like this:
mounted() {
this.scrollToElement();
}
Else if you wanted to scroll to it on a button click or some other action you could call it the same way:
<button #click="scrollToElement">scroll to me</button>
The scroll works all the way down to IE 8. The smooth scroll effect does not work out of the box in IE or Safari. If needed there is a polyfill available for this here as #mostafaznv mentioned in the comments.
As I understand, the desired effect you want is to scroll to the end of a list (or scrollable div) when something happens (e.g.: an item is added to the list). If so, you can scroll to the end of a container element (or even the page it self) using only pure JavaScript and the VueJS selectors.
var container = this.$el.querySelector("#container");
container.scrollTop = container.scrollHeight;
I've provided a working example in this fiddle.
Every time a item is added to the list, the list is scrolled to the end to show the new item.
I tried the accepted solution and it didn't work for me. I use the browser debugger and found out the actual height that should be used is the clientHeight BUT you have to put this into the updated() hook for the whole solution to work.
data(){
return {
conversation: [
{
}
]
},
mounted(){
EventBus.$on('msg-ctr--push-msg-in-conversation', textMsg => {
this.conversation.push(textMsg)
// Didn't work doing scroll here
})
},
updated(){ <=== PUT IT HERE !!
var elem = this.$el
elem.scrollTop = elem.clientHeight;
},
Use the ref attribute on the DOM element for reference
<div class="content scrollable" ref="msgContainer">
<!-- content -->
</div>
You need to setup a WATCH
data() {
return {
count: 5
};
},
watch: {
count: function() {
this.$nextTick(function() {
var container = this.$refs.msgContainer;
container.scrollTop = container.scrollHeight + 120;
});
}
}
Ensure you're using proper CSS
.scrollable {
overflow: hidden;
overflow-y: scroll;
height: calc(100vh - 20px);
}
Here is a simple example using ref to scroll to the bottom of a div.
/*
Defined somewhere:
var vueContent = new Vue({
el: '#vue-content',
...
*/
var messageDisplay = vueContent.$refs.messageDisplay;
messageDisplay.scrollTop = messageDisplay.scrollHeight;
<div id='vue-content'>
<div ref='messageDisplay' id='messages'>
<div v-for="message in messages">
{{ message }}
</div>
</div>
</div>
Notice that by putting ref='messageDisplay' in the HTML, you have access to the element through vueContent.$refs.messageDisplay
If you need to support IE11 and (old) Edge, you can use:
scrollToBottom() {
let element = document.getElementById("yourID");
element.scrollIntoView(false);
}
If you don't need to support IE11, the following will work (clearer code):
scrollToBottom() {
let element = document.getElementById("yourID");
element.scrollIntoView({behavior: "smooth", block: "end"});
}
Try vue-chat-scroll:
Install via npm: npm install --save vue-chat-scroll
Import:
import Vue from 'vue'
import VueChatScroll from 'vue-chat-scroll'
Vue.use(VueChatScroll)
in app.js after window.Vue = require('vue').default;
then use it with :
<ul class="messages" v-chat-scroll>
// your message/chat code...
</ul>
For those that haven't found a working solution above, I believe I have a working one. My specific use case was that I wanted to scroll to the bottom of a specific div - in my case a chatbox - whenever a new message was added to the array.
const container = this.$el.querySelector('#messagesCardContent');
this.$nextTick(() => {
// DOM updated
container.scrollTop = container.scrollHeight;
});
I have to use nextTick as we need to wait for the dom to update from the data change before doing the scroll!
I just put the above code in a watcher for the messages array, like so:
messages: {
handler() {
// this scrolls the messages to the bottom on loading data
const container = this.$el.querySelector('#messagesCard');
this.$nextTick(() => {
// DOM updated
container.scrollTop = container.scrollHeight;
});
},
deep: true,
},
The solution did not work for me but the following code works for me. I am working on dynamic items with class of message-box.
scrollToEnd() {
setTimeout(() => {
this.$el
.getElementsByClassName("message-box")
[
this.$el.getElementsByClassName("message-box").length -
1
].scrollIntoView();
}, 50);
}
Remember to put the method in mounted() not created() and add class message-box to the dynamic item. setTimeout() is essential for this to work. You can refer to https://forum.vuejs.org/t/getelementsbyclassname-and-htmlcollection-within-a-watcher/26478 for more information about this.
This is what worked for me
this.$nextTick(() => {
let scrollHeight = this.$refs.messages.scrollHeight
window.scrollTo(0, scrollHeight)
})
In the related question you posted, we already have a way to achieve that in plain javascript, so we only need to get the js reference to the dom node we want to scroll.
The ref attribute can be used to declare reference to html elements to make them available in vue's component methods.
Or, if the method in the component is a handler for some UI event, and the target is related to the div you want to scroll in space, you can simply pass in the event object along with your wanted arguments, and do the scroll like scroll(event.target.nextSibling).
I had the same need in my app (with complex nested components structure) and I unfortunately did not succeed to make it work.
Finally I used vue-scrollto that works fine !
My solutions without modules:
Template
<div class="scrollable-content" ref="conversations" />
Script
scrollToBottom() {
const container = this.$refs.conversations;
container.scrollTop = container.scrollHeight;
},
scrollToBottom() {
this.$nextTick(function () {
let BoxEl = document.querySelector('#Box');
if(BoxEl)
BoxEl.scrollTop = BoxEl.scrollHeight;
});
}
Agree with Lurein Perera
Just want to add extra info
watch: {
arrayName: {
handler() {
const container = this.$el.querySelector("#idName");
this.$nextTick(() => {
container.scrollTop = container.scrollHeight;
});
},
deep: true,
},
},
Where as:
arrayName = Name of array
idName = The id attribute has to be added to the div where you want the scrollbar to auto-scroll down when arrayName length increases.
scrollToElement() {
const element = this.$refs.abc; // here abc is the ref of the element
if (element) {
el.scrollIntoView({behavior: 'smooth'});
}
}
}
here you need to use ref for the particular div or element which you want make visible on scroll.
if you have a table and you want to locate the last row of the table then you have to use -
element.lastElementChild.scrollIntoView({behaviour:'smooth'})
Here not that if you ware asynchronously adding the element to the table then you have to take care of it. you can test it using setTimeout, if that is making any difference.
e.g.
const element = this.$refs.abc;
if (element) {
setTimeout(() => {
element.lastElementChild.scrollIntoView({behaviour:'smooth'})
}, 1000);
}
}
replace set timeout with your own async logic.
Using Composition API and TypeScript
I set the parameter scrollTop equal to scrollHeightfrom the HTMLDivElment API.
<template>
<div id="container" ref="comments">
Content ...
</div>
</template>
<script lang="ts">
import { defineComponent, ref, Ref, watchEffect } from 'vue'
export default defineComponent({
setup() {
const comments: Ref<null | HTMLDivElement> = ref(null)
watchEffect(() => {
if(comments.value) {
comments.value.scrollTop = comments.value.scrollHeight
}
})
return {
comments
}
}
})
</script>

Categories

Resources