Smoothly Transitioning Between ReactJS Prop Changes - reactjs

There are two buttons that toggle the layout of an item. When either button is clicked, I'd like the item to fade out, change, then fade back in.
Using ReactJS, I'm running into two problems:
Using componentDidUpdate to trigger the "fade back in" event causes a loop; changing the state re-triggers componentDidUpdate endlessly.
Using componentWillReceiveProps allows me to update the class on the element to have it fade out, but it also immediately changes the layout. I need to delay the change until it's invisible.
Thoughts? Have I constructed this wrong?
(The code is below, but something is broken in Stack's version that works in JSFiddle: https://jsfiddle.net/nathanziarek/69z2wepo/15009/)
var Hello = React.createClass({
getInitialState: function() {
return { visible: " x-hidden" };
},
render: function() {
return <div className={"hello" + this.state.visible}>Hello {this.props.name}</div>;
},
componentDidMount: function(){
this.setState({ visible: "" })
},
componentWillReceiveProps: function(){
// The item is changing, make it invisible
// Wait until it's done being invisible before changing
// anything?
this.setState({ visible: " x-hidden" })
},
componentDidUpdate: function(){
// The item has changed, make it visible
// Setting anything here causes this function
// to get called again, creating a loop
// this.setState({ visible: "" })
}
});
var Button = React.createClass({
render: function() {
return <button onClick={this.handleClick}>{this.props.label}</button>;
},
handleClick: function() {
React.render(
<span>
<Button label="Universe"/>
<Button label="World"/>
<Hello name={this.props.label} />
</span>,
document.getElementById('container')
);
}
});
React.render(
<span>
<Button label="Universe"/>
<Button label="World"/>
<Hello name="_______" />
</span>,
document.getElementById('container')
);
.hello {
opacity: 1;
transition: 300ms opacity linear;
-webkit-transition: 300ms opacity linear;
}
.x-hidden { opacity: 0 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.1/JSXTransformer.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.1/react-with-addons.js"></script>
<script src="https://facebook.github.io/react/js/jsfiddle-integration.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>

I would look into using React's CSSTransitionGroup add-on component. You'll have to do some restructuring, but this will give you the behavior you want without having to add a bunch of logic around setting CSS classes; React will take care of animating (in your case, fading) components that are entering/leaving the DOM.
Unrelated: I would also avoid re-rendering your entire component like how you're doing it in your button click event. This breaks the flow of React and will inevitably cause problems. Prefer changing state and pushing down new props.

Easiest way to do this is to have both on screen then to do the fade entirely with css, and essentially completely omit React from the process other than to place the appropriate classes.
If you're fading between them you need both sets of contents anyway. That, in turn, means there's no reason to not have these in the document, and that in turn means it's just CSS hide/show tomfoolery, and there's no need for any kind of React lifecycle stuff.
You're overcomplicating it by trying to have React handle way too much.
Supporting CSS:
#foo span {
opacity: 0;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
transition: all 0.5s ease;
}
#foo span.show {
opacity: 1;
}
Then the relevant React:
var Foo = react.createComponent({
render: function() {
var showKey = this.props.showKey,
Things = {
a: 'Apple',
b: 'Bear',
c: 'Cold inevitable doom'
};
return (
<div id="foo">
{ Things.map(X,k) {
return <span class={(showKey === k)? 'show':undefined}>{X}</span>
}}
</div>
);
}
});
You'll find that as you switch that control's showKey, it will fade in and out its relevant contents as appropriate.

Related

Element is removed after transitionLeaveTimeout - but how come not inserted after transitionEnterTimeout

