Trigger CSS Transition on component prop changes - javascript

I want to fade in/out a component whenever its prop changes. Some basic examples I've found achieve this via a boolean condition that toggles the element's styles, but I want to update the toggle state via a prop change.
For example, in my code sandbox, I have a few buttons that when clicked, will update the text on screen. I want to be able to fade in/out this text whenever the user clicks on a different option.
class Example extends Component {
constructor(props) {
super(props);
this.state = {
current: "dog",
isVisible: false
};
}
componentDidMount() {
this.setState({ isVisible: true });
}
handleOnClick = option => {
this.setState({ current: option });
};
// only used to test css transition
forceToggleState = () => {
this.setState(prevState => ({ isVisible: !prevState.isVisible }));
};
render() {
const options = ["dog", "cat", "rabbit"];
return (
<div>
{options.map((option, index) => (
<button onClick={() => this.handleOnClick(option)} key={index}>
{option}
</button>
))}
<div style={this.state.isVisible ? styles.toggleIn : styles.toggleOut}>
{this.state.current}
</div>
<button onClick={this.forceToggleState}>toggle me</button>
</div>
);
}
}
const styles = {
toggleIn: {
opacity: 1,
transition: "all 0.5s ease",
border: "1px solid red"
},
toggleOut: {
opacity: 0,
transition: "all 0.5s ease-in"
}
};
current sandbox

Solution: https://codesandbox.io/s/react-example-lyf3f
I made a few edits to your sandbox to achieve the desired functionality.
Move options array into state
Pass this.state.current as a prop to an Option component
Create a toggleIn animation using an #keyframes to create the desired animation
Set the key prop equal to this.props.current in the Option component to make sure the DOM element remounts any time the this.props.current changes. This will ensure our animation runs each time the this.props.current changes.

Related

How to animate child prop changes in react?

I'm trying to create a component that takes a child prop and when this child prop changes I'd like the old component that was the child prop to animate out of screen and the new component to animate in. I attempted to do this using react-spring using the code below, you can run it here https://codesandbox.io/s/react-spring-issue-898-forked-ril7mc?file=/src/App.js.
What am I missing here? Shouldn't the component animate left when it unmounts and animate in from the right when it mounts?
/** #jsx jsx */
import { useState } from "react";
import { useTransition, animated } from "react-spring";
import { jsx } from "#emotion/core";
function FadeTransition({ children, onClick }) {
const [toggle, setToggle] = useState(true);
const transition = useTransition(toggle, {
from: {
opacity: 0,
transform: "translateX(100%)"
},
enter: {
opacity: 1,
transform: "translateX(0)"
},
leave: { transform: "translateX(-100%)" }
});
return (
<div>
{transition(
(props, item) =>
item && <animated.div style={props}>{children}</animated.div>
)}
<button
onClick={() => {
setToggle(!toggle);
onClick();
}}
style={{ marginTop: "2rem" }}
>
Change
</button>
</div>
);
}
export default function App() {
const [currentComponent, setCurrentComponent] = useState(0);
let components = [<div>Thing 1</div>, <div>Thing 2</div>];
const onClick = () => {
setCurrentComponent((currentComponent + 1) % 2);
};
return (
<div style={{ paddingTop: "2rem" }}>
<FadeTransition onClick={onClick}>
{components[currentComponent]}
</FadeTransition>
</div>
);
}
I didn't use rect-sprint as much,There is issue with toggle state so when thing 1 is rendered animation component (FadeTransition) is rendered with setting up toggle state to true.
Now when you click on change button we are setting up the toggle button to opposite of current state which will be false this time and without unmounting animation component (FadeTransition) it will update dom and so the item which is thing 2.
As we are passing toggle in useTransition and if it's values will be false then animation will not be performed.
So I have updated snippet of yours, Here is the working example codesandbox

React-select multi select update a prop when no value is selected

