How can I add a fade when the image is changed? - javascript

This is an image that changes every 5 seconds, but I think it's distracting, because it doesn't have any fade. How can I make it so that there is a short transition time like you can do in CSS?
import React, { useEffect, useState } from "react";
import aa from '../imgs/aa.JPG'
import aa2 from '../imgs/aa2.JPG'
import aa3 from '../imgs/aa3.JPG'
import aa4 from '../imgs/aa4.JPG'
import gg from '../imgs/gg.jpeg'
import gg2 from '../imgs/gg2.jpeg'
import gg3 from '../imgs/gg3.jpeg'
import gg4 from '../imgs/gg4.jpeg'
import './AnimatedGalery.css'
const images = [aa, aa2, aa3, aa4, gg, gg2, gg3, gg4];
export default function () {
let [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
if(currentIndex == images.length - 1) {
setCurrentIndex(currentIndex = 0);
}
else {
setCurrentIndex(currentIndex = currentIndex + 1);
}
}, 5000)
return () => clearInterval(intervalId);
}, [])
return (
<div>
<img src={images[currentIndex]} />
</div>
)
}

Have a look at React Spring https://aleclarson.github.io/react-spring/v9/
Here is a quick sandbox showing a demo of what it sounds like you're after.
https://codesandbox.io/s/affectionate-nightingale-r7yjm?file=/src/App.tsx
Essentially, React Spring's useTransition hook will play spring-based animations when the data provided to the hook changes.
import { animated, useTransition } from "#react-spring/web";
import * as React from "react";
const imageUrls = [
"https://images.unsplash.com/photo-1462396240927-52058a6a84ec?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=966&q=80",
"https://images.unsplash.com/photo-1495314736024-fa5e4b37b979?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1352&q=80",
"https://images.unsplash.com/photo-1612004687343-617e7c8f68d8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80"
];
export default function App() {
const [index, setIndex] = React.useState(0);
const [imageUrl, setImageUrl] = React.useState(imageUrls[index]);
React.useEffect(() => {
const timeoutId = window.setTimeout(() => {
setIndex((s) => {
let newIndex = 0;
if (s < imageUrls.length - 1) newIndex = s + 1;
setImageUrl(imageUrls[newIndex]);
return newIndex;
});
}, 2500);
return () => clearTimeout(timeoutId);
}, [index]);
const transition = useTransition(imageUrl, {
from: { opacity: 0, transform: "translateY(-1rem) scale(0.75)" },
enter: { opacity: 1, transform: "translateY(-0rem) scale(1)" },
leave: { opacity: 0, transform: "translateY(1rem) scale(1.25)" }
});
const fragment = transition((style, item) => (
<animated.div style={{ ...style, position: "absolute" }}>
<img
src={item}
alt=""
style={{ width: 200, height: 200, objectFit: "cover" }}
/>
</animated.div>
));
return (
<div
style={{
display: "flex",
width: "100vw",
height: "100vh",
alignItems: "center",
justifyContent: "center"
}}
>
{fragment}
</div>
);
}

Related

Add shake animation when the character limit react 100

