Background
I am trying to make an element disappear after the animation ends (I am using animate.css to create the animations).
The above 'copied' text uses animated fadeOut upon clicking the 'Copy to Journal Link'. Additionally, the above demo shows that it takes two clicks on the link to toggle the span containing the text 'copied' from displayed to not displayed.
According to the animate.css docs, one can also detect when an animation ends using:
const element = document.querySelector('.my-element')
element.classList.add('animated', 'bounceOutLeft')
element.addEventListener('animationend', function() { doSomething() })
My Problem
However, within the componentDidMount() tooltip is null when attempting to integrate what animate.css docs suggest.
What am I doing wrong? Is there a better way to handle this behavior?
ClipboardBtn.js
import React, { Component } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
class ClipboardBtn extends Component {
constructor(props) {
super(props)
this.state = {
copied: false,
isShown: true,
}
}
componentDidMount() {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
handleAnimationEnd() {
this.setState({
isShown: false,
})
}
render() {
const { isShown, copied } = this.state
const { title, value } = this.props
return (
<span>
<CopyToClipboard onCopy={() => this.setState({ copied: !copied })} text={value}>
<span className="clipboard-btn">{title}</span>
</CopyToClipboard>
{this.state.copied ? (
<span
id="clipboard-tooltip"
className="animated fadeOut"
style={{
display: isShown ? 'inline' : 'none',
marginLeft: 15,
color: '#e0dbda',
}}
>
Copied!
</span>
) : null}
</span>
)
}
}
export default ClipboardBtn
Using query selectors in React is a big NO. You should NEVER do it. (not that that's the problem in this case)
But even though it's not the problem, it will fix your problem:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
https://reactjs.org/docs/refs-and-the-dom.html
componentDidMount gets called only once during the inital mount. I can see that in the inital component state, copied is false, hence #clipboard-tooltip never gets rendered. That is why tooltip is null.
Instead try this :
componentDidUpdate(prevProps, prevState) {
if(this.state.copied === true && prevState.copied === false) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
if(this.state.copied === false && prevState.copied === true) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.removeEventListener('animationend', this.handleAnimationEnd)
}
}
componentDidUpdate gets called for every prop/state change and hence as soon as copied is set to true, the event handler is set inside componentDidUpdate. I have added a condition based on your requirement, so that it doesn't get executed everytime. Feel free to tweak it as needed.
Related
I want to send events down to my React child.
I feel like this is kind of an easy thing to do, so maybe i just have a mental block, and there is something obvious that is staring me in the face.
Anyway, I have a little Test app which illustrates the problem:
export class Test extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let {buttonClicked, textFieldChanged} = this.state
return (
<div>
<button onClick={()=>this.handleClick()}>
Click
</button>
<input type={"text"} onChange={()=>this.handleTextChange()}/>
<Inner buttonClicked={buttonClicked} textFieldChanged={textFieldChanged}/>
</div>
);
}
handleClick(e) {
this.setState({ buttonClicked: true })
}
handleTextChange(e) {
this.setState({textFieldChanged:true})
}
}
class Inner extends React.Component {
render() {
let {buttonClicked, textFieldChanged} = this.props;
return (
<React.Fragment>
<div>Clicked : {buttonClicked ? "CLICKED!" : " "}</div>
<div>Text input : {textFieldChanged ? "TYPED!" : " "}</div>
</React.Fragment>
);
}
}
A button and a textfield live in the parent. Both these widgets can fire off events and change the child component.
This is simply achieved by passing a state value as a property down to the child. Very easy stuff.
However I would like an either/or situation. When I click the button this removes the text event, and vice versa. Ie. I do not want to see a situation like this :
Now there is a very obvious way to fix this by changing the state value to "false" of the other value.
handleClick(e) {
this.setState({ buttonClicked: true, textFieldChanged: false })
}
handleTextChange(e) {
this.setState({textFieldChanged:true, buttonClicked: false})
}
Is there any OTHER way of doing this?
The problem is that I have LOTS and LOTS of even handlers in my component and I don't want to negate the other state properties of the other values.
if i understood you correctly just one function will help - pass the attribute name into it
handleClick(propName) {
this.setState({
...this.state,
[propName]: !this.state[propName]
})
}
Create property lastEventType in parent component state , whenever you click or type - update it. And pass only this property to Inner component
Here is a snippet of my code base - https://codesandbox.io/s/transition-code-1wr5z
Currently, via changing classes and CSS transitions, text fades in when a new Paragraph component is loaded in and mounted.
However, I wish to also have this transition occur when the text prop within the Paragraph component changes.
Doing this within the lifecycle update or render just triggers an infinite update loop.
Not sure where to go from here as most discussions I can find are about tackling the functionality of getting the text to fade in on load, rather than on update.
Here is the snippet solution
import React, { Component } from "react";
import "./Style.css";
class Paragraph extends Component {
constructor(props) {
super(props);
this.state = {
open: false
};
}
componentDidMount() {
setInterval(() => {
this.fade();
}, 1000);
}
fade() {
this.setState({ open: !this.state.open });
}
render() {
const text = this.props.text;
const classes = this.state.open ? "greenCls" : "redCls";
return <div className={classes}>{text}</div>;
}
}
export default Paragraph;
css class
.redCls {
background: red;
}
.greenCls {
background: green;
}
Please try using setTimeout to fix this issue
https://codesandbox.io/s/transition-code-fgmyq
In my component im trying to sync the received props with the current state in order to make it visible from outside (I know this is an anti-pattern, but I haven't figured out another solution to this yet. Im very open to suggestions!).
Anyways, this is what I've got:
export class PopupContainer extends React.Component {
state = {
show: false,
};
shouldComponentUpdate(nextProps, nextState) {
if (this.props.show === nextProps.show) return true;
return true;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// if popup shown, fade it out in 500ms
if (this.props.show !== prevProps.show)
this.setState({ show: this.props.show });
if (this.state.show) {
setTimeout(() => this.setState({ show: false }), 2000);
}
}
render() {
return <Popup {...{ ...this.props, show: this.state.show }} />;
}
}
And in my external component I'm rendering the container :
<PopupContainer
show={this.state.popup.show}
message={this.state.popup.message}
level={this.state.popup.level}
/>
Now when I initially set this.state.show to true it works, but every successive assignment which is also true without any false assignment inbetween doesn't work. How do I force componentdidUpdate() to fire anyways even if the props are the same value? shouldComponentUpdate() didn't seem to solve the problem.
Thank you!
Edit: I noticed that the render() method is only called in the parent element. It seems like as there is no change in properties for the child, react doesn't even bother rerendering the childern which somehow makes sense. But how can I force them to rerender anyways?
This is kind of a hack, but it works for me.
In the child class
Add a property to state in constructor - let's call it myChildTrigger, and set it to an empty string:
this.state = {
...
myChildTrigger: ''
}
then add this to componentDidUpdate:
componentDidUpdate(prevProps) {
if(this.state.myChildTrigger !== this.props.myParentTrigger) {
// Do what you want here
this.setState({myChildTrigger: this.props.myParentTrigger});
}
}
In the parent class
Add a myParentTrigger to state in constructor:
this.state = {
...
myParentTrigger: ''
}
In the render method, add it as a prop, like this:
<ChildClass ... myParentTrigger={this.state.myParentTrigger} />
Now you can trigger a call to componentDidUpdate to execute whatever is inside the if-statement, just by setting myParentTrigger to a new value, like:
this.setState({ myParentTrigger: this.state.myParentTrigger + 'a' });
I'm having trouble figuring out the problem with the following code. I'm trying to change the prop animating on a ProgressBarAndroid, and make it toggle every second. The code works as intended if I set loading to true in my constructor, but not if it's set to false (which is what I want, I don't want it to start animating right away). When it's set to false, the progressbar stays invisible all the time. Any ideas?
import React, { Component } from 'react';
import { ProgressBarAndroid } from 'react-native';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {loading: false}; // works if it is set to true here instead
// Toggle the state every second
setInterval(() => {
this.setState({loading: !this.state.loading});
}, 1000);
}
render() {
return (
<ProgressBarAndroid animating={this.state.loading}></ProgressBarAndroid>
);
}
}
This is actually an issue I had with ProgressBarAndroid a few weeks back. Once initiated as false, I was never able to set it back true.
The quick and dirty solution I had at the time was to just to move the state change outside of the animating prop.
In your case, changing this:
<ProgressBarAndroid animating={this.state.loading}></ProgressBarAndroid>
to this:
<View>
{ this.state.loading ? <ProgressBarAndroid/> : null }
</View>
and make sure to also include View from react-native.
You might have seen this type of effect. Example - https://codepen.io/anon/pen/GEmOQy
But I need to implement same way in React. I know I can use componentDidMount method for ajax, but thing is how to display response on hover.
I don't had practice on implementing hover with react, like I do in pure css approach with :hover.
So any solutions are welcome.
This a very flat question, and there are many possibilities. All I can give is a basic skeleton on how to do it.
Define a ImageCard component in whose componentDidMount you do the API call. Then on your parent component ( whichevere component the button is ), store a state key that manages whether to show the card or not:
class MyComponent extends React.Component {
constructor() {
super();
this.state = {
showCard: false
};
}
render () {
return (
<div>
{
this.state.showCard &&
<ImageCard/>
}
<button
onMouseEnter={() => this.setState({ showCard: true })}
onMouseLeave={() => this.setState({ showCard: false })}
>Hover Me!</button>
</div>
)
}
}
Use onMouseEnter event on the button and load a new image when it fires,
class SomeComp extends Component{
coonstructor(){
this.state = {
url: 'http://some-intial-url.com'
}
this.onMouseEnter = this.onMouseEnter.bind(this)
}
onMouseEnter(){
this.setState({
url: GetOtherImageURl()
})
}
render(){
<img src = {this.state.url} />
}
}