I have a simple component like this:
var component = React.createClass({
render: function(){
if (this.props.isCollapsed){
return this.renderCollapsed();
}
return this.renderActive()
},
renderActive: function(){
return (
<div>
...
</div>
);
},
renderCollapsed: function(){
return (
<div>
...
</div>
);
},
});
Basically, when the property changes, the component will either show active state or collapse state.
What I am thinking is, when the property change happens, i.e. active->collapse, or the other way around, I want the old view "shrink" or "expand" smoothly to show the new view. For example, if it is active -> collapse, I want the active UI to shrink to the size of collapse UI, and show it smoothly.
I am not sure how to achieve this effect. Please share some ideas. Thanks!
Here is a minimal working example:
const collapsible = ({active, toggle}) =>
<div>
<button type="button" onClick={toggle}>Toggle</button>
<div className={'collapsible' + (active? ' active': '')}>
text
</div>
</div>
const component = React.createClass({
getInitialState() {
return {active: false}
},
toggle() {
this.setState({active: !this.state.active})
},
render() {
return collapsible({active: this.state.active, toggle: this.toggle})
}
})
ReactDOM.render(React.createElement(component), document.querySelector('#root'))
.collapsible {
height: 1.5rem;
transition: height 0.25s linear;
background: #333;
border-radius: 0.25rem
}
.collapsible.active {
height: 7rem
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div id="root"></div>
The view can be "shrink" or "expand" smoothly by CSS transition, which is triggered by changing CSS properties.
To control CSS properties with React, we can reflect state changes to property values or className
in render()
.
In this example, .active
class affects height
value, and is controlled by state.active
. The class is toggled by React in response to state changes, and triggers CSS transition.
For smoother transitions, See this article.
As you want to render two different component for each of active and collapsed, wrap them in a div that controls the height with the help of CSS.
render: function(){
var cls = this.props.isCollapsed() ? 'collapsed' : 'expanded';
return(
<div className={cls + ' wrapper'}>
{
this.props.isCollapsed() ?
this.renderCollapsed() :
this.renderActive()
}
</div>
);
}
and in your CSS:
.wrapper{
transition: transform .5s linear;
}
.expanded{
height: 200px;
}
.collapsed{
height: 20px;
}
One more approach to this situation might be changing state after animation completes. The benefits of it is that you can apply not only transitions but whatever actions you want (js animations, smil, etc ..), main thing is not to forget to call an end callback;)
Here is working example CodePen
And here is the code example:
const runTransition = (node, {property = 'opacity', from, to, duration = 600, post = ''}, end) => {
const dif = to - from;
const start = Date.now();
const animate = ()=>{
const step = Date.now() - start;
if (step >= duration) {
node.style[property] = to + post;
return typeof end == 'function' && end();
}
const val =from + (dif * (step/duration));
node.style[property] = val + post;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
class Comp extends React.Component {
constructor(props) {
super(props);
this.state = {
isCollapsed: false
}
this.onclick = (e)=>{
this.hide(e.currentTarget,()=>{
this.setState({isCollapsed: !this.state.isCollapsed})
});
};
this.refF = (n)=>{
n && this.show(n);
};
}
render() {
if (this.state.isCollapsed){
return this.renderCollapsed();
}
return this.renderActive()
}
renderCollapsed() {
return (
<div
key='b'
style={{opacity: 0}}
ref={this.refF}
className={`b`}
onClick={this.onclick}>
<h2>I'm Collapsed</h2>
</div>
)
}
renderActive() {
return (
<div
key='a'
style={{opacity: 0}}
ref={this.refF}
className={`a`}
onClick={this.onclick}>
<h2>I'm Active</h2>
</div>
)
}
show(node, cb) {
runTransition(node, {from: 0, to: 1}, cb);
}
hide(node, cb) {
runTransition(node, {from: 1, to: 0}, cb);
}
}
ReactDOM.render(<Comp />, document.getElementById('content'));
And for sure, for this approach to work your only opportunity is to relay on state, instead of props of Component, which you can always set in componentWillReceiveProps method if you have to deal with them.
Codepen link updated with more clear example which is showing benefits of this approach. Transition changed to javascript animation, without relying on transitionend event.
The standard way is to use CSSTransitionGroup from react-transition-group, which is quite easy. Wrap the component with the CSSTransitionGroup
and set timeouts on enter and leave, like this:
<CSSTransitionGroup
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}>
{items}
</CSSTransitionGroup>
From the v1-stable docs:
"In this component, when a new item is added to CSSTransitionGroup it will get the example-enter CSS class and the example-enter-active CSS class added in the next tick."
Add styling for the CSS classes to get the correct animation.
There's also pretty good explanation in React docs, check it out.
There are third-party components for animation as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With