I have a component that has a limitation, character limit that if it the user reach the limit the component will shake, thats the expected result, but in my current code nothing happen.
const useStyles = makeStyles(() => ({
shake:{
animation: 'description 0.5s',
animationIterationCount: '1',
},
'#keyframes description':{
'0%': { transform: 'translate(0)' },
'15%': { transform: 'translate(-4px, 0)' },
'30%': { transform: 'translate(6px, 0)' },
'45%': { transform: 'translate(-4px, 0)' },
'60%': { transform: 'translate(6px, 0)' },
'100%': { transform: 'translate(0)' },
},
}));
const StudentAdd = ({ setOpenAlertStudent }) => {
const classes = useStyles();
const CHARACTER_LIMIT = 100;
const [isShake, setShake] = useState(false)
const onHandleChangeInputDescription = (field, value) => {
if(value.length === 100){
setShake(true)
}
......
<TextArea
label="Description (optional)"
inputProps={{
maxLength: CHARACTER_LIMIT,
}}
values={getStringDescription.name}
onChange={(value) =>
onHandleChangeInputDescription('description', value)
}
helperText={`${getStringDescription.length}/${CHARACTER_LIMIT}`}
className={
isShake
? classes.shake
: null
}
id="description"
//id={isShake === true ? classes.shake : 'description'}
/>
......
this is the sample code on codesandbox
https://codesandbox.io/s/lucid-banach-o6sc1j?file=/src/App.js:1081-1089
import React, { useState, useContext } from "react";
import "./styles.css";
import { makeStyles } from "#mui/styles";
import { TextField } from "#mui/material";
const useStyles = makeStyles(() => ({
shake: {
animation: "$description 15s",
animationIterationCount: "1"
},
"#keyframes description": {
"0%": { opacity: 0, transform: "translateY(0)" },
"15%": { transform: "translateY(-4px, 0)" },
"30%": { transform: "translateY(6px, 0)" },
"45%": { transform: "translateY(-4px, 0)" },
"60%": { transform: "translateY(6px, 0)" },
"100%": { opacity: 1, transform: "translateY(0)" }
}
}));
export default function App() {
const classes = useStyles();
console.log(classes.shake);
const CHARACTER_LIMIT = 100;
const [isShake, setShake] = useState(false);
const [getStringDescription, setStringDescription] = useState({
length: 0,
value: ""
});
const onHandleChangeInputDescription = (field, value) => {
if (value.target.value.length === 100) {
setShake(true);
}
setStringDescription({
length: value.target.value.length,
value: value.target.value
});
};
return (
<div className="App">
<TextField
label="Description"
inputProps={{
maxLength: CHARACTER_LIMIT
}}
values={getStringDescription.name}
onChange={(value) =>
onHandleChangeInputDescription("description", value)
}
helperText={`${getStringDescription.length}/${CHARACTER_LIMIT}`}
sx={{
"& .MuiInputBase-input.MuiInputBase-inputMultiline": {
height: "100px !important"
},
"& .MuiTextField-root > .MuiOutlinedInput-root": {
padding: "8.5px 14px"
}
}}
id="description"
className={isShake ? classes.shake : null}
/>
</div>
);
}
your shaking animation code is not working, make changes in the keyframes for desired animation.
upon 100 character the animation class will be added. but once added you may need to remove the class, inorder to display the animation once again if the character is 100.
reference: https://stackoverflow.com/questions/58948890/how-to-apply-custom-animation-effect-keyframes-in-mui[1]

How to pass a react-transition-group TransitionComponent to react-bootstrap Toast?

I have the below custom <CompleteToast/> component built using react-bootstrap. I want to apply custom CSS transitions, but don't understand how to achieve this. The docs say I should pass a custom react-transition-group TransitionComponent but don't expand on how or give an example.
I've tried looking at the default <ToastFade/> but don't understand Typescript. I've tried passing <ToastTransition/> as a reference but the children don't render (the reference works as "Exit animation complete!" is logged). Passing <ToastTransition children={...}/> causes errors.
What approach should I be taking here? Is this possible without TypeScript?
My instinct is that I need to figure out how ToastTransition can automatically inherit Toast's children.
CompleteToast.js
import PropTypes from 'prop-types';
import {Toast} from "react-bootstrap";
import {useContext, useEffect, useRef, useState} from "react";
import {IoCheckmarkCircleOutline} from "react-icons/all";
import {GlobalAppContext} from "../../App";
import ToastTransition from "../react-transition-group/ToastTransition";
const dividers = {
s: 1000,
m: 60000,
h: 3.6e+6,
d: 8.64e+7,
w: 6.048e+8,
mo: 2.628e+9,
y: 3.154e+10
}
const calculateTimeDiff = (timeInMs) => {
const diff = Date.now() - timeInMs;
switch (true) {
case (diff < dividers.s):
return "Just now";
case (diff < dividers.m):
return Math.floor(diff / dividers.s) + "s";
case (diff < dividers.h):
return Math.floor(diff / dividers.m) + "m";
case (diff < dividers.d):
return Math.floor(diff / dividers.h) + "h";
case (diff < dividers.w):
return Math.floor(diff / dividers.d) + "d";
case (diff < dividers.mo):
return Math.floor(diff / dividers.w) + "w";
case (diff < dividers.y):
return Math.floor(diff / dividers.mo) + "mo";
case (diff >= dividers.y):
return Math.floor(diff / dividers.y) + "y";
default:
return diff + "ms";
}
}
const timeUntilNext = (from, unit = "s") => {
let divider = dividers[unit];
return (Math.ceil(from / divider) * divider) - from;
}
function CompleteToast({show, title, timestamp, bodyText, headerClass, updateDeleteIds, id, deleteIds}) {
const [showState, setShowState] = useState(show);
const [timestampUpdated, setTimestampUpdated] = useState(null);
const [timestampState, setTimestampState] = useState(timestamp);
const [timestampText, setTimestampText] = useState("Just now");
const setToasts = useContext(GlobalAppContext)[0].setStateFunctions.toasts;
const shownOnce = useRef(false);
const deleteTimeout = useRef(null);
const hovering = useRef(false);
const close = () => {
if (!hovering.current) {
setShowState(false);
}
}
const setDeleteTimeout = () => {
if (shownOnce.current && !hovering.current) {
deleteTimeout.current = setTimeout(() => {
setToasts(prevState => prevState.filter(x => x.id !== id));
}, 2000)
}
}
useEffect(() => {
if (showState) {
setTimestampUpdated(Date.now());
setTimestampState(Date.now);
} else {
setDeleteTimeout();
}
}, [showState]);
useEffect(() => {
if (!showState) {
setShowState(true);
shownOnce.current = true;
}
}, []);
useEffect(() => {
if (showState) {
//timestamp has been updated and the toast is still showing - update the text to the current time difference
const timeDiff = calculateTimeDiff(timestampState);
setTimestampText(timeDiff);
setTimeout(() => {
//trigger new update to timestamp text on the next second
setTimestampUpdated(Date.now());
}, timeUntilNext(Date.now(), "s"));
}
}, [timestampUpdated]);
return (
<Toast style={{whiteSpace: "pre-wrap"}}
onClose={close}
onClick={close}
onMouseEnter={() => {
hovering.current = true;
if (deleteTimeout.current) {
clearTimeout(deleteTimeout.current);
deleteTimeout.current = null;
setShowState(true);
}
}}
onMouseLeave={() => {
hovering.current = false;
setTimeout(close, 3000);
}}
show={showState}
delay={4000}
autohide
id={id}
transition={ToastTransition}
className="cursor-pointer">
<Toast.Header className={headerClass} closeButton={false}>
<IoCheckmarkCircleOutline className={"smallIcon me-2"}/>
<p className="fs-5 my-0 me-auto">{title}</p>
<small>{timestampText}</small>
</Toast.Header>
<Toast.Body className={"position-relative"}>
{bodyText}
</Toast.Body>
</Toast>
);
}
CompleteToast.propTypes = {
show: PropTypes.bool,
handleClick: PropTypes.func,
buttonText: PropTypes.string,
buttonVariant: PropTypes.string,
bodyText: PropTypes.string,
headerClass: PropTypes.string,
title: PropTypes.string
};
CompleteToast.defaultProps = {
show: false,
timestamp: Date.now(),
timestampText: Date.now(),
title: "Success",
bodyText: "The operation was completed successfully",
headerClass: "bg-success text-white",
buttonVariant: "primary"
}
export default CompleteToast;
ToastTransition.js
import {Transition} from 'react-transition-group';
//copied from react-transition-group docks - aim is to getit working, then customise the actual transition
const duration = 300;
const defaultStyle = {
transition: `opacity ${duration}ms ease-out, maxHeight opacity ${duration}ms ease-out`,
opacity: 0,
maxHeight: 0,
}
const transitionStyles = {
entering: {opacity: 1, maxHeight: "200px"},
entered: {opacity: 1, maxHeight: "200px"},
exiting: {opacity: 0, maxHeight: 0},
exited: {opacity: 0, maxHeight: 0},
};
const ToastTransition = ({in: inProp, children}) => (
<Transition
in={inProp}
timeout={duration}
onExited={() => {
console.log("Exit animation complete!")
}}>
{state => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{children}
</div>
)}
</Transition>
);
export default ToastTransition;
I'm an idiot - it's as simple as calling the children prop which is 'special' and is passed automatically.
It was visually rendering because I needed to manually set a show class on the <CompleteToast/> component, i.e.:
className="cursor-pointer show"
The react-transition-group component handles the actual showing/hiding it seems (this is inconsistent with the default behaviour, but as far as I can see it's not cuasing issues so far).
Working ToastTransition.js (need to tweak CSS)
import {Transition} from 'react-transition-group';
const duration = 500;
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out, max-height ${duration}ms ease-out`,
opacity: 0,
maxHeight: 0
}
const transitionStyles = {
entering: {opacity: 1, maxHeight: "200px"},
entered: {opacity: 1, maxHeight: "200px"},
exiting: {opacity: 0, maxHeight: 0},
exited: {opacity: 0, maxHeight: 0},
};
const ToastTransition = ({in: inProp, children}) => {
return (
<Transition
in={inProp}
timeout={duration}
onExited={() => {
children.props.onExited();
}}>
{(state) => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{children}
</div>
)}
</Transition>
)
}
export default ToastTransition;

Why is the state changing by itself in react?

What I'm Trying to do: I have the following react component, which is chat box, which toggles to view on clicking a button. The chatBox will be opened or closed according the value in the state 'open'. When the user clicks on the chat button, the function 'toggleChat' is run, which just toggles the value of 'open' state between true and false.
Problem: Now the problem is, when a new message is received, I am trying to keep the count of unread messages, if the chatBox isn't 'opened'. But it fails. In my opinion it should work, but the 'open' state is not what I expect it to be sometimes. Sometimes, even though the chatBox is opened, the open state inside is 'false'.
Minified Code
export default function Chat (props) {
const [open, setOpen] = useState(false);
const [unreadMsgs, setUnreadMsgs] = useState(0);
useEffect(() => {
socket.emit('join', uuid);
socket.on('newMsg', msg => {
setMsgArr(prevMsgArr => [ ...prevMsgArr, { type: 'received', msg }]);
if(!open) setUnreadMsgs(prevCount => prevCount + 1);
});
}, []);
const toggleChat = () => {
if(open) setOpen(false);
else {
setOpen(true);
setUnreadMsgs(0);
}
}
Entire code
import { io } from 'socket.io-client';
import React, {useState, useRef, useEffect} from 'react';
import Menu from '#material-ui/core/Menu';
import MailIcon from '#material-ui/icons/Mail';
import CloseIcon from '#material-ui/icons/Close';
import IconButton from '#material-ui/core/IconButton';
import { makeStyles } from '#material-ui/core/styles';
import Badge from '#material-ui/core/Badge';
import Tooltip from '#material-ui/core/Tooltip';
import FiberManualRecordIcon from '#material-ui/icons/FiberManualRecord';
const socket = io('http://127.1.1.1:4000');
const useStyles = makeStyles({
paper : {
height : 300,
},
list : {
height : '100%',
boxSizing : 'border-box',
},
chatContainer : {
position : 'relative',
height : '95%',
width : 300,
},
chatBox : {
height : '82%',
position : 'absolute',
top : '8%',
width : '100%',
overflowY : 'auto',
},
msgForm : {
width : '100%',
padding : 10,
position : 'absolute',
bottom : 0,
height : '6%',
textAlign : 'center',
},
anchor : {
top : 7,
},
badge : {
background : '#007eff',
},
});
export default function Chat (props) {
const uuid = props.uuid;
const classes = useStyles();
const [open, setOpen] = useState(false);
const [activeStatus, setActiveStatus] = useState('Offline');
const [msgArr, setMsgArr] = useState([]);
const chatBtnRef = useRef();
const chatBoxRef = useRef();
const msgInputRef = useRef();
//working on showing count of unread messages
const [unreadMsgs, setUnreadMsgs] = useState(0);
useEffect(() => {
socket.emit('join', uuid);
socket.on('newMsg', msg => {
setMsgArr(prevMsgArr => [ ...prevMsgArr, { type: 'received', msg }]);
setTimeout(() => {
chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
}, 50);
console.log(open);
if(!open) setUnreadMsgs(prevCount => prevCount + 1);
});
if(props.isReceiver) {
socket.emit('isReceiverOnline', uuid);
socket.on('isSenderOnline', () => {
setActiveStatus('Online');
});
} else {
socket.emit('isSenderOnline', uuid);
socket.on('isReceiverOnline', () => {
setActiveStatus('Online');
socket.emit('isSenderOnline', uuid);
});
}
socket.on("isOffline", () => {
setActiveStatus('Offline');
});
return () => {
socket.off('isOffline');
socket.off('newMsg');
socket.off('isOnline');
}
}, []);
const handleMsgSend = e => {
e.preventDefault();
let msg = msgInputRef.current.value;
setMsgArr([ ...msgArr, { type: 'sent', msg }]);
e.currentTarget.reset();
socket.emit('newMsg', {uuid, msg});
setTimeout(() => chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight, 50);
}
const toggleChat = () => {
if(open) setOpen(false);
else {
setOpen(true);
setUnreadMsgs(0);
}
}
return (
<>
<Tooltip title={ `${activeStatus}` }>
<IconButton>
<FiberManualRecordIcon style={{ height : 14, width : 14, fill : (activeStatus == "Offline")? '#00000057' : 'rgb(136 210 130)'}}/>
</IconButton>
</Tooltip>
<Tooltip title={ `${unreadMsgs} unread messages` }>
<IconButton ref={chatBtnRef} onClick={ toggleChat }>
<MailIcon style={{ fill:'white' }}/>
</IconButton>
</Tooltip>
<Menu
classes={{ paper : classes.paper, list : classes.list}}
anchorEl={chatBtnRef.current}
keepMounted
open={ open }
>
<div style={{ position : 'relative', zIndex : '1', height : '5%', textAlign:'right'}}>
<IconButton onClick={ toggleChat }>
<CloseIcon />
</IconButton>
</div>
<div className={classes.chatContainer}>
<div ref={ chatBoxRef } className={ classes.chatBox }>
{
msgArr.map((msgObj, index) => {
return (
<div key={index} className={`msg-container ${(msgObj.type == 'sent')? 'myMsg' : 'hisMsg'}`}>
<span className='msg'>
{ msgObj.msg }
</span>
</div>
)
})
}
</div>
<form className={ classes.msgForm } onSubmit={ handleMsgSend }>
<input
style ={{
padding : 3,
fontSize : 14,
borderRadius : 3,
width : 250
}}
ref={msgInputRef} type="text"
className={classes.msgInput}
placeholder="Type your Msg here."/>
</form>
</div>
</Menu>
</>
);
}
Try adding the dependencies to useEffect that are used in the function.
Also add const [msgArr, setMsgArr] = useState([]); if it is not there.
useEffect(() => {
socket.emit('join', uuid);
}, []); // you can pass socket here as it's not going to change
useEffect(() => {
socket.on("newMsg", (msg) => {
setMsgArr((prevMsgArr) => [...prevMsgArr, { type: "received", msg }]);
if (!open) setUnreadMsgs((prevCount) => prevCount + 1);
});
return () => socket.removeAllListeners("newMsg"); // remove earlier registered handlers
}, [open]);
Updated the De-registration of the event handler if a dependency changes. (based on comment discussion and answer https://stackoverflow.com/a/67073527/8915198)
Already given the explanation for closures in the comments but the working solution for you should look something like below :-
useEffect(() => {
socket.emit('join', uuid);
}, []);
useEffect(() => {
function getNewMsg(msg){
setMsgArr((prevMsgArr) => [...prevMsgArr, { type: "received", msg }]);
if (!open) setUnreadMsgs((prevCount) => prevCount + 1);
}
socket.on("newMsg",getNewMsg)
return ()=>socket.removeListener("newMsg",getNewMsg);
}, [open]);
Basically the cleanup step I think is necessary as you do with major event paradigms.

Trigger a CSS animation once the element is in view with Material-UI

I've done some research on how to trigger CSS animation once the element comes into view, and I've found the answer that makes use of IntersectionObserver and element.classList.add('.some-class-name')
Above method is demonstrated in pure CSS, but I want to implement it with Material-UI. Here is my code.
import React, { useEffect } from 'react';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
height: '100vh'
},
box: {
opacity: 0,
width: 100,
height: 100,
backgroundColor: 'red'
},
animated: {
animationName: '$fadein',
animationDuration: '1s'
},
'#keyframes fadein': {
'0%': {
opacity: 0
},
'100%': {
opacity: 1
}
},
}));
function App() {
const classes = useStyles();
useEffect(() => {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
// trigger animation
entry.target.classList.add('animated');
// remove observer
observer.unobserve(entry.target);
}
});
});
const element = document.getElementById('item');
observer.observe(element);
}, []);
return (
<div>
<div className={classes.root} />
<div id="item" className={classes.box} />
</div>
);
};
export default App;
Unfortunately, the above code isn't working and I think it's because the className 'animated' does not exist. I know Material-UI has internal logic that generates the unique className, so my question is how do I figure out the real className of 'animated'? Or, is there a better way to go about this? Any help would be appreciated.
This is what I came up with.
import React, { useEffect, useState, useRef } from 'react';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
height: '100vh'
},
box: {
opacity: 0,
width: 100,
height: 100,
backgroundColor: 'red'
},
animated: {
animationName: '$fadein',
animationDuration: '1s',
animationFillMode: 'forwards'
},
'#keyframes fadein': {
'0%': {
opacity: 0
},
'100%': {
opacity: 1
}
}
}));
function App() {
const classes = useStyles();
const BoxSection = (props) => {
const [isVisible, setVisible] = useState(false);
const domRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => setVisible(entry.isIntersecting));
});
observer.observe(domRef.current);
return () => observer.unobserve(domRef.current); // clean up
}, []);
return (
<div className={`${classes.box} ${isVisible ? classes.animated : ''}`} ref={domRef}>
{props.children}
</div>
);
};
return (
<div>
<div className={classes.root} />
<BoxSection />
</div>
);
}
export default App;
Basically, I've decided to use state to trigger animation by adding the class like above. I've got some pointers from this article, if anyone is interested.

React Masonry Layout has bugs rendering images with fade-in effect

Code is here: https://codesandbox.io/s/gatsby-starter-default-ry8sm
You can try demo: https://ry8sm.sse.codesandbox.io/
Every picture is an Enlarger component which will zoom in when you click on it. And they are designed to show up sequentially by fading in. I use Ref to track every Enlarger and here is the code snippet for it.
import Img from "react-image-enlarger"
class Enlarger extends React.Component {
state = { zoomed: false, opacity: 0 }
toggleOpacity = o => {
this.setState({ opacity: o })
}
render() {
const { index, orderIndex, src, enlargedSrc, onLoad } = this.props
return (
<div style={{ margin: "0.25rem" }} onLoad={onLoad}>
<Img
style={{
opacity: this.state.opacity,
transition: "opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94)",
transitionDelay: `${orderIndex * 0.07}s`,
}}
zoomed={this.state.zoomed}
src={src}
enlargedSrc={enlargedSrc}
onClick={() => {
this.setState({ zoomed: true })
}}
onRequestClose={() => {
this.setState({ zoomed: false })
}}
/>
</div>
)
}
}
export default Enlarger
And I have a Masonry component which will achieve the Masonry layout
import React, { Component } from "react"
import imagesLoaded from "imagesloaded"
import PropTypes from "prop-types"
import TransitionGroup from "react-transition-group/TransitionGroup"
class MasonryGrid extends Component {
componentDidMount() {
window.onload = this.resizeAllGridItems()
window.addEventListener("resize", this.resizeAllGridItems)
let allItems = document.getElementsByClassName("masonry-grid--item")
for (let x = 0; x < allItems.length; x++) {
imagesLoaded(allItems[x], this.resizeInstance)
}
}
resizeAllGridItems = () => {
let allItems = document.getElementsByClassName("masonry-grid--item")
for (let x = 0; x < allItems.length; x++) {
this.resizeGridItem(allItems[x])
}
}
resizeGridItem = item => {
let grid = document.getElementsByClassName("masonry-grid")[0]
let rowHeight = parseInt(
window.getComputedStyle(grid).getPropertyValue("grid-auto-rows")
)
let rowGap = parseInt(
window.getComputedStyle(grid).getPropertyValue("grid-row-gap")
)
let rowSpan = Math.ceil(
(item.querySelector(".content").getBoundingClientRect().height + rowGap) /
(rowHeight + rowGap)
)
item.style.gridRowEnd = "span " + rowSpan
}
resizeInstance = instance => {
let item = instance.elements[0]
this.resizeGridItem(item)
}
render() {
const MasonryGrid = {
display: "grid",
gridGap: `${this.props.gridGap}`,
gridTemplateColumns: `repeat(auto-fill, minmax(${
this.props.itemWidth
}px, 1fr))`,
gridAutoRows: "10px",
}
return (
<TransitionGroup>
<div className="masonry-grid" style={MasonryGrid}>
{this.props.children.length >= 1 &&
this.props.children.map((item, index) => {
return (
<div className="masonry-grid--item" key={index}>
<div className="content">{item}</div>
</div>
)
})}
</div>
</TransitionGroup>
)
}
}
MasonryGrid.defaultProps = {
itemWidth: 250,
gridGap: "6px 10px",
}
MasonryGrid.propTypes = {
itemWidth: PropTypes.number,
gridGap: PropTypes.string,
}
export default MasonryGrid
The problem is, if you look at the demo, when you click on tab project1, you will see the pictures show up on top of each other and doesn't spread well as intended. But once you resize the browser a little bit, they becomes normal and form the Masonry layout I wanted. I suspect it has something to do with the fade-in effect I implemented but I don't know how to fix it.

Categories