I am using react-select library to create a multi-selection menu. When there is no value selected I want a certain style width to be used on the div and when a user starts selecting values from the select dropdown, I want this width to be set to null so that the select component can use it's own auto adjusting width capability. I have the below code but I can't get the width to update synchronously as it updates after the next render call. Not sure how to make it update with the new width immediately. I know setState is async but if you use a callback in the setState function I would imagine it would render with the new state.
...
constructor(props) {
super(props);
this.state = {
dropBoxWidth: { width: 130 },
selectLength: 1
}
}
.....
handleChange = selectedOption => {
if (this.state.selectLength > 0) {
this.setState(
{ selectedOption, selectLength: selectedOption.length, dropBoxWidth: null },
() => console.log(`Option selected:`, this.state.selectedOption, this.state.selectLength, this.state.dropBoxWidth)
);
} else {
this.setState({ dropBoxWidth: { width: 130 }, selectLength: 1 }), () =>
console.log("New Dropbox Width ", this.state.dropBoxWidth)
}
};
render() {
return (
<div style={this.state.dropBoxWidth}>
<Select
closeMenuOnSelect={false}
isMulti
options={aList}
onChange={this.handleChange}
placeholder="Item Select"
/>
</div>
)
}
Again to be clear, I want the style width of the div to be set to 130 when there is no value selected. This can be when the page is opened or refreshed(constructor props has the width to 130), and if a user selects values then decides to clear all the selections from the menu.
You can use inital state in styles of component.
<Select
options={options}
styles={{
container: (provided, state) => ({
...provided,
width: !state.hasValue && "130px",
borderBottom: "1px dotted pink"
})
}}
/>
Like here: https://codesandbox.io/s/amazing-dawn-xwcld?file=/src/App.js:290-517
React select styles doc: https://react-select.com/styles
The reason your console.log isn't printing what you'd expect as the dropbox width is that it's not actually being passed as a callback.
If you format the setState call a bit, you can notice that the comma is actually after the closing parenthesis:
this.setState({
dropBoxWidth: { width: 130 },
selectLength: 1
}),
() => console.log("New Dropbox Width ", this.state.dropBoxWidth);
But the issue with the code is that you're using stale data when doing the this.state.selectLength comparison. You're not really interested in what the value was previously, but what the value is currently. The selectedOption parameter which react-select passes to you is either an array, or null, so you can check the length from there directly (i.e. instead of this.state.selectLength > 0, you can do selectedOption && selectedOption.length > 0).
However, since you mentioned that you're interested in the style and length because you want to style the parent div, you don't really need to store the style or the length - you can just store the selected option and derive the state in render():
const selectedStyle = { width: 130 }
class Selector extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null,
};
}
handleChange = (selectedOption) => {
// null or object array
this.setState({ selectedOption });
};
render() {
const { selectedOption } = this.state;
const anythingSelected = selectedOption && selectedOption.length > 0;
return (
<div style={!anythingSelected ? selectedStyle : null}>
<Select
closeMenuOnSelect={false}
isMulti
options={aList}
onChange={this.handleChange}
placeholder="Item Select"
/>
</div>
);
}
}

How can I force React to re-render all components on a page that come from array.map?

