How to animate conditionally-rendered components? - javascript

I'm trying to use React Motion UI Pack to animate the slide-in/slide-out animation for my Side Navigation. This is it:
constructor(props){
super(props);
this.state = {
isThere: false,
showOverlay: false
}
this.updatePredicate = this.updatePredicate.bind(this);
this.handleToggleClick = this.handleToggleClick.bind(this);
this.handleOverlayClick = this.handleOverlayClick.bind(this);
}
componentDidMount() {
this.updatePredicate();
window.addEventListener("resize", this.updatePredicate);
}
componentWillUnmount() {
window.removeEventListener("resize", this.updatePredicate);
}
updatePredicate() {
this.setState({ isThere: window.innerWidth > this.props.breakWidth })
}
handleToggleClick(){
this.setState({
isThere: true,
showOverlay: true
})
}
handleOverlayClick(){
this.setState({
isThere: false,
showOverlay: false
});
}
let sidenav = (
<Tag {...attributes} className={classes} key="sidenav">
<ul className="custom-scrollbar">
{src &&
<li>
<div className="logo-wrapper">
<a href={href}>
<img src={src} className="img-fluid"/>
</a>
</div>
</li>
}
{children}
</ul>
</Tag>
);
return (
<div>
{ isThere ? (
<Transition
component={false}
appear={{ opacity: 0.2, translateX: -300 }}
enter={{ opacity: 1, translateX: 0 }}
leave={{ opacity: 0.2, translateX: -300 }}
>
{ sidenav }
</Transition>
) : (
<Button color="primary" onClick={this.handleToggleClick} key="sideNavToggles">ClickMe</Button>
) }
{showOverlay && (
<div id="sidenav-overlay" onClick={this.handleOverlayClick} key="overlay"></div>
)}
</div>
);
}
}
The utility seems awesome, yet there is something I cannot wrap my head around. My component renders button or the sidenav depending on the breakWith prop. Clicking on the rendered button causes the SideNav to slide-in anyway, this time together with an overlay. Transition allowed for a smooth slide-in animation, but now I would like to apply a slide-out one upon clicking on the overlay.
Few hours passed and I'm starting to think it is impossible. Rendering of components is conditional & state-based (the isThere ? (... part in render()), right? As the pack offers no willLeave props, there seems to be no time to animate the leave in between the change of state and the re-render with the conditionally-rendered element already missing.
Or am I missing something?

yes - the answer found here effectively addresses the problem. To solve it, I moved the conditional logic of the component up, created appropriate variable, and encapsulated it inside a <Transition> in render(). If there is a lesson to be learned here, it is that <Transition> from Reakt Motion UI Pack (and, perhaps, elsewhere) does not fire its leave animation if surrounded by a conditional statement, making it impossible to use it together with ternary operator if you don't want the false component to be animated as well.

Related

show more and show less dinamicaly for each div