I have an issue, with the Animation One or Zero Items of the Animation page - http://facebook.github.io/react/docs/animation.html#animating-one-or-zero-items
My component shows a message initially. Then when it gets the next message, it fades out the "leaving" element in 1second. Because I set transitionLeaveTimeout to 1second as well, this element is removed after the fade out completes.
However, I set transitionEnterTimeout to be 1second (and then i tried 2second), however, the "entering" element is inserted as first child immediately on start of the "leaving". How come the "entering" element is not inserted AFTER the transitionEnterTimeout? Is there any way to make this happen?
The reason I ask is because if both are present, then the "leaving" element gets shifted down by line-height. I can't do CSS tricks like -100% line height and others as the CSS is a bit complex.
Here is a copy paste eample that shows this issue:
<html>
<head>
<script src="https://fb.me/react-with-addons-0.14.4.js"></script>
<script src="https://fb.me/react-dom-0.14.4.js"></script>
<script>
var MyStore = {};
function init() {
var TheThing = React.createClass({
displayName: 'TheThing',
getInitialState: function() {
return {
msg: this.props.msg
};
},
componentDidMount: function() {
MyStore.setState = this.setState.bind(this);
},
render: function() {
return React.createElement(React.addons.CSSTransitionGroup, {transitionName:'thing', transitionEnterTimeout:6000, transitionLeaveTimeout:3000},
React.createElement('div', {key:this.state.msg},
this.state.msg
)
);
}
});
ReactDOM.render(React.createElement(TheThing, {msg:'initial'}), document.getElementById('message'));
setTimeout(function() {
MyStore.setState({msg:'hi'})
}, 6000)
setTimeout(function() {
MyStore.setState({msg:'bye'})
}, 12000)
}
window.addEventListener('DOMContentLoaded', init, false);
</script>
<style>
.thing-enter,
.thing-appear {
opacity: 0;
}
.thing-enter.thing-enter-active,
.thing-appear.thing-appear-active {
opacity: 1;
transition: opacity 6s ease 3s;
}
.thing-leave {
opacity: 1;
}
.thing-leave.thing-leave-active {
opacity: 0;
transition: opacity 3s ease;
}
</style>
</head>
<body>
<div class="container">
<div id="message"></div>
</div>
</body>
</html>

ReactJS: Control a child state from the child and the parent