I have a parent component (Game) that renders children (Card) from an array. I also have a Menu component that does a callback to Game to change its state. When I change levels (button click from Menu), I want all current cards to fade out, then fade in with the new ones, so they mimic the fade-in CSS applied to newly rendered Cards. I've tried forceUpdate in a couple places, and passing dummy props to check for a change. Basicly, I want all cards currently on the page to re-render while the page renders new ones when needed.
Here is a snippet from Game:
{this._gameArray.map( (item, i) => {
let dummyProp = Math.random();
return <Card key={i}
...
dummyProp={dummyProp}/>
})}
And a snippet from Card:
componentWillReceiveProps(nextProps) {
...
if (nextProps.dummyProp !== this.props.dummyProps) {
this.forceUpdate();
}
}
I'm using componentWillReceiveProps for another purpose, and decided to try it for testing the dummyProp change. The new Cards fade in, but the ones before do not do anything.
React has some algorithms to define which components should be re-rendered, in your case React won't re-render the component because you use indexes as keys and indexes are the same. You need to update key between re-renderings.
As the simplest solution you can use the index + some trail in a key, something like this:
<Card key={index + '-' + Date.now()} card={card}/>;
In this case, the <Card/> component will be re-rendered every time when the state of the <Game/> component has changed.
class Card extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div class="card">Card {this.props.card.name}</div>
)
}
}
class Game extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0,
cards: [
{ name: 'Card1' },
]
}
}
increaseCounter = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<h2 onClick={this.increaseCounter}>Click me: {this.state.counter}</h2>
<h3>Will be re-rendered</h3>
{this.state.cards.map((card, index) => {
return <Card key={index + '-' + Date.now()} card={card} />;
})}
<h3>Won't be re-rendered</h3>
{this.state.cards.map((card, index) => {
return <Card key={index} card={card} />;
})}
</div>
)
}
}
ReactDOM.render(<Game />, document.querySelector("#app"))
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.card{
animation: fade-in-out 3000ms ease-in-out forwards;
}
#keyframes fade-in-out {
0% {
background-color: transparent;
}
50% {
background-color: red;
}
100%{
background-color: transparent;
}
}
<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="app"></div>
BUT, for real-life application, I would recommend you to consider the using of some React animation library like react-transition-group, because if you animate cards only with CSS, there is can be some flashing if re-rendering will happen faster than animation timeout.
You can simply try a different trick.
If you want all the components to re-render each time a level changes, you can use a boolean, set it to false when you start the change, and set it to true when you finish the change / after some time.
Then add a condition to the rendering of the elements.
{shouldRender && this._gameArray.map( (item, i) => {
let dummyProp = Math.random();
return <Card key={i}
...
dummyProp={dummyProp}/>
})}
when shouldRender is true the cards will be rendered, and when it's false they won't. That way each time it will be true they will all re-rendered and you'll have you effect.

Modal pop-up not working properly (want the background page to dim)

There's a button overlayed on a page, and once the button is pressed, the page is supposed to be dimmed and a modal popup shows up.
However, for my code, the popup just shows up but the background page isn't affected whatsoever. The modal opens and closes correctly, however.
Code for the button:
class MessageBrokerButton extends Component {
constructor(props) {
super(props)
this.state = {
isShowing: false
}
}
openModalHandler = (e) => {
this.setState({
isShowing: true
});
e.preventDefault();
}
closeModalHandler = () => {
this.setState({
isShowing: false
});
}
render() {
return (
<div>
<button className='message-broker' onClick={this.openModalHandler}>
Message Broker
</button>
<div className='message-icon'></div>
{ this.state.isShowing ?
<Popup
show={this.state.isShowing}
close={this.closeModalHandler}>
Test
</Popup>
: null
}
</div>
)
}
}
Code for the modal:
const Popup = (props) => {
return (
<div>
<button onClick={props.close}>CLOSE</button>
<p> {props.children}</p>
</div>
)
}
Relevant code for the page the button is on:
class Page extends Component {
constructor(props) {
super(props);
this.state = {
};
};
render() {
let page100 =
<div>
<MessageBrokerButton></MessageBrokerButton>
</div>
if (this.props.currentPage !== this.state.page) {
page100 = null;
}
return (
<form onSubmit={this.handleFormSubmit}>
{page100}
</form>
);
}
}
I know I'm definitely not taking into consideration the right styling rules, and also what to do about altering the Page class once the MessageBrokerButton is clicked. But I'm not exactly sure which parts to change and what I should be changing.
There are multiple ways of doing this. What you could do is create a new Shadow-component that is a child of Page (or whatever is your actual root component). This Shadow is then given a state prop that tells the component whether to render the shadow-div or not.
All this is pretty easy. The most complex part is usually to come up with some sensible state management system so that you can trigger the Shadow without passing the "enable shadow" handler all around. I suggest to check out Redux for this.
Use z-index style to make sure the shadow appears between the Page and the modal. For Example shadow z-index 1, modal container 2. Also, make sure both shadow and the modal container are positioned as absolute as they both should overlay the page.
Here's a pseudo-simplification:
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {
hasShadow: false,
modal: null, // Modal can be a React-component, like the Popup.
};
}
handleHasShadow = hasShadow => {
this.setState({ hasShadow });
}
handleSetModal = modal => {
this.setState({ modal });
}
render() {
return (
<div>
<form ...>
{
this.props.currentPage === this.state.page
? <MessageBrokerButton handleHasShadow={this.handleHasShadow} />
: null
}
</form>
<Shadow isEnabled={this.state.hasShadow} />
{/* You can give modal as a prop too! */}
<ModalContainer>{modal}</ModalContainer>
</div>
);
}
}
Shadow.jsx
const Shadow = ({ isEnabled }) => {
return isEnabled ? (
<div
style={{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
background: '#000000',
opacity: 0.5,
zIndex: 1,
}}
/>
) : null;
}
Now you just use the this.props.handleHasShadow(true/false) function to enable/disable the shadow. The shadow will be as big as the parent (Page) component.
You may also want to make sure that the focus stays inside your modal. Otherwise users will be able to tab-out from the modal. Keyword here is focus-trap.

