How to create draggable components with javascript and react hooks? - javascript

I need to add draggable divs, one for every image in an array of images, to a container.
The below works BUT only on the second drag. i.e. I have to drag the component twice for it to move. It obviously has something to do with the hooks used and how the divs are being created. Any help will be appreciated.
Code sandbox
const App = ({images}) => {
const [selectedImages, setSelectedImages] = useState(images)
const [dragId, setDragId] = useState()
const handleDrag = (ev) => {
setDragId(ev.currentTarget.id)
}
const handleDrop = (ev) => {
const sortedImages = sortImages(selectedImages, dragId)
setSelectedImages(sortedImages)
}
return (
<Container
images={selectedImages}
handleDrag={handleDrag}
handleDrop={handleDrop}
/>
)
}
export default App
const Container = ({ images, handleDrag, handleDrop }) => {
const ref = useRef(null)
useEffect(() => {
if (containerRef.current) {
for (let i = 0; i < images.length; ++i) {
const draggable = document.createElement('div')
draggable.ondragstart = handleDrag
draggable.ondrop = handleDrop
const style = 'position: absolute; ...'
draggable.setAttribute('style', style)
draggable.setAttribute('draggable', true)
draggable.setAttribute('id', images[i].id)
ref.current.appendChild(draggable)
}
}
}, [images, ref, handleDrag, handleDrop])
return (
<div className={'relative'}>
<div ref={ref} />
</div>
)
}
export default Container
It looks like it is setting the dragId on first drag and then only on the second drag it actually drags and drops the div. How can I set this up so that it drags and drops on the first try?

