Is it possible to specify the order of items in a ReactCSSTransitionGroup?
Consider a list of items which their order is important. If you want to show one item and hide its adjacent with one action, ReactCSSTransitionGroup confuses their order. Take a look the following fiddle, the items are supposed to be [1, 2, 3, 4, 5].
http://jsfiddle.net/mehranziadloo/kb3gN/15519/
Is it possible to tell ReactCSSTransitionGroup (or ReactTransitionGroup) the sequence of items?
The ReactCSSTransitionGroup just animates changes in DOM, it doesn't care about order. Your state changes from odd to even numbers, there is no moment when it contains sorted array with all the numbers. You can work around it by modifying state in different way, temporary saving old items for animation purposes, something like that:
switch: function() {
var newItems;
if (this.state.items[0] % 2 !== 1) {
newItems = [1, 3, 5];
}
else {
newItems = [2, 4];
}
this.setState({
items: newItems,
previousItems: this.state.items
}, function() {
this.setState({
previousItems: []
})
});
}
After that, you need to modify your render method:
render: function() {
var currentItems = this.state.items.concat(this.state.previousItems).sort();
var items = currentItems.map(function(item, i) {
return (
<div key={item}>
{item}
</div>
);
}.bind(this));
return (
<div>
<button onClick={this.switch}>Switch</button>
<ReactCSSTransitionGroup transitionName="example" transitionEnterTimeout={1000} transitionLeaveTimeout={1000}>
{items}
</ReactCSSTransitionGroup>
</div>
);
}
Here is an updated fiddle: http://jsfiddle.net/ny5La5ky/
Related
I am using React/JSX and Lodash in my app in order to accomplish what I want.
I need to repeat an element a certain amount of times depending on a condition. How should I do that?
Here is the element:
<span className="busterCards">♦</span>;
And I am assigning it like this:
let card;
if (data.hand === '8 or more cards') {
card = <span className='busterCards'>♦</span>;
}
So in this case, I need to repeat the element 8 times. What should be the process using Lodash?
The shortest way to do this without any external libraries:
const n = 8; // Or something else
[...Array(n)].map((e, i) => <span className="busterCards" key={i}>♦</span>)
solution without lodash or ES6 spread syntax:
Array.apply(null, { length: 10 }).map((e, i) => (
<span className="busterCards" key={i}>
♦
</span>
));
Here you go:
let card = [];
_.times(8, () => {
card.push(<span className="busterCards">♦</span>);
});
You may want to add key to each span element so React won't complain about missing the key attribute:
let card = [];
_.times(8, (i) => {
card.push(<span className="busterCards" key={i}>♦</span>);
});
For more info about .times, refer here: https://lodash.com/docs#times
Implementing this without Lodash
<section>
{Array.from({ length: 10 }, (_, i) => <span key={i}>Your text</span>)}
</section>
How does this work?
Array.from() is used in two contexts:
Creating an array from an array-like data structure. For example, we can convert a map into an array using Array.from()
const map = new Map([ [1, 2], [3, 4], [4, 5] ])
console.log(Array.from(map)) //gives an array - [[1, 2], [3, 4], [4, 5]]
Creating an array and filling out the values (This can be handy when we need to create an array containing more elements)
Array.from() accepts an object and a callback function.
Array.from({ length: 7 }, (() => 10)) // gives [10,10,10,10,10,10,10]
We can take advantage of the index (second parameter) inside the callback function to provide unique array elements
Array.from({ length: 4 }, ((_, i) => i + 1)) // [1,2,3,4]
I'm using this and works for me.
[...Array(10)].map((elementInArray, index) => (
<div key={index}>
Text in Loop
</div>
))
Using _.times: https://jsfiddle.net/v1baqwxv/
var Cards = React.createClass({
render() {
return <div>cards {
_.times( this.props.count, () => <span>♦</span> )
}</div>;
}
});
You could do it like this (without lodash):
var numberOfCards = 8; // or more.
if (data.hand >= numberOfCards) {
var cards = [];
for (var i = 0; i < numberOfCards; i++) {
cards[i] = (<span className="busterCards">♦</span>);
}
}
Straight forward options ways to do that without any external libraries (2021):
// straight forward but without key index. Not so good for react but work fine with worning
Array(X).fill(<span className="busterCards">♦</span>)
// with index
Array(X).fill().map((v,i)=> <span className="busterCards">♦</span>)
Array.from( Array(X), (v,i) => <span key={i} className="busterCards">♦</span> )
// same thing basically
Array.from( {length:X}, (v,i) => <span key={i} className="busterCards">♦</span> )
[...Array(3)].map( (_,i)=> <span key={i} className="busterCards">♦</span> )
You can create an array with as many items as you need rendered and then map through the array to render the correct number of elements you need.
const totalItems = 8;
const items = new Array(totalItems).fill(null);
// .... then
return (
{items.map((_, idx) => <span className="busterCards" key = {idx}>♦</span>)}
);
You can use Lodash range.
_.range(20).map((_n, i) => <MyComponent key={i}/>)
I thought, that if someone would like to use it in many places in the code, it would be a good idea to make it an npm package: https://www.npmjs.com/package/repeat-component. I think it will help someone :)
I believe I have two basic problems, which are probably connected. I'm trying to place an event handler with a callback function on a nested component. It didn't seem to be doing anything, so I replaced the callback function with an alert of JSON.stringify(this.props) to see if that would shed any light. It illuminated two problems: 1) my callback function was not in the props. 2) the alert popped up 2 times on page load, but did not pop up on click, like it was supposed to. I'm working through this React tutorial. Here are the relevant components:
var App = React.createClass({
mixins: [Catalyst.LinkedStateMixin],
getInitialState: function(){
return {
fishes: {},
order: {}
}
},
componentDidMount: function(){
base.syncState(this.props.params.storeId + '/fishes', {
context: this,
state: 'fishes'
});
var localStorageRef = localStorage.getItem('order-' + this.props.params.storeId);
if(localStorageRef){
this.setState({
order: JSON.parse(localStorageRef)
});
}
},
componentWillUpdate: function(nextProps, nextState){
localStorage.setItem('order-' + this.props.params.storeId, JSON.stringify(nextState.order));
},
loadSamples: function(){
this.setState({
fishes: require('./sample-fishes.js')
});
},
addFish: function(fish){
var timestamp = (new Date()).getTime();
this.state.fishes['fish-' + timestamp] = fish;
this.setState({ fishes: this.state.fishes });
},
removeFish: function(key){
if(confirm("Are you sure you want to remove this fish?")){
this.state.fishes[key] = null;
this.setState({ fishes: this.state.fishes });
}
},
addToOrder: function(key){
this.state.order[key] = this.state.order[key] + 1 || 1;
this.setState({ order: this.state.order });
},
// <<<<<<<< the function I'm having trouble with >>>>>>>>
removeFromOrder: function(key){
alert('hi');
delete this.state.order[key];
this.setState({ order: this.state.order });
},
renderFish(key){
return <Fish key={key} index={key} details={this.state.fishes[key]} addToOrder={this.addToOrder}/>
},
render: function(){
return (
<div className="catch-of-the-day">
<div className="menu">
<Header tagline="Fresh Seafood Market"/>
<ul className="list-of-fish">
{/*{ Object.keys(this.state.fishes).map(this.renderFish) }*/}
{ Object.keys(this.state.fishes).length > 0 ? Object.keys(this.state.fishes).map(this.renderFish) : <li>No Fishes!</li> }
</ul>
</div>
// <<<<<<<< I pass the function through to the Order component >>>>>>>>
<Order fishes={this.state.fishes} order={this.state.order} removeFromOrder={this.removeFromOrder}/>
<Inventory fishes={this.state.fishes} addFish={this.addFish} removeFish={this.removeFish} loadSamples={this.loadSamples} linkState={this.linkState}/>
</div>
)
}
});
var Order = React.createClass({
renderOrder: function(key){
var fish = this.props.fishes[key];
var count = this.props.order[key];
// <<<<<<<< the onClick I'm having trouble with >>>>>>>>
var removeButton = <button onCLick={this.props.removeFromOrder.bind(null, key)}>×</button>
// var removeButton = <button onCLick={alert(JSON.stringify(this.props))}>×</button>
if(!fish) {
return <li key={key}>Sorry, that fish is no longer available! {removeButton}</li>
// return <li key={key}>Sorry, that fish is no longer available!</li>
}
return (
<li key={key}>
{count}lbs
{" " + fish.name}
<span className="price">{helpers.formatPrice(count * fish.price)} {removeButton}</span>
{/*<span className="price">{helpers.formatPrice(count * fish.price)}</span>*/}
</li>
)
},
render: function(){
var orderIds = Object.keys(this.props.order);
var total = orderIds.reduce((prevTotal, key)=>{
var fish = this.props.fishes[key];
var count = this.props.order[key];
var isAvailable = fish && fish.status === 'available';
if(isAvailable) {
return prevTotal + (count * parseInt(fish.price) || 0);
}
return prevTotal;
}, 0);
return (
<div className="order-wrap">
<h2 className="order-title">Your Order</h2>
<ul className="order">
{ orderIds.length > 0 ? orderIds.map(this.renderOrder) : ""}
<li className="total">
<strong>Total:</strong>
{helpers.formatPrice(total)}
</li>
</ul>
</div>
)
}
});
The props for Order should include: the available fishes with all of their details, the current order with a fish id and quantity, and the removeFromOrder callback. When I explore the component in React dev tools, it has all of these things.
When I replace the removeFromOrder callback with an alert of the props, what happens is:
- on click, nothing
- on page refresh, two alerts pop up: the props in the first include the current order and an empty fishes array, the props in the second include the current order and the populated fishes array. Neither show the removeFromOrder callback function, which appears to be undefined from the perspective of the event listener.
On a potentially related note, when I explore the component in React dev tools and hover over a list item in the Order, I get the following error: TypeError: node.getBoundingClientRect is not a function. I'm not sure if this is part of my problem; if it's not, I'm not too concerned about it, since it only seems to pop up when I hover over the element in dev tools.
Thank you for reading this long thing, and any help would be much appreciated!
As #azium pointed out, the problem was a simple typo: onCLick={alert()} should instead be onClick={() => alert()}. Facepalm.
So I'm attempting to render multiple input fields with React.
Everything looks fine until I remove an item. Always the last item is being "removed". If you want to try my code, write "A" in input field 1, "B" in 2, "C" in 3 and remove "B". You'll notice that you have removed "C" instead.
I have tried both value and defaultValue for input to no avail. I have also tried giving a name to the input. I think I am missing a key point here.
Any recommendations?
var MultiInput = React.createClass({
getInitialState: function() {
value = this.props.value
// force at least one element
if (!value || value == '') {
value = [ null ]
}
return {
value: value
}
},
getDefaultProps: function() {
return {
}
},
add_more: function() {
new_val = this.state.value.concat([])
new_val.push(null)
this.setState({ value: new_val })
},
remove_item: function(e, i) {
new_state = this.state.value.concat([])
new_state.splice(i,1)
this.setState({ value: new_state })
},
render: function() {
me = this
// console.log(this.state.value)
lines = this.state.value.map( function(e, i) {
return (
<div key={i}>
<input value={e} />
<button onClick={me.remove_item} >X</button>
</div>
)
})
return (
<div>
{lines}
<button onClick={this.add_more}>Add More</button>
</div>
)
}
})
There are a few things going on here.
To start, you shouldn't use the array index as the key when rendering in an array:
lines = this.state.value.map( function(e, i) {
return (
<div key={i}>
<input value={e} />
<button onClick={me.remove_item} >X</button>
</div>
)
})
The first time through, ["A", "B", "C"] renders:
<div key={0}>
...
</div>
<div key={1}>
...
</div>
<div key={2}>
...
</div>
Then, the second time, once you've removed "B" and left ["A", "C"], it renders the following:
<div key={0}>
...
</div>
<div key={1}>
...
</div>
So, when you removed item at index 1, the item previous at index 2 moves to index 1. You'll want to use some unique value that doesn't change when the position in the array changes.
Second, you should use the empty string instead of null for initialization, and then you'll see that you can't type anything in your inputs. That's because value ensures that an input's value is always whatever you pass it; you'd have to attach an onChange handler to allow the value to be edited.
Changing to defaultValue allows you to type in the box, but when you type, the string in this.state.value doesn't get updated--you'd still need an onChange handler.
Finally, your button has an onClick of this.remove_item, but your remove_item method seems to take the event and index as parameters. However, React will not pass the current index to remove_item; you would need to create a new function that passes the correct params:
onClick={me.remove_item.bind(null, i)}
That said, you really shouldn't call Function#bind inside render as you'll create new functions every time it runs.
Working Code
#BinaryMuse clearly explains why my code above doesn't work: by removing an item from the array and render is called again, the items change position and apparently React's algorithm picks the "wrong changes" because the key we're providing has changed.
I think the simplest way around this is to not remove the item from the array but rather replace it with undefined. The array would keep growing with this solution but I don't think the number of actions would slow this down too much, especially that generating a unique id that doesn't change might involve storing this ID as well.
Here's the working code: (If you wish to optimize it, please check #BinaryMuse's suggestions in the accepted answer. My MultInput uses a custom Input component that is too large to paste here =) )
var MultiInput = React.createClass({
getInitialState: function() {
value = this.props.value
if (!value || value == '') {
value = [ '' ]
}
return {
value: value
}
},
getDefaultProps: function() {
return {
}
},
add_more: function() {
new_val = this.state.value.concat([])
new_val.push('')
this.setState({ value: new_val })
},
remove_item: function(i,e) {
new_state = this.state.value.concat([])
new_state[i] = undefined
this.setState({ value: new_state })
},
render: function() {
me = this
lines = this.state.value.map( function(e, i) {
if (e == undefined) {
return null
}
return (
<div key={i}>
<input defaultValue={e} />
<button onClick={me.remove_item.bind(null, i)} >X</button>
</div>
)
}).filter( function(e) {
return e != undefined
})
return (
<div>
{lines}
<button onClick={this.add_more}>Add More</button>
</div>
)
}
})
i have issues to probably understand ReactJS well enough to create dynamic div wrapper in every five steps. To be more specific here is an example:
render() {
return (
<div className='holder'>
{this.props.elements.map(
(b,n) =>
{n%5 == 0 ? '<div class="grid grid-pad">' : ''}
<Component param={b} />
{n%5 == 0 ? '</div>' : ''}
)}
</div>
)
}
The results should look like this:
<div class='grid grid-pad'>
<div class='child'></div>
<div class='child'></div>
<div class='child'></div>
<div class='child'></div>
<div class='child'></div>
</div>
<div class='grid grid-pad'>
<div class='child'></div>
<div class='child'></div>
...
</div>
....
So the result would be that every 5 elements would be wrapped in div.
I am aware that this is not the right way, at this code actually it produces errors for not closed tags. Is there any way how to actually achieve similar functionality. Thank you in advance.
As you can't dynamically create a React component from a string quite like that. You'll just need to group the children manually and add them as a whole to a container within the render method.
Here's an example: http://jsfiddle.net/wiredprairie/ko8so1mu/.
If you were using something like lodash, you could reduce the number of lines of code below by using the take function.
render() {
// make all elements into Components
var elements = this.props.elements || [];
var components = elements.map(
(b) => {
return <Component param={b} />;
}
);
// then just group into chunks of 5
var groups = [];
var children = [];
while(components.length > 0) {
children.push(components.shift());
if (children.length === 5) {
groups.push(<div className="grid grid-pad">{children}</div>);
children = [];
}
}
// remaining children
if (children.length > 0 ) {
groups.push(<div className="grid grid-pad">{children}</div>);
}
return (
<div className='holder'>
{ groups }
</div>
);
}
This is how you could do it:
/** #jsx React.DOM */
var Parent = React.createClass({
getInitialState: function() {
return {
list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
}
},
render: function() {
return(
<div>
<Child elements={this.state.list} />
</div>
);
}
});
var Child = React.createClass({
render: function() {
var itemsList = this.props.elements.map(function(element, index) {
if (index % 5 == 0) {
return (
<div> {element} </div>
);
}
});
return(
<div>
{itemsList}
</div>
);
}
});
React.render(<Parent />, document.getElementById('app'));
You keep the data in the Parent component then pass it to the Child component in props.
if you do console.log(itemsList) you can see what is happening inside map() method.
Here is JSFiddle.
Edit:
After you updated your question, here is something I quickly hacked, this is not the best way of doing it, but it works:
/** #jsx React.DOM */
var Parent = React.createClass({
getInitialState: function() {
return {
list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
}
},
render: function() {
return(
<div>
<Child elements={this.state.list} />
</div>
);
}
});
var Child = React.createClass({
render: function() {
var arr_index = -1;
var itemsList = [];
var items = this.props.elements.map(function(item, i) {
if (i % 5 == 0) {
arr_index += 1
}
itemsList.push(<div> {item}, conatiner: {arr_index} </div>)
});
return (
<div>{itemsList}</div>
);
}
});
React.render(<Parent />, document.getElementById('app'));
You still need to tweak it a bit to meet your requirements, but basically it divides children into "containers", I guess that's what you were looking for.
I started to learn React and I hit first wall.
I have a list component which should display a list of rows + button for adding a new row.
All is in those 2 gists:
https://gist.github.com/matiit/7b361dee3f878502e10a
https://gist.github.com/matiit/8bac28c4d5c6ce3993c7
The addRow method is executed on click, because I can see the console.log, but no InputRows are added.
Can't really see why.
This is a little updated (dirty) code which doesn't work either.
Now it's only one file:
var InputList = React.createClass({
getInitialState: function () {
return {
rowCount: 1
}
},
getClassNames: function () {
if (this.props.type === 'incomes') {
return 'col-md-4 ' + this.props.type;
} else if (this.props.type === 'expenses') {
return 'col-md-4 col-md-offset-1 ' + this.props.type;
}
},
addRow: function () {
this.state.rowCount = this.state.rowCount + 1;
this.render();
},
render: function () {
var inputs = [];
for (var i=0;i<this.state.rowCount; i++) {
inputs.push(i);
}
console.log(inputs);
return (
<div className={ this.getClassNames() }>
{inputs.map(function (result) {
return <InputRow key={result} />;
})}
<div className="row">
<button onClick={this.addRow} className="btn btn-success">Add more</button>
</div>
</div>
);
}
});
this.render() doesn't do anything. If you look at the function, it simply does some calculations and returns some data (the virtual dom nodes).
You should be using setState instead of directly modifying it. This is cleaner, and allows react to know something's changed.
addRow: function () {
this.setState({rowCount: this.state.rowCount + 1});
},
Don't store list of components in state.
Instead store the income row count and expense row count in state.
Use click handler to increment these counts.
Use render method to generate required rows based on count.