React stop/start fade-out on mouseEnter and mouseLeave without Jquery

I am trying to show an error message as a toast(React Component) that will fade-out after certain seconds. However When the user hover-in the mouse on the toast while fading, the fade-out should stop and the toast should be restored to its initial state and when he hovers-out the mouse on the toast, the fade-out should start again. It can be achieved by using JQuery like this -
//function to start the fade-out after time - t sec
static fadeOutToast(id, t) {
let toast = document.getElementById(id);
if (toast) {
setTimeout(() => {
FadeAndRemove('#' + id);
}, t * 1000);
}
}
/**
* t1 - time for fadeout animation
*/
static FadeAndRemove(id,t1) {
jQuery(id).fadeOut(t1 * 1000, function () {
jQuery(this).hide();
});
handleReAppear(id);
}
static handleReAppear(id) {
jQuery(id).on("mouseover", function (e) {
jQuery(this).stop(true).fadeIn(0);
});
jQuery(id).on("mouseleave", function (e) {
FadeAndRemove(this);
});
}
Its working perfectly fine. However due to projects constraints I am not supposed to mixup Jquery and react.
I tried to achieve it by manipulating the CSS opacity on mouseEnter and mouseLeave events. The problem I face is the toast never goes away from the page using opacity. Is there any way in which we can detect when the opacity of the toast becomes 0 so that I can remove it from the page just when the opacity becomes 0 ?
Can someone help me in achieving the same without using Jquery ?
For the fading animation I would use React-Spring. With a Spring you can delay the start animation so it will fade-out after the delay.
Then you can add onMouseEnter and onMouseLeave event handler to detect the hovering of the toastr.
With this mouse detection you can toggle the to value of the Spring to opacity 1. That way it won't fade-out if the mouse is over the toast.
For the removal of the toastr you can use onRest of Spring and check if opacity is zero. onRest will be called as soon as the animation will end.
The state management is done inside Toastrcomponent which will render all displayed toasts. This component will also handle the removal of the toast with no opacity.
For click event addToast I'm using a higher order component withToastr so I can add the prop of to the containing component.
For event handling I'm using Eventemitter3. If you're using Redux you could also use it to trigger the toasts.
In the next sections I'll give some details to every component that I've created in the following Codesandbox. (Note: The snippets here are not running - for testing the code please have a look at the sandbox)
ToastrItem component
Responsible for rendering a toast and for the animation.
import React, { PureComponent } from "react";
import { Spring } from "react-spring";
import styled from "styled-components";
import PropTypes from "prop-types";
class ToastrItem extends PureComponent {
static propTypes = {
id: PropTypes.string,
timeout: PropTypes.number,
destroy: PropTypes.func
};
static defaultProps = {
timeout: 5000
};
state = {
hovered: false
};
handleRest = ({ opacity }) => {
if (opacity === 0) {
this.props.destroy(this.props.id);
}
};
handleMouseEnter = () => {
this.setState({
hovered: true
});
};
handleMouseLeave = () => {
this.setState({
hovered: false
});
};
render() {
const { message, index, timeout } = this.props;
const { hovered } = this.state;
return (
<Spring
config={{ duration: 600, delay: timeout }}
from={{ opacity: 1.0 }}
to={{ opacity: hovered ? 1.0 : 0 }}
onRest={this.handleRest}
>
{interpolated => (
<Wrapper>
<ToastBox
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
pos={index}
opacity={interpolated.opacity}
>
{message}
{/*- debug info: {JSON.stringify(interpolated)}*/}
</ToastBox>
</Wrapper>
)}
</Spring>
);
}
}
const Wrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
z-index: 100;
`;
const ToastBox = styled.div.attrs(props => ({
style: {
transform: `translateY(${props.pos * 80}px)`,
opacity: props.opacity
}
}))`
width: 60%;
height: 50px;
line-height: 50px;
margin: 0 auto;
color: white;
padding: 10px;
background: rgba(0, 0, 0, 0.8);
text-align: center;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
border-radius: 10px;
pointer-events: auto;
`;
export default ToastrItem;
The Spring is doing the animation as mentioned before. The mouse events enter/leave are setting local state hovered so we can change the animation end opacity - this will avoid the animation.
I've also tried reset prop from React-Spring but that wasn't working as expected.
Toastr component
This component is managing the active toasts. Nothing special here. It's rendering the toasts array that are added with addToast.
addToast is creating a relatively unique key with timestamp and array index. It's needed so React is getting a key prop on the component. We could also use a uuid library here but I think the timestamp-id is OK.
destroy will be called if opacity is 0 then it's filter by key and update the state. The map is just there so we're updating the positions of the toasts.
class Toastr extends PureComponent {
state = {
toasts: []
};
addToast = (message, config) => {
const index = this.state.toasts.length;
const id = `toastr-${Date.now()}-${index}`;
const ToastComponent = (
<ToastrItem
key={id}
id={id}
index={index}
message={message}
timeout={config.timeout || 3000}
destroy={this.destroy}
/>
);
this.setState(state => ({
toasts: [...state.toasts, ToastComponent]
}));
};
destroy = id => {
this.setState(state => ({
toasts: [
...state.toasts
.filter(toast => toast.key !== id)
.map((toast, index) => ({
// map for updating index
...toast,
props: {
...toast.props,
index: index
}
}))
]
}));
};
componentDidMount() {
emitter.on("add/toastr", this.addToast);
}
render() {
const { toasts } = this.state;
return toasts;
}
}
export const withToastr = WrappedComponent => {
return class extends PureComponent {
render() {
return <WrappedComponent addToast={actions.add} />;
}
};
};
Usage in the app
We're adding addToast by using withToastr(App). This will add the prop addToastr to the App component.
Then we're rendering the Toastr component that will manage & render our toasts.
Finally we add a button so we can trigger the toasts.
class App extends Component {
toastr;
render() {
const { addToast } = this.props;
return (
<div className="App">
<Toastr />
<button onClick={() => addToast("Hello", { timeout: 4000 })}>
Show toast
</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
const AppWithToasts = withToastr(App);
ReactDOM.render(<AppWithToasts />, rootElement);
Conclusion
The code is working but I would add native prop to the Spring and I would also check if a transition would be a better fit for the use-case. See the example from MessageHub example from React-spring docs. Should be also possible to prevent the fade-out but I haven't checked.
You might want to think about using the Animatable library. It uses a declarative syntax that's quite easy to incorporate.
import * from 'react-native-animatable';
return(
<Animatable.View animation="fadeOut" duration={2000} delay={1000}>
<View>
{/* YOUR CONTENT */}
</View>
</Animatable.View>
);
https://github.com/oblador/react-native-animatable

Categories

Resources