I should have just used React to add the functions
const Container = ({ images, handleDrag, handleDrop }) => {
const containerRef = useRef(null)
return (
<div ref={containerRef}>
{images.map((image) => (
<div
key={image.id}
id={image.id}
draggable
onDragOver={(ev) => ev.preventDefault()}
onDrop={handleDrop}
onDragStart={handleDrag}
style={{
position: 'absolute'...`,
}}
/>
))}
</div>
)
}
export default Container
Sandbox

Related

How to check which functional React Component has been viewed in the viewport latest?

My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can't figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
function useIsInViewport(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) =>
setIsIntersecting(entry.isIntersecting)
),
[]
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, observer]);
return isIntersecting;
}
var videoProxy = new Proxy(videoViewports, {
set: function (target, key, value) {
// console.log("value " + value)
// console.log("key " + key)
console.log(videoViewports);
if (value) {
setMostRecentVideo(key);
//console.log("Most Rec: " + mostRecentVideo);
}
target[key] = value;
return true;
},
});
const [isGlobalMute, setIsGlobalMute] = useState(true);
const [mostRecentVideo, setMostRecentVideo] = useState("");
videoProxy["Podcast 1"] = useIsInViewport(ref1);
videoProxy["Podcast 2"] = useIsInViewport(ref2);
videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer
ref={ref1}
videoProxy={videoProxy}
mostRecentVideo={mostRecentVideo}
setMostRecentVideo={setMostRecentVideo}
title="Podcast 1"
isGlobalMute={isGlobalMute}
setIsGlobalMute={setIsGlobalMute}
videoSource={video1}
podcastName={podcastName}
networkName={networkName}
episodeName={episodeName}
episodeDescription={episodeDescription}
logo={takeLogo}
muteIcon={muteIcon}
unmuteIcon={unmuteIcon}
></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don't understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can't be wrapped in a useEffect(() => {}, []) hook. Maybe I'm doing this completely the wrong way with the wrong React Hooks, but I've been bashing my head against this for so long...
First of all: I'm not quite sure, if a Proxy.set is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I've created a simple solution that uses two components. First the a VideoList that contains all videos and manages the viewport calculations so you don't have thousands of event listeners on resize, scroll and so on (or Observers respectively).
The Video component is a forwardRef component, so we get the ref of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react";
function inViewport(el) {
if (!el) {
return false;
}
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const Video = forwardRef((props, ref) => {
return (
<div ref={ref}>
<p>{props.source}</p>
<video {...props} />
</div>
);
});
const VideoList = ({ sources }) => {
const sourcesLength = sources.length;
const [refs, setRefs] = useState([]);
useEffect(() => {
// set refs
setRefs((r) =>
Array(sources.length)
.fill()
.map((_, i) => refs[i] || createRef())
);
}, [sourcesLength]);
const isInViewport = useCallback(() => {
// this returns only the first but you can also apply a `filter` instead of the index
const videoIndex = refs.findIndex((ref) => {
return inViewport(ref.current);
});
if (videoIndex < 0) {
return;
}
console.log(`lastSeen video is ${sources[videoIndex]}`);
}, [refs, sources]);
useEffect(() => {
// add more listeners like resize, or use observer
document.addEventListener("scroll", isInViewport);
document.addEventListener("resize", isInViewport);
return () => {
document.removeEventListener("scroll", isInViewport);
document.removeEventListener("resize", isInViewport);
};
}, [isInViewport]);
return (
<div>
{sources.map((source, i) => {
return <Video ref={refs[i]} source={source} key={i} />;
})}
</div>
);
};
export default function App() {
const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"];
return (
<div className="App">
<VideoList sources={sources} />
</div>
);
}
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I'm using a useState for the ref list.

How can I retrieve the scrollTop value from a ref using Jest and Enzyme in a React functional component?

I have an Articles component that renders article boxes. When a box is clicked on the page animates out and the article animates in.
It saves the scrollTop position in state so that when the article is closed it scrolls back to where the container scroll position from before.
I'm having trouble writing a test for it though as from my understanding you can't access state values directly in functional components, and spying wouldn't give me the the values right?
I've been searching and reading various posts and articles about it but I can only seem to find info on mocking the value which i don't need or the scrollTo method which is not what i'm using.
Essentially the code I have is something along these lines:
const Articles = ({articlesJsx}) => {
const pageContRef = React.useRef(null);
const currentArticle = useRef(articlesJsx[0]);
const scrollPos = useRef(0);
const [articleVisible, setArticleVisible] = useState(false);
const setScrollPos = () => {
setTimeout(() => {
pageContRef.current.scrollTop = scrollPos.current;
}, 500);
};
const scrollToTop = () => {
setTimeout(() => {
pageContRef.current.scrollTop = 0;
}, 500);
};
const handleShowArticle = (index) => {
currentArticle.current = articlesJsx[index];
scrollPos.current = pageContRef.current.scrollTop;
setArticleVisible(true);
setTimeout(() => {
scrollToTop();
}, 500);
};
const closeArticle = () => {
setArticleVisible(false);
setTimeout(() => {
setScrollPos();
}, 500);
};
return (
<>
<div
className={pageCont}
data-test="articlesContainer"
ref={pageContRef}
>
<div className={styles[animDir]}>
<CSSTransition
in={!articleVisible}
>
<Hero />
{articlesJsx.map((article, index) => (
<ArticleBox handleShowArticle={() => handleShowArticle(index)} />
))}
</CSSTransition>
<CSSTransition
in={articleVisible}
unmountOnExit
>
<Article
content={currentArticle.current}
>
<BackArrow handleCloseArticle={closeArticle} />
<Article />
</CSSTransition>
</div>
</div>
</>
);
};
My test so far:
describe("Articles tests", () => {
const articles = articlesData;
let component;
beforeEach(() => {
jest.useFakeTimers();
const useRefSpy = jest.spyOn(React, "useRef");
component = mount(
<Articles articlesJsx={articles} />
});
it("should scroll to the top of the article when viewed", () => {
const articleBox = findByTestAttr(component, "articleBoxMain");
let articlesCont = findByTestAttr(component, "articlesContainer");
articlesCont.simulate("scroll", {target: {scrollTop: 100}});
articlesCont = findByTestAttr(component, "articlesContainer");
act(() => {
articleBox.first().simulate("click");
});
act(() => jest.runAllTimers());
// const scrollTopPosition = articlesCont.......?
expect(scrollTopPosition).toEqual(0);
});
});
Enzyme docs say you can find a component through one of its properties but console.log(component.find({scrollTop: 100}).length); comes up as 0.
As a last resort I tried expect(useRefSpy).toHaveBeenCalledWith(0) but the received value comes up as undefined.
What is the correct way to test for this?

How do I store an HTML elements offsetTop on initial React.js render to be used with a scroll event listener?

I'm making a "sticky" header when scrollY meets offsetTop, but I don't want to hard code the offset. I've been attempting to store the initial offsetTop which can then be checked inside the isSticky() method attached to the scroll listener.
So far I've been unable to make the value stick (no pun intended) and it appears to be null inside the method being run.
const ProfileHeader: React.FC<ProfileHeaderData> = ({ profile }) => {
const bannerUrl = getBannerImageUrl(profile.id, null, profile.bannerImageVersion);
const logoUrl = getLogoImageUrl(profile.id, null, profile.logoImageVersion);
const headerRef = useRef<HTMLDivElement>(null);
const [sticky, setSticky] = useState("");
const [headerOffSet, setHeaderOffSet] = useState<number>(null);
console.log(`Outside: ${headerOffSet}`);
// on render, set listener
useEffect(() => {
console.log(`Render: ${headerRef.current.offsetTop}`);
setHeaderOffSet(headerRef.current.offsetTop);
window.addEventListener("scroll", isSticky);
return () => {
window.removeEventListener("scroll", isSticky);
};
}, []);
const isSticky = () => {
/* Method that will fix header after a specific scrollable */
const scrollTop = window.scrollY;
let originalOffset = headerOffSet ?? headerRef.current.offsetTop;
if (headerOffSet === null) {
console.log(`Setting header off set`);
setHeaderOffSet(originalOffset);
}
console.log(`top: ${scrollTop} | offset: ${originalOffset} | state: ${headerOffSet}`);
const stickyClass = scrollTop >= originalOffset ? styles.sticky : "";
setSticky(stickyClass);
};
return (
<div className={styles.container}>
<div className={styles.bannerContainer}>
{profile.bannerImageVersion && (
<Image
src={bannerUrl}
layout='fill'
className={styles.banner}
/>
)}
</div>
<div ref={headerRef} className={`${styles.header} ${sticky}`}>
<div className={styles.logoContainer}>
{profile.logoImageVersion && (
<Image
src={logoUrl}
layout='fill'
className={styles.logo}
/>
)}
</div>
<FlagCircleIcon {...profile.country} size={32} />
<h1 className={styles.displayName}>{profile.displayName}</h1>
</div>
</div>
)
}
On the initial page load, I get the following console output:
As I start to scroll, the output is as follows:
Seems like the state is never set?
Found a blog post explaining that you need to use a reference (useRef) and access the value via that inside the listener.
https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559
This references another SO post:
React useState hook event handler using initial state
EDIT
Providing working example based on original question code and solution provided by the blog post. The change the key change is to have a references to the useState variables and read from those inside the event listener.
import { useEffect, useRef, useState } from 'react';
import styles from './testComponent.module.scss';
export const TestComponent: React.FC = ({ children }) => {
const headerRef = useRef<HTMLDivElement>(null);
const [sticky, _setSticky] = useState(false);
const stickyRef = useRef(sticky);
const setSticky = (data: boolean) => {
stickyRef.current = data;
_setSticky(data);
}
const [headerOffSet, _setHeaderOffSet] = useState<number>(null);
const headerOffSetRef = useRef(headerOffSet);
const setHeaderOffSet = (data: number) => {
headerOffSetRef.current = data;
_setHeaderOffSet(data);
}
const isSticky = () => {
const scrollTop = window.scrollY;
const isSticky = scrollTop >= headerOffSetRef.current;
setSticky(isSticky);
};
// on render, set listener
useEffect(() => {
setHeaderOffSet(headerRef.current.offsetTop);
window.addEventListener("scroll", isSticky);
return () => {
window.removeEventListener("scroll", isSticky);
};
}, []);
return (
<div className={styles.container}>
<div ref={headerRef} className={`${styles.header} ${sticky ? styles.isSticky : null}`}>
{children}
</div>
</div>
)
}

how to calculate width of component in react js?

I am trying to calculate the width of child component .is it possible to calculate width of children's ? here is my code
here is my code
https://codesandbox.io/s/reverent-hermann-yt7es?file=/src/App.js
<Tabs>
{data.map((i) => (
<li>{i}</li>
))}
</Tabs>
TABS.js
import React, { useEffect, useRef } from "react";
const Tabs = ({ children }) => {
const tabsRef = useRef(null);
const setTabsDimensions = () => {
if (!tabsRef.current) {
return;
}
// initial wrapper width calculation
const blockWidth = tabsRef.current.offsetWidth;
// const showMoreWidth = moreItemRef.current.offsetWidth;
// calculate width and offset for each tab
let tabsTotalWidth = 0;
const tabDimensions = {};
children.forEach((tab, index) => {
if (tab) {
console.log(tab);
// const width = !isMobile ? 200 : 110;
// tabDimensions[index] = {
// width,
// offset: tabsTotalWidth
// };
// tabsTotalWidth += width;
}
});
};
useEffect(() => {
setTabsDimensions();
});
return (
<ul ref={tabsRef} className="rc64nav">
{children}
</ul>
);
};
export default Tabs;
I know using ref we can access the dom property but how to apply ref in children element element.
here i am trying to calculate all item width
children.forEach((tab, index) => {
if (tab) {
console.log(tab);
Just answer your question how to get child's ref.
const Parent = () => {
const childRef = useRef()
// An example of use the childRef
const onClick = () => {
if (!childRef.current) return
console.log(childRef.current.offsetWidth)
}
return <Child childRef={childRef} />
}
const Child = ({ childRef }) => {
return <div ref={childRef}>Hello Child</div>
}
NOTE: the way I pass ref here is exactly the same way you pass parent method to child. And also ref supports two formats, i'm using the simple one, but you can also use () => {} format. https://reactjs.org/docs/refs-and-the-dom.html

Transition effect not working for dynamic height

I have created a collapse component, but the transition effect is not working when i open and close the collapse component.
index.js
const Collapse = ({ title, text, child, ...props }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleCollapse = () => setIsOpen((isOpen) => !isOpen);
const closeCollapse = () => setIsOpen(false);
const content = useRef(null);
const isParentOpen = props.isParentOpen;
useEffect(() => {
if (!isParentOpen) closeCollapse();
}, [isParentOpen]);
const height = !isOpen ? "0px" : `auto`; // ${content.current?.scrollHeight}px
return (
<CollapseContainer>
<CollapseButton isOpen={isOpen} onClick={toggleCollapse}>
<CollapseTitleWrapper>{title}</CollapseTitleWrapper>
</CollapseButton>
<CollapseContent ref={content} max_height={height}>
<CollapseText>
{text}
{child?.map((datumn, index) => (
<Collapse
{...datumn}
key={`collapse-child-${index}`}
isParentOpen={isOpen}
/>
))}
</CollapseText>
</CollapseContent>
</CollapseContainer>
);
};
I think the height calculation should happen when the nested child also gets opened and closed.
Working codesandbox

Categories

Resources