I have a rather simple problem and I'm not sure how to solve it with React's one way data flow.
Say you have a link in the parent that shows a modal
In the modal, you have an "X" that closes it.
I know I can change the state of the modal from the parent via props
// In the parent
<Modal display={this.state.showModal} />
// In the modal
<div className={this.props.display ? "show" : "hide"}>
<a className="close">×</a>
...
</div>
And I know how to close the modal, but not both. Not sure how to keep a state that is shared and controllable by both the parent and the child modal.
UPDATE
In trying to keep this as modular as possible, I think the React way would be to store the open/close logic in the modal variable.
var ParentThing = React.createClass({
...
render (
<Modal /> // How can I call this.open in the modal from here?
)
});
var Modal = React.createClass({
setInitialState: function() {
return {
display: false
}
},
close: function() {
this.setState({ display: false });
},
open: function() {
this.setState({ display: true });
},
render: function() {
return (
<div className={this.state.display ? "show" : "hide"}>
<a className="close" onClick={this.close}>×</a>
</div>
)
}
});
I saw this method, but it seems to be a little more than I need to do here. Reactjs: how to modify child state or props from parent?
There are two ways to handle this kind of thing in React:
Make the child "controlled," just like a form input with a value and onChange property, where the owner of the input controls the input.
Make the child "uncontrolled," just like a form input without a value.
The second choice seems faster up front, but just like managing a collection of form inputs in React, the advantage to using fully controlled components becomes apparent as complexity builds and the need to fully describe your UI at any point and time increases. (See this excellent answer from FakeRainBrigand if you're curious exactly why controlled components is better than uncontrolled in most cases.)
However, just like form inputs, there's no reason your component can't be either controlled or uncontrolled. If the user passes a display and onClose property, like Austin Greco's answer, you have a controlled modal, and the parent fully decides when to show or hide the modal.
If the user doesn't, you can skip using the properties and instead delegate to internal state managed by public methods on the modal component:
var ParentThing = React.createClass({
...
render: function() {
return <Modal ref="modal" />;
},
handleSomeClick: function() {
this.refs.modal.open();
}
});
var Modal = React.createClass({
setInitialState: function() {
return {
display: false
}
},
close: function() {
this.setState({ display: false });
},
open: function() {
this.setState({ display: true });
},
render: function() {
return (
<div className={this.state.display ? "show" : "hide"}>
<a className="close" onClick={this.close}>×</a>
</div>
)
}
});
If you like the idea of a controlled Modal component, but don't want to do all the boilerplate typing, you could even go so far as to implement something like the valueLink property for the Modal to simplify this pattern.
var ParentThing = React.createClass({
...
mixins: [React.addons.LinkedStateMixin],
getInitialState: function() {
return { showModal: false };
},
render: function() {
return <Modal displayLink={this.linkState("showModal")} />;
},
handleSomeClick: function() {
this.setState({showModal: true});
}
});
var Modal = React.createClass({
close: function() {
this.props.displayLink.requestChange(false);
},
render: function() {
return (
<div className={this.props.displayLink.value? "show" : "hide"}>
<a className="close" onClick={this.close}>×</a>
</div>
)
}
});
(See my blog post on creating custom components that work with linkState/valueLink for more info.)
So now you get the benefit of using a fully parent-controlled Modal, but you've removed a portion of the boilerplate around creating a function that sets the value to false and passing it to the modal.
You could pass a callback as a prop to the child component:
// In the parent
<Modal display={this.state.showModal} onClose={this.closeModal} />
// In the modal
<div className={this.props.display ? "show" : "hide"}>
<a className="close" onClick={this.props.onClose}>×</a>
...
</div>
Then when you click the close button on the child, it will call the function of the parent

CSS Transition not showing with directive

I'm playing with transitions and directives. I've created a Card directive that should show a clone of it self in fullscreen when clicked. The transition doesn't happen if I don't apply the altering css class in a timeout. Is that how it should be done?
<div ng-app='trans'>
<div data-card class='card'>timeout</div>
<div data-card='notimeout' class='card'>not timeout</div>
</div>
Between to original position and the fullscreen mode it should transition with a spin. The goto class is just so that i can add/remove transitions so that the card doesn't transition widht/height when the window is resized. I think it reads nice too =)
.card {
width:10vh;
height:14vh;
background-color:pink;
margin: 10px;
}
.card.goto.fullscreen {
transition: all 0.6s linear;
}
.card.fullscreen {
height:95vh;
width: 68vh;
position:absolut;
position: absolute;
top: 50% !important;
left: 50% !important;
margin: 0;
transform: translate(-50%, -50%) rotateY(360deg);
}
This is a simplified version of my directive.
var app = angular.module('trans', []);
app.directive('card', ['$document', '$timeout', function ($document, $timeout) {
return {
restrict: 'A',
link: link,
scope: {}
};
function link(scope, element, attrs) {
var clone;
element.on('click', function () {
if (clone) {
clone.off().remove();
}
clone = element.clone();
var spec = getCardSpecifications();
clone.css({
'margin': '0',
'top': spec.top + 'px',
'left': spec.left + 'px',
'position': 'absolute'
});
$document.find('body').append(clone);
clone.addClass('goto');
if (attrs.card == 'notimeout') {
clone.addClass('fullscreen');
} else {
$timeout(function () {
clone.addClass('fullscreen');
}, 0);
}
});
function getCardSpecifications() {
var spec = {};
spec.top = element.prop('offsetTop');
spec.left = element.prop('offsetLeft');
spec.height = element[0].offsetHeight;
spec.width = element[0].offsetWidth;
return spec;
}
}
}]);
I've created this jsfiddle that demonstrates the problem.
The problem doesn't have anything to do with Angular itself, but with creating a new DOM node and setting a class on it right after. Such a problem is described e.g. here, and it uses the same solution as yours in the first example.
DISCLAIMER: The real Angular way of doing this would be ngAnimate. What follows is a solution that is almost the same as the OP's, and one you'd only want to use if you don't want to depend on that module – but it's only ~11kb uncompressed, and 4kb gzipped. Choose wisely!
What also worked for me is waiting for the DOM node to be ready:
clone.ready(function() {
clone.addClass('fullscreen');
});
This amounts to almost the same thing as using a 0ms timeout, but is a. more descriptive and b. works in all cases, while the timeout solution apparently sometimes fails in Firefox (see linked article).
The second solution given in the article also reads a little more hackish (matter of opinion, really), and you'll have to retrieve the actual DOM element instead of the jqLite wrapper around it to use it.
Why exactly this happens, even though you are adding the class "after appending", I wasn't able to quickly find out. Perhaps appendChild, which append most likely uses internall, is asynchronous (i.e. pushes the DOM manipulation task onto the event queue)? Some more googling might be useful if you're really interested in the cause of this problem.
You should probably use animate to do an animation
$animate.addClass(clone, 'fullscreen'
I had issues trying to get the dependency for animate in fiddle so
I made a Plunker
When changing the DOM via the directive with methods like css, you have to inform the digest loop of these changes.
In order to accomplish that you should add scope.$apply() after adding the css class inside your if statement. The reason why $timeout works for you is that because it calls $apply after the timeout executes.

Notification when transition completes

I use Angular 1.3.1, ngAnimate with CSS transitions. And I want to know when the CSS transition starts and completes.
I was expecting to be able to use module.animation but it does not work as I expect it (and as the docs suggest). I'm not able to get a notification when the transition completes.
I have created a Plunker: http://plnkr.co/edit/UGCmbBoiLT1xvhx7JGst.
This is my HTML code:
<body ng-controller="Controller as ctrl">
<h1>Angular Animation issue!</h1>
<div class="animation" id="resizeme" ng-class="ctrl.className"></div>
<label>
<input type="checkbox" ng-model="ctrl.className"
ng-true-value="'big'" ng-false-value="''">
Bigger!
</label>
This is the CSS code:
#resizeme {
width: 100px;
height: 100px;
background-color: red;
-webkit-transition: width linear 1s;
transition: width linear 1s;
}
#resizeme.big {
width: 200px;
}
And this is the JavaScript code:
var module = angular.module('app', ['ngAnimate']);
module.controller('Controller', function() {
this.className = '';
});
module.animation('.animation', function() {
return {
addClass: function(elemennt, className) {
console.log('addClass animation starts');
return function(cancel) {
console.log('addClass animation completes')
}
},
removeClass: function(elemennt, className) {
console.log('removeClass animation starts');
return function(cancel) {
console.log('removeClass animation completes')
}
},
}
});
Checking the checkbox runs the "addClass" animation. I do get the "addClass animation starts" message in the console, but I don't get the "addClass animation completes" message when the transition is complete.
The Javascript-defined Animations section of ngAnimate docs says the following for the function returned by enter, addClass and friends:
//this (optional) function will be called when the animation
//completes or when the animation is cancelled (the cancelled
//flag will be set to true if cancelled).
So I'd expect the function that outputs "addClass animation completes" to be called when the transition completes. But it's not called.
Does anyone know how I can get a notification when the transition completes?
PS: I know I could use the $animate service and the promise returned by $animate.addClass. But I'm trying to use ng-class (and other animation-compatible Angular directives), so I don't have access to the promise provided by the $animate service.

Popover close button doesn't work (on SVG element)

Short
The question basically is why doesn't this fiddle not working properly. The close button on the popover works only once.
Long
I am creating a set of SVG elements that is dynamically added (through angular directives) to the page, and want them to have Popovers with close buttons. My current approach is not only not working, but seems too messy. I am looking for a better solution.
HTML
<div id="test-popover">Click on the rectangle for the popover.
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="width:100%; height:200px;">
</svg>
</div>
Javascript
var $element = $(document.createElementNS("http://www.w3.org/2000/svg", 'rect'))
.attr({ x: 42, y: 50, width: 50, height:75, fill: "#0011ff", })
var $closebtn = $('<button type="button" class="close" aria-hidden="true">&times</button>')
.on('click', (function ($element) {
return function () {
console.log("close...");
$element.popover('hide');
}
})($element));
var $poptitle = $('<div>Title</div>').append($closebtn);
var popcontent = function () {
return "some content with <b>HTML</b>";
};
$element.popover({
html: true,
title: $poptitle,
content: popcontent,
container: 'body',
});
$('#test-popover svg').append($element);
fiddle: http://jsfiddle.net/k9vva
Does anyone have a better way of doing this?
I suspect is because the popover is changing the html and the event handler is getting removed or something. One way could be to include the hidepopup call within the html thats appended. There's a fiddle here http://jsfiddle.net/k9vva/1/ (there may be a better solution that doesn't use the main scope, but it may depend on how you are integrating it).
var $element = $(document.createElementNS("http://www.w3.org/2000/svg", 'rect')).attr(
{ x: 42, y: 50, width: 50, height:75, fill: "#0011ff", });
var $closebtn = $('<button type="button" class="close" aria-hidden="true" onclick="hidepop();">&times</button>');
var $poptitle = $('<div>Title</div>').append($closebtn);
var popcontent = function () {
return "some content with <b>HTML</b>";
};
$('#test-popover svg').append($element);
$element.popover({
html: true,
title: $poptitle,
content: popcontent,
container: 'body',
});
window.hidepop = function() {
$element.popover( 'hide' );
};
I actually fixed it by calling title as a function with closure.
var $poptitle = (function($closebtn){
return function(){
return $('<div>Title</div>').append($closebtn);
}
})($closebtn);
Thought i'd add it here in case anyone wondered. This solution seems a little weird though, but oh well...
(I actually added the creation of the $closebtn and the $popover under one closure, in case this gives anyone problems).

Resources