when I click the particular show more button the content should be displayed, the condition is the whole component should not re-rendering
I have used a useState when I clicked the button it is re-rendering the whole component
it is taking a long time to re-render every div
give an easy solution for this.
const [arr,setmarr] =useState([])
const oncl=(e)=>{
setarr((prev)=>[...prev,e.target.value])
}
return{
divarray.map((i,j)=>{
{console.log("tdic)")}
return(
<Commentbox divarr={arr[j]} value={j} oncl={(e)=>oncl(e) } />
)
}
}
Commentbox component
return
<div>
div{j}
// some icons here
{divarr && <div> right side div </div>}
<button onClick={(e)=>{oncl(e)}} value={j} >see more</button >
</div>
before onClick on showmore
after showmore button has been clicked on the second div
you should change each box to a component to solve this problem.
make that component with class base component because you need getSanpShotBeforeUpadte.
getSanpShotBeforeUpadte: you can control your component's render with this method.dont forget this method will give you nextProps,nextState and snapshot as parameter
class Box extends Component{
state = {
// more
showMore: false,
}
getSnapshotBeforeUpdate(nextProps,nextState){
// OTHER CONDITIONS
if(nextState.showMore !== this.state.showMore) return true
return false
}
render(){
return (
<div>
{/* CODE ... */}
<div style={{display: this.state.showMore ? 'block' : 'none'}}>
HERE IS A TEXT
</div>
<button onClick={()=>this.setState({showMore: !this.state.showMore})}>show more</button>
</div>
)
}
}

React resizable only resizing the width of the box, not the height

I have a React-mui draggable dialog component on which I am using resizable box:
return (
<StyledDialog
open={open}
classes={{root: classes.dialog, paper: classes.paper}}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog"
>
<ResizableBox
height={520}
width={370}
minConstraints={[300, 500]}
maxConstraints={[Infinity, Infinity]}
className={classes.resizable}
>
<DialogContent classes={{root: classes.dialogContent}} id="draggable-dialog">
<IconButton className={classes.clearIcon} aria-label="Clear" onClick={onClose}>
<ClearIcon/>
</IconButton>
<iframe
src={hjelpemiddel.url}
title={hjelpemiddel.navn}
width="100%"
height="100%">
</iframe>
</DialogContent>
</ResizableBox>
</StyledDialog>
);
I would like to resize the iframe inside the dialog along with the ResizableBox. But, it seems I can only resize the width of the ResizableBox, and not the height of the box, at least the maximum height seems to be the one that is set initially. How can I fix that, so that I can resize the height as well?
UPDATE
Codesanbox is available here.
FYI, for some reason sometimes import fail message appears, but everything works fine if you refresh the page of the codesandbox.
In your CodeSandBox, based on my testing, the events for dragging & resizing are simultaneously firing. You could use the cancel prop of react-draggable so that the dragging would not occur when the resize handle is the component being interacted with.
<Draggable
cancel={".react-resizable-handle"}
...
When you do this, the draggable element will not be updating its CSS transform: translate property anymore while resizing - for this, you can opt to control your Draggable component's position. Translate/Set X & Y as necessary to retain its position while resizing. Note that the x & y state & state setters should be residing on a common parent/ancestor among these components that you will be passing down as props.
export default function App() {
// have the source of truth for the positions on a common parent/ancestor
const [x, setX] = React.useState(0);
const [y, setY] = React.useState(0);
return (
<div className="App">
<PDFDialog x={x} y={y} setX={setX} setY={setY} />
</div>
);
}
...
class PDFDialog extends React.Component {
state = {
open: true
};
render() {
const { open } = this.state;
const { classes } = this.props;
return (
<StyledDialog
open={open}
classes={{ root: classes.dialog, paper: classes.paper }}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog"
PaperProps={{
x: this.props.x,
y: this.props.y,
setX: this.props.setX,
setY: this.props.setY
}}
>
<ResizableBox
height={520}
width={370}
minConstraints={[300, 500]}
maxConstraints={[Infinity, Infinity]}
className={classes.resizable}
onResize={(e) => {
if (e.movementX !== 0) {
this.props.setX((prev) => prev + e.movementX);
} else if (e.movementY !== 0) {
this.props.setY((prev) => prev + e.movementY / 2);
}
}}
></ResizableBox>
...
// refactored draggable component:
<Draggable
position={{ x: x, y: y }}
cancel={".react-resizable-handle"} // <-- cancel the dragging if resize is interacted with
onDrag={(e) => {
if (e.movementX !== 0) {
setX((prev) => prev + e.movementX);
}
if (e.movementY !== 0) {
setY((prev) => prev + e.movementY);
}
}}
>
<Paper {...props} />
</Draggable>
(On my CodeSandBox, I've gotten rid of constraints such as minimum height & width to clearly show the example)
You can bind the event of resize of window, calculate the new height and width and pass it to the resizable-box of yours
object.addEventListener("resize", function() {
//Here you can write logic to apply resizing on the resizable-box
});
Obviously, react-resizable uses inline CSS to handle width and height of a box, and for your issue, I simulate this issue, pay attention to the below screenshot from Google chrome devTools:
The react-resizable-box has an important flag and it overwrites the inline height value so in the view, I have the following behavior:
Your information is not enough so I cannot say directly your issue cause or causes but it is very probable CSS overwriting is the root cause of this issue.
So, inspect the resizable-box on your project and seek to find CSS overwriting.
Update after adding re-produce the issue
Actually, based on my last answer something overwrite your height now in the re-production sandbox, I delete the iframe tag, and everything works well, you pass a height="100%" attribute to your iframe tag and it prevents the change of height. Also, you pass a minConstraints={[300, 500]} to your ResizableBox component, so it could not have a smaller height than 500px.

My animation is not working when re rendering my react component?

So, my problem is that I have a component, I associated an animation to it and it is working when the component is rendered for the first time, but on an event click I change some conditions and some props associated to this component, But my element is not re rendered, it is just changing what has been changed, that means that the element is not removed from the dom et added to the DOM again, that's why I am not able to see the animation again, so it is not re-rendered or I just did not get what re render means.
I tried some solutions of course, but I am stuck, I tried to use this method :
this.forceUpdate();
But again, I am still not getting anything.
I dont think I have to write the whole code I wrote, becuase it is a lot and includes many other things but This is what I think is needed.
methodWillReceiveProps in my component :
componentWillReceiveProps(props) {
if (props.isRerendered) {
this.forceUpdate();
}
}
props.isRendered is returning true everytime, I checked with some console.log methods.
This is what is rendered :
render() {
return (
<div
className={cs({
"tls-forms": true,
"tls-forms--large": this.props.type === "S",
"tls-forms--medium tls-forms--login": !(this.props.type === "S")
})}
>
// content here
</div>);
}
And here is the sass file and the simple fading animation :
.tls-forms {
animation: formFading 3s;
// childs properties here
}
#keyframes formFading {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
I will really appreciate any help given.
You could make use of keys that react is using to determine whether something has changed. This means that your render method should look something like this:
import shortid from "shortid";
getRandomKey = () => {
return shortid.generate();
}
render() {
return (
<div
key={this.getRandomKey()}
className={cs({
"tls-forms": true,
"tls-forms--large": this.props.type === "S",
"tls-forms--medium tls-forms--login": !(this.props.type === "S")
})}
>
// content here
</div>);
}
Since you need to run animation on each render, you'll need to generate some random key every time (that's why we are calling this.getRandomKey() on each render). You can use whatever you like for your getRandomKey implementation, though shortid is pretty good for generating unique keys.
One way of animating a component is to attach a CSS class to it. But, when animation is done, you have to detach the CSS class so that you can re-attach when you want to animate again.
Here is a basic example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
animateFlag: false
};
}
componentDidUpdate() {
if (this.state.animateFlag) {
setTimeout(() => {
this.setState({ animateFlag: false });
}, 3000);
}
}
render() {
return (
<div className="App">
<button
onClick={() =>
this.setState({ animateFlag: !this.state.animateFlag })
}
>
{this.state.animateFlag ? "Wait" : "Re-animate"}
</button>
<div className={this.state.animateFlag ? "text animate" : "text"}>
Hello CodeSandbox
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.text {
font-size: 40px;
}
.text.animate {
animation: formFading 3s;
}
#keyframes formFading {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Note that, I am setting animateFlag to false in ComponentDidUpdate, so that when I click the Re-animate button again, I can re-attach the animate class to the div element.
I set timeout duration to 3000ms because, the animation takes 3000ms.

React, Using Refs to scrollIntoView() doesn't work in componentDidUpdate()

I'm using Redux in my app, inside a Component I want to scroll to an specific div tag when a change in the store happens.
I have the Redux part working so it triggers the componentDidUpdate() method (I routed to this compoennt view already).
The problem as far as I can tell, is that the method scrollIntoView() doesn't work properly cos componentDidUpdate() has a default behavior that scrolls to the top overwriting the scrollIntoView().
To work-around it I wrapped the function calling scrollIntoView() in a setTimeout to ensure that happens afeterwards.
What I would like to do is to call a preventDefault() or any other more elegant solution but I can't find where to get the event triggering the 'scrollTop'
I looked through the Doc here: https://facebook.github.io/react/docs/react-component.html#componentdidupdate
and the params passed in this function are componentDidUpdate(prevProps, prevState) ,since there is no event I don't know how to call preventDefault()
I've followd this Docs: https://facebook.github.io/react/docs/refs-and-the-dom.html
And tried different approaches people suggested here: How can I scroll a div to be visible in ReactJS?
Nothing worked though
Here is my code if anyone has any tip for me, thanks
class PhotoContainer extends React.Component {
componentDidUpdate(){
setTimeout(() => {
this.focusDiv();
}, 500);
}
focusDiv(){
var scrolling = this.theDiv;
scrolling.scrollIntoView();
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>this is the div I'm trying to scroll to</div>
</div>
)
};
}
Ok it's been a while but I got it working in another project without the setTimeOut function so I wanted to answer this question.
Since Redux pass the new updates through props, I used the componentWillRecieveProps() method instead of componentDidUpdate() , this allowes you a better control over the updated properties and works as expected with the scrollIntoView() function.
class PhotoContainer extends React.Component {
componentWillReceiveProps(newProps) {
if (
this.props.navigation.sectionSelected !==
newProps.navigation.sectionSelected &&
newProps.navigation.sectionSelected !== ""
) {
this.focusDiv(newProps.navigation.sectionSelected);
}
}
focusDiv(section){
var scrolling = this[section]; //section would be 'theDiv' in this example
scrolling.scrollIntoView({ block: "start", behavior: "smooth" });//corrected typo
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>
this is the div I am trying to scroll to
</div>
</div>
)
};
}
I also struggled with scrolling to the bottom of a list in react that's responding to a change in a redux store and I happened upon this and a few other stackoverflow articles related to scrolling. In case you also land on this question as well there are a few ways this could be a problem. My scenario was that I wanted a 'loading' spinner screen while the list was rendering. Here are a few wrong ways to do this:
When loading = true, render spinner, otherwise render list.
{loading ?
<Spinner />
:
<List />
}
as stated above this doesn't work because the list you might want to scroll to the bottom of isn't rendered yet.
When loading set the display to block for the spinner and none for the list. When done loading, reverse the display.
<div style={{display: loading ? 'block' : 'none'>
<Spinner />
</div>
<div style={{display: loading ? 'none' : 'block'>
<List />
</div>
This doesn't work either since the list you want to scroll to the bottom of isn't actually being displayed likely when you call the scroll.
The better approach for the above scenario is to use a loading that acts as an overlay to the component. This way both the spinner and list are rendered and displayed, the scroll happens, and when the loading is complete, the spinner can be de-rendered or set to be invisible.

React, transition group - css transitions when swapping components

I have a component that swaps child components in ( it is a small quiz, and the child components are questions. I am trying to animate the swap of components using ReactCSSTransitionGroup.
So, I have a function that renders the components, that I am swapping wrapping in the ReactCSSTransitionGroup like so :
render() {
return (
<div className=" questions-container">
<ReactCSSTransitionGroup transitionName="switch">
{this.renderQuiz(step)}
</ReactCSSTransitionGroup>
</div>
);
}
The renderQuiz function is just a switch statement that returns the correct component based on the right state - something like this:
renderQuiz(step) {
switch (step) {
case 0:
return (
<StepZero />
);
case 1:
return (
<StepOne />
);
....
}
}
Step is just a local component variable (this.state.step). I see this only partially working - when the first component loads I see it fade in, but there is no transition between switching the components.
Here is the CSS associated with the transition:
.switch-enter {
opacity: 0.01;
}
.switch-enter.switch-enter-active {
opacity: 1.0;
transition: opacity 500ms ease-in;
}
.switch-leave {
opacity: 1.0;
}
.switch-leave.switch-leave-active {
opacity: 0;
transition: opacity 500ms ease-out;
}
Unsure how to get this to work properly. Any input would be greatly appreciated. Thanks!
I think this is due to missing transitionEnterTimeout and transitionLeaveTimeout as they determine how long the *-active classes remain applied.
I think they default to zero, which would visually mean no transition since your transition CSS is on the -active class.
render() {
return (
<div className=" questions-container">
<ReactCSSTransitionGroup
transitionName="switch"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
{this.renderQuiz(step)}
</ReactCSSTransitionGroup>
</div>
);
}

Categories