Why ReactCSSTransitionGroup generates multiple components though I expect only one? - javascript

I am using ReactCSSTransitionGroup to do some animation and I found an interesting thing which does not make any sense to me.
In the example below, when I click <div className="HeartControl">, it will update the height of the <div className="HeartFill"> which works fine. (I know to achieve the effect does not necessarily need ReactCSSTransitionGroup here though).
Interesting thing is that when I click, there will be another <div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div> with a new React component id added after the existing one.
But I expect there will always be only ONE <div className="HeartFill"> there.
Why this happened???
P.S.. after a few clicks, the result will look like:
<span data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2">
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3.5999999999999996px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$6:0"></div>
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$5:0"></div>
var HEIGHT_HEART = 30;
var NUM_HEART_MAX = 50;
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var Heart = React.createClass({
getInitialState: function() {
return {
heartHeight: 0
};
},
onClick: function(e) {
var currentHeartHeight = this.state.heartHeight;
this.setState({
heartHeight: currentHeartHeight + 1
});
},
render: function() {
var styleHeartFill = {
'position': 'absolute',
'bottom': 0,
'left': 0,
'width': 30,
'height': this.state.heartHeight / NUM_HEART_MAX * HEIGHT_HEART,
'background-color': '#D64541'
};
return (
<div className="Heart" >
<div className="HeartControl" onClick={this.onClick}>
<i className="fa fa-angle-up" />
</div>
<img src="heart.png" className="HeartOutline" />
<ReactCSSTransitionGroup transitionName="HeartFill">
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
</ReactCSSTransitionGroup>
</div>
);
}
});
React.renderComponent(<Heart />, document.getElementById('Heart'));
`

I suspect the reason your getting more than one is because your using the key prop
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
From React docs http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
When React reconciles the keyed children, it will ensure that any child with key will be reordered (instead of clobbered) or destroyed (instead of reused).
Heres a jsfiddle using the key prop http://jsfiddle.net/kb3gN/3826/
Heres a jsfiddle not using the key prop http://jsfiddle.net/kb3gN/3827/
P.s I've made a few changes in the fiddle just to try and better demostrate the reasoning

I'm fairly late to the game with this answer, but I ran into this issue as well and want to provide a solution for others. Removing the key is not a sufficient solution since React relies on it to know when to animate items. The documentation now has a section discouraging this.
You must provide the key attribute for all children of
ReactCSSTransitionGroup, even when only rendering a single item. This
is how React will determine which children have entered, left, or
stayed.
- https://facebook.github.io/react/docs/animation.html
If you are only animating the entry/exit of a single item, a CSS hack can be used to fix flickering that may be seen from multiple items entering/exiting.
.HeartFill {
display: none;
}
.HeartFill:first-child {
display: block;
}
React will add new elements on top in most cases, but this isn't guaranteed. If your transitionEndTimeout prop is set to a relatively short time, this shouldn't be a huge concern. The timeout prop should also match the CSS transition time.

Here is the problem:
You are providing a value for key which is changing over time. Keys are used to decide if an element is the same or different.
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
When you do this the value for key changes and React thinks a new element is entering and an old element is leaving.
Usually you need a unique key which can either be sequential or be generated using Math.random(). (remember to generate them once with getInitialState or DefaultProps, not in render, as that would create a new key every time).
The order of elements is another thing that can be in trouble.
From React's documentation:
In practice browsers will preserve property order except for properties that can be
parsed as a 32-bit unsigned integers. Numeric properties will be ordered sequentially
and before other properties. If this happens React will render components out of
order. This can be avoided by adding a string prefix to the key:

ReactCSSTransitionGroup should create a second element
it will remove it when a specified css animation has finished
or print a warning when there is no animation in the css
maybe look at the Low-level API for better understanding: http://facebook.github.io/react/docs/animation.html (bottom of the page)

I used gluxon's advice as a starting point - what worked for me was removing the leave transition and making it display nothing:
.example-leave.example-leave-active { display: none; }

Related

How can I use React Material UI's transition components to animate adding an item to a list?

I have this class.
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.add = this.add.bind(this);
this.clear = this.clear.bind(this);
}
add() {
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
}
clear() {
this.setState({ items: [] });
}
render() {
return (
<div>
<div>
<button onClick={this.add}>Add</button>
<button onClick={this.clear}>Clear</button>
</div>
{/* This is wrong, not sure what to do though... */}
<Collapse in={this.state.items.length > 0}>
<ul>{this.state.items}</ul>
</Collapse>
</div>
);
}
}
Sandbox link: https://codesandbox.io/s/material-demo-ggv04?file=/Demo.js
I'm trying to make it so that every time I click the "add" button, a new item gets animated into existence at the top of the list and the existing items get pushed down. Not sure how to proceed though.
Extra Resources
Example of what I'm trying to achieve: https://codeburst.io/yet-another-to-do-list-app-this-time-with-react-transition-group-7d2d1cdf37fd
React Transition Group Transition docs: http://reactcommunity.org/react-transition-group/transition (which seem to be used internally by Collapse)
I updated your Sandbox code to achieve what you wanted, but I don't think MaterialUI is the best library for that (I could be missing a better way to do it).
The challenge is that when you add a new item, that doesn't exist in the DOM yet. And most of those animation libraries/components require the element to be in the DOM and they just "hide" and "show" it with a transition time.
I had a similar situation and after some research, the better library I found that can handle animation for elements that are not yet in the DOM, was the Framer Motion. (You can check their documentation for mount animations)
Anyway, here is the link for the new Code Sandbox so you can take a look. The changes I made:
Removed random key
In the map function that creates your list using the <Collapse /> component, there was a function to get a random integer and assign that as a key to your component. React needs to have consistent keys to properly do its pretenders, so removing that random number fixes the issue where your "Toggle" button wasn't animating properly. (If your list of items doesn't have an unique ID, just use the index of the map function, which is not a good solution, but still better than random numbers).
<Collapse key={i} timeout={this.state.collapseTimeout} in={this.state.open}>
{it}
</Collapse>
Added a new function to control the toggle
The approach here was: add the item in your list and, after the element is in the DOM, close the <Collapse />, wait a little bit and open it again (so you can visually see the animation). In order to do that, we needed a new "toggle" function that can explicit set the value of the collapse.
toggleValue(value) {
this.setState(() => {
return {
open: value
};
});
}
Added a variable timeout for the collapse
The last issue was that, closing the <Collapse /> when the new item is added, was triggering the animation to close it. The solution here was to dynamically change the timeout of the collapse, so you don't see that.
setCollapseTimeout(value) {
this.setState(() => {
return {
collapseTimeout: value
};
});
}
When adding the element to the list, wait to trigger the animation
Again, to work around the issue with elements not yet in the DOM, we need to use a setTimeout or something to wait to toggle the <Collapse />. That was added in your add() function.
add() {
this.toggleValue(false);
this.setCollapseTimeout(0);
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
setTimeout(() => {
this.setCollapseTimeout(300);
this.toggleValue(true);
}, 100);
}
Again, this is a hacky solution to make <Collapse /> from MaterialUI work with elements that are not yet in the DOM. But, as mentioned, there are other libraries better for that.
Good luck :)
Ended up here earlier on and then came back to create a sandbox showing hopefully a simple method for this scenario. The material-ui docs are a bit (lot) light in this area and I was fighting with a very similar situation, but I tried something with TransitionGroup from react-transition-group, crossed my fingers and it seemed to work.
Forked CodeSandbox with TransitionGroup
The gist is that you
wrap all of the components you want to transition in the <TransitionGroup> component
Inside the TransitionGroup, put in the "condition" (logic or loop output) for the data you want to render
Wrap the individual components you want to transition with transition component of your choice - <Collapse> in this example
e.g. In its most simple setup where "items" is an array of unique numbers coming from either props, state or a redux store
<TransitionGroup>
{items.map(item => (
<Collapse key={item}>
I am item {item}
</Collapse>
))}
</TransitionGroup>
With this setup I have found that I didn't need to put any props on the TransitionGroup or Collapse, and the TransitionGroup handled all the mounting and unmounting in the loop rendering. Material UI doesn't produce the lightest of HTML output, but I guess it's all rendered on the fly so maybe that makes it better (unless you have thousands of elements, then things start to drag).
You can even go a step further and wrap the whole thing in another TransitionGroup to cover situations where you want to remove the whole thing without transitioning all of the individual items - in this instance I switched it to a <Slide>. I was absolutely certain that this wouldn't work, but it seemed to not care. You can also try and be semantic and use the "component" property rather than wrapping in another element e.g.
{items.length > 0 && (
<TransitionGroup>
<Slide>
<TransitionGroup component="ul">
{items.map((item) => (
<Collapse component="li" key={item}>I am item {item}</Collapse>
))}
</TransitionGroup>
</Slide>
</TransitionGroup>
)}
I have changed the sandbox in the following ways
Included TransitionGroup from react-transition-group
Changed the "add" logic so that the components aren't part of the "items" array - the array only contains the data required to render the components
I have added a simple "count" and pushed that to the array to give the items a unique index (had originally used Math.random, but I wanted a "prettier" output). Generally your items will probably be coming from a database somewhere where a unique id will already be set.
Rendered the components in a loop based on the data in the array (this could be done in a separate function, but the gist is that the components aren't being stored in the array)
added a "delete" function to show the removal of single items
wrapped the whole group in a second <TransitionGroup> to show that the unmounting can happen in a group level
Put in some simple styling to get a better idea of the effect. You could use Material UI components here, but just wanted to keep it simple.
Hope this helps someone in the future.

Is there a way to perform svelte transition without a if block?

<button on:click={() => (visible = !visible)}>Toggle</button>
{#if !visible}
<QuizArea
transition:slide
on:score={e => {
playerScore = e.detail.score;
}} />
{/if}
My question is can I use the transition without toggling the visibility?
Using a {#key} block:
<script>
import { fly } from "svelte/transition"
let unique = {}
function restart() {
unique = {} // every {} is unique, {} === {} evaluates to false
}
</script>
{#key unique}
<h1 in:fly={{x: 100}}>Hello world!</h1>
{/key}
<button on:click={restart}>Restart</button>
REPL
{#key} was introduced in Svelte v3.28, before that you needed to use a keyed {#each} block with only one item
When the key changes, svelte removes the component and adds a new one, therefor triggering the transition.
Using { create_in_transition } from "svelte/internal"
<script>
import { fly } from "svelte/transition"
import { create_in_transition } from "svelte/internal"
let element;
let intro
function animate() {
if (!intro) {
intro = create_in_transition(element, fly, {x: 100});
}
intro.start();
}
</script>
<h1 bind:this={element}>Hello world!</h1>
<button on:click={animate}>Go</button>
REPL
Has a similar effect, but instead of removing the previous component and creating a new one, this method re-uses the same instance.
But using internal api's is dangerous as these may change when you update svelte.
If you decide to use this, add a line to your project Readme.md mentioning which internal api's you used and why.
Try to write it using other methods when you can.
the transition directive or intro/outro is for transition when your component is created and added into the DOM, or destroyed and removed from the DOM.
The only way to add/remove a component with Svelte is to use logic blocks like {#if} to add/remove a component based on a logic.
If you want to keep the component on the DOM, but still add animation, like fading the component in and out, you can consider using CSS transition or CSS animation by adding/removing CSS class, something like this.

Vue component not updating property correctly

I have a chat component that has a multiline textbox and the chat room where the messages goes, the multiline textbox starts with a single line with 44px of height, when the user types its grows and the chat room get smaller as seen in the images below.
I get that behavior with the next code
//chat room in the chat-room i use css variables to calculate the height
//template
<vue-perfect-scrollbar ref="chatRoomScrollbar" :style="css">
<div v-viewer="viewerOptions" class="chat-room px-3 py-3">\
...
</div>
</vue-perfect-scrollbar>
//script
props: {
textFieldSize: {
type: Number,
default: 44,
},
},
computed: {
css() {
return {
'--height': `calc(310px - ${this.textFieldSize}px)`,
'--max-height': `calc(310px - ${this.textFieldSize}px)`,
};
},
},
//style
.chat-room-container > .ps-container {
height: var(--height);
min-height: var(--max-height);
background-color: #eceff1 !important;
}
and i get textFieldSize in the parent component when the #input event is fired
//chat-room
<chat-room
:text-field-size="textFieldSize">
</chat-room>
// textbox
<v-text-field
id="messageField"
v-model="form.text"
#input="getTextboxSize">
<v-icon slot="append-icon">send</v-icon>
</v-text-field>
// script
data() {
return {
textFieldSize: 44,
};
},
methods: {
getTextboxSize() {
if (document.getElementById('messageField')) {
const height = document.getElementById('messageField').offsetHeight;
this.textFieldSize = height;
this.updateChatScrollbar = true;
}
if (this.form.text === '') {
this.textFieldSize = 44;
}
},
},
when the user types, i get the height of the textfield, and pass it to the chat room and with css variable i get the height difference.
THE PROBLEM
When i select the text and delete it, or i use ctrl+z the value of textFieldSize is not beign recalculated until i type again, i have tried to use the input event in the text field, using a watcher in the form.text but none of them worked, how can make this work?
In the images below i have selected the text, then delete it, and show the textFieldSize value does not change
I would bring up 2 points:
Why are you not using $refs in getTextboxSize() - I don't think it is the cause of your issue, but it certainly could be. It's just weird to see you jump into raw js when you could set a ref="message-field" on the v-text-field element and then access the element directly, the Vue way, using this.$refs['message-field']. I'm probably off with this, but whenever I have these kinds of issue with a loss of reactivity, it's usually due to something like this.
This is cheap and not usually best practice, but can you try putting this.$forceUpdate() at the end of your getTextboxSize() function. This is a way to tell Vue to update the layout again. I would try it, and if it solves the issue, then you know it's a type of reactivity/race-condition issue. I don't ship production code with this.$forceUpdate() because if you need to use it it's usually a sign of a more fundamental design issue.
The problem is than Ctrl+Z does not trigger an input event. I think you should better not bind to input event of a textbox, but wach a form.text property for changes.
watch:{
'form.text':function(){
getTextBoxSize();
}
}

ReactJS loosing reference of an element

I am forcing element to be focused like this
/**focusing the element if the element is active */
componentDidUpdate() {
console.log(this.activeElementContainer);
if(this.activeElementContainer!==undefined && this.activeElementContainer!==null) {
/** need to focus the active elemnent for the keyboard bindings */
this.activeElementContainer.focus();
}
}
My render has conditional rendering the elements are being rendered dynamically from the array,
Let say I have one element in div and I am adding another from the toolbox. In that case I need to focus the last element I dragged.
render() {
let childControl= <span tabIndex="-1" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>;
if(this.props.activeItem){
childControl=<span ref={ (c) => this.activeElementContainer = c }
tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
}
//later I ma using childControl to array and it works fine.
The logs says, first time it works fine
But, second time the this.activeElementContainer is undefined
Is there any alternative way or possible solution to this?
The thing is I need to focus only one element at the time.
Remember: Activecontrol has too many things to do like it can have right click menu, drag etc. so, I need to render it separately.
After reading this one github:
This is intended (discussed elsewhere) but rather unintuitive
behavior. Every time you render:
<Value ref={(e) => { if (e) { console.log("ref", e); }}} /> You are
generating a new function and supplying it as the ref-callback. React
has no way of knowing that it's (for all intents and purposes)
identical to the previous one so React treats the new ref callback as
different from the previous one and initializes it with the current
reference.
PS. Blame JavaScript :P
Source
I changed my code to
<span id={this.props.uniqueId} ref={(c)=>
{if (c) { this.activeElementContainer=c; }}
} tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
Adding if was the real change. now, it has a ref.
For others who face this problem:
I need to write custom function too, in the componentDidUpdate I am still getting old reference,
ref={(c)=>{if (c) { this.activeElementContainer=c; this.ChangeFocus(); }}
Adding this was the perfect solution for me

Drawing 3D objects

I'm looking to draw a 3D cylinder with javascript by copying the layers and applying an increased margin to these elements. I have tried to set the height of the element in my input and run the copy function while total margin of the copied elements is lower than the set height of elements.
http://jsfiddle.net/yuX7Y/3/
<form>
<input type="number" id="userHeight" />
<button type="submit" onclick="circleHeight">Submit</button>
</form>
<div class="circle">
</div>
<div class="slice">
</div>
$(document).ready(function(){
var initMargin = 4;
var totalMargin = 0;
var i = 0;
function copy(){
$(".slice").clone().appendTo( ".content" ).css({'margin-top': totalMargin + "px"});
console.log("cloned");
i++;
totalMargin = initMargin + 4;
}
function setH(){
while(i < (document.getElementById("userHeight").value)){
copy();
}
if(i>100){
initMargin = 4;
i=0;
}
}
});
Jump To The Result: http://jsfiddle.net/yuX7Y/15/
Notes
This Fiddle/question intrigued me so I went ahead and looked at it for a little while. There were actually a number of issues, some obvious and some less obvious. Somewhat in the order I noticed them these are some of the issues:
jQuery wasn't included in the fiddle
The click event wasn't wired up correctly - it was actually trying to submit the form. You need to use e.preventDefault to stop the form from submitting. Since you were already using jQuery I just wired up with the jQuery click event:
$("#recalculateHeight").click(function (e) {
e.preventDefault();
setH();
});
Because of the use of "global" variables (variables not initialized within the routines), the submit would only work once. Instead of this, I moved variable declarations to the appropriate routine.
Calling $(".slice").clone() clones ALL slice elements on the page. The first time you clone, this is fine. But after that you are cloning two elements, then three elements, etc (as many slice elements as are on the page). To solve this I created a slice template like:
<div class="slice" id="slice-template" style="display: none"></div>
Then you can clone to your hearts content like $("#slice-template").clone(). Just don't forget to call the jQuery show() method or set display back to block on the cloned element.
Finally, if you want to repeat the process many times you need to be able to clear previous elements from the page. I find this easiest to do by creating containers, then clearing the contents of the container. So I put all of the "slices" into this container:
<div class="content">
Now when you want to clear the content node you can just call $(".content").empty();.
I also made a few style based changes in my Fiddle, but those don't make it work or not work, they just help me read the code! So, there you have it! Best of luck!

Categories