I'm pretty new to React, but liking it so far. I'm building a large application, which is going well, except I've run into an issue. I'm building a list of responses to questions, and they can be deleted, but I also want to have a "Cancel" button so all unsaved changes can be reverted. What is confusing me is the cancel button reverts to the initial state for the name value, but not the responses. If I add in some console logging to the response deletion script, I would expect to see log lines 1 & 2 match, with 3 being different. However, I'm seeing that 1 is the original, but 2 & 3 match. Why is state being updated before I call setState, and why does updating state seem to update the my initial props?
EDIT: I added a jsFiddle
getInitialState: function() {
return {
name: this.props.question.name,
responses: this.props.question.responses,
};
},
handleCancelButtonClick: function(e) {
this.replaceState(this.getInitialState());
},
handleNameChange: function(e) {
this.setState({name: e.target.value});
},
handleResponseDeletion: function(e) {
var resp = this.state.responses;
var from = Number(e.target.value);
console.log(JSON.stringify(this.state.responses));
resp.splice(from, 1);
console.log(JSON.stringify(this.state.responses));
this.setState({responses: resp});
console.log(JSON.stringify(this.state.responses));
},
render: function() {
var key = "mp" + this.props.question.name;
var resp = [];
if (this.state.responses) {
this.state.responses.forEach(function(response, i) {
var rkey = "r_" + this.props.question.name + "_" + i;
resp.push(<ModalResponse response={response} key={rkey} value={i} deleteResponse={this.handleResponseDeletion} />);
}.bind(this));
}
return (
<layer id={this.props.question.name} style={questionModal} key={key}>
<h2>Edit {this.state.name}</h2>
<button onClick={this.handleCancelButtonClick}>Cancel</button>
<div class='form-group'>
<label for='client_name' style={formLabel}>Question Name:</label><br />
<input type='text' style={formControl} id='question_name' name='question_name' value={this.state.name} onChange={this.handleNameChange} required />
</div>
<div class='form-group'>
<label style={formLabel}>Responses:</label><br />
<ul style={responseList} type="response_list" value={this.props.qname}>
{resp}
</ul>
</div>
</layer>
);
}
});
The problem is that splice modifies original array. It means the one that belongs to the original question. So when you call getInitialState from within handleCancelButtonClick you get modified array.
To avoid this you need to somehow clone original data inside getInitialState. For example
getInitialState: function() {
//copy array and responses
const copy = resp => ({...resp})
return {
name: this.props.question.name,
responses: this.props.question.responses.map(copy)
};
}
Here's what I did to fix the issue:
handleResponseDeletion: function(e) {
var resp = []
var from = Number(e.target.value);
this.state.responses.forEach(function(res, i) {
if (i != from) {
resp.push(res);
}
});
this.setState({responses: resp});
},
Related
I have a component with prop List. List is list of input files. At once input changed I add another one input.
Weird behavior if I try to delete .
https://jsfiddle.net/apokjqxx/115/
removeAnother: function(item) {
var vm = this;
var num = vm.$parent.cornerList.indexOf(item);
vm.$parent.cornerList.splice(num, 1);
},
How to reproduce:
choose file in first input
choose file in second input (will added after step 1)
choose file in third input (will added after step 2)
then click to remove on first item in list
Expected: removed first item but has removed last added
Use a key on your list.
<div v-for="(item, index) in list" :key="item.id">
I modified your fiddle to generate an id for each object added to the cornerList array.
var formuploadimage = Vue.extend({
template: '#template-form-upload-image',
props: {
list: {
type: Array
}
},
data: function() {
return {
isFileChanged: false
}
},
watch: {
validCnt: function() {
},
},
methods: {
onFileChange: function(item) {
var vm = this;
let id = Math.max.apply(Math, vm.$parent.cornerList.map(c => c.id)) + 1
var newItem = {id};
vm.$parent.cornerList.push(newItem);
},
removeAnother: function(item) {
var vm = this;
var num = vm.$parent.cornerList.indexOf(item);
vm.$parent.cornerList.splice(num, 1);
},
},
});
var app = new Vue({
el: ".lists-wrappers",
data: {
cornerList: [{id: 1}],
},
components: {
formuploadimage: formuploadimage
},
methods: {
},
});
.select-file{
width:250px;
border:1px solid red;
}
<script src="https://unpkg.com/vue#2.4.4/dist/vue.js"></script>
<div class="lists-wrappers">
<formuploadimage :list="cornerList"></formuploadimage>
</div>
<script type="text/x-template" id="template-form-upload-image">
<div>
<div v-for="(item, index) in list" :key="item.id">
<div class="select-file">
REMOVE<br/>
<label for="file-input">
+Add photo
</label>
<input type="file" #change="onFileChange(item)" />
</div>
</div>
</div>
</script>
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 used jquery in the function checkall to check/uncheck all the checkboxes in a specific column for a fixed-data-table. I wonder if this is the best approach or is there a "react-way" of doing this?
'use strict';
var React = require('react/addons');
var Table = require('fixed-data-table').Table;
var Column = require('fixed-data-table').Column;
// Table data as a list of array.
var rows = [
['a1', 'b1', 'c1'],
['a2', 'b3', 'c2'],
['a3', 'b3', 'c3'],
];
function checkBox(data){
return (
<input type="checkbox" value={data} className="kill-checkbox" />
);
}
function rowGetter(rowIndex) {
return rows[rowIndex];
}
// Fixed data tables (Facebook) requirement:http://facebook.github.io/fixed-data-table/
require('../../styles/fixed-data-table.css');
var TableTester = React.createClass({
render: function () {
return (
<div>
<label>Check All</label> <input type="checkbox" id="check-all" onClick={this.checkall} />
<Table rowsCount={rows.length} width={1000} height={300} headerHeight={50} rowHeight={50} rowGetter={rowGetter}>
<Column label="Col 1" width={500} dataKey={0} />
<Column label="Col 2" width={500} dataKey={1} cellRenderer={checkBox} align="center" />
</Table>
</div>
);
},
checkall:function(){
var checked = $("#check-all").is(':checked');
$(".kill-checkbox").each(function(){
$(this).prop('checked', checked);
});
},
});
module.exports = TableTester;
It depends what a checkbox being checked or unchecked means in your application and when you need this information. Doing it this way effectively puts state into the DOM and you will have to go back to the DOM to get a hold of it when you need that information later.
Another way to do this would be to have some data which represents or can be used to derive the checked-ness of each checkbox and use this to set its checked prop. You would then need to implement the appropriate event handlers to update the data appropriately based on the user's actions.
e.g., if you're displaying a grid of checkboxes which have associated values, you might have a selectedValues list. We'll put it in the component's own state for convenience of creating example code, but this might be coming in as a prop, or live in a store, or...
getInitialState() {
return {
selectedValues: []
}
},
You could then derive the checked state of each checkbox using the list of selected values:
renderCheckbox(data) {
var checked = this.state.selectedvalues.indexOf(data) != -1
return <input type="checkbox" value={data} checked={checked}/>
}
in render(): cellRenderer={this.renderCheckbox}
But now your checkboxes are read-only, because you haven't implemented changing selectedValues in response to an event on them, so let's do that:
_onCheckboxChange(e) {
var {selectedValues} = this.state
var {checked, value} = e.target
if (checked) {
selectedValues.push(value)
}
else {
selectedValues.splice(selectedValues.indexOf(value), 1)
}
this.setState({selectedValues})
},
renderCheckbox(data) {
var checked = this.state.selectedValues.indexOf(data) != -1
return <input type="checkbox" value={data} checked={checked} onChange={this._onCheckboxChange}/>
}
Now to implement something like a select/deselect all toggle, you would need to implement an event handler which makes the appropriate data changes and causes the component to re-render:
_onCheckAll(e) {
var selectedValues
if (e.target.checked) {
// Set all the available values as selected - using the rows variable
// from your question to put something meaningful here.
selectedValues = rows.reduce((all, row) => all.concat(row))
}
else {
selectedValues = []
}
this.setState({selectedValues})
}
I suppose that means the "React way" is to model your UI in terms of data, rather than the end result you want to see, in this case it's "which of the values I'm displaying are selected?" versus "which checkboxes are checked?"
As promised I have a working example based on the solution/explanation provided in a fixed-data-table issue: https://github.com/facebook/fixed-data-table/issues/70
'use strict';
var React = require('react/addons');
var Table = require('fixed-data-table').Table;
var Column = require('fixed-data-table').Column;
var dataKey = 1; // key to store in selected in the sets below it represents the values [b1|b2|b3] as in index 1
// Fixed data tables (Facebook) requirement:http://facebook.github.io/fixed-data-table/
require('../../styles/fixed-data-table.css');
var TableTester = React.createClass({
getInitialState: function(){
return ({
selected:[],
rows: [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3'],
]
})
},
render: function () {
var data = this.state.rows;
return (
<div>
<label>Check All</label> <input type="checkbox" onClick={this._toggleAll} />
<Table rowsCount={data.length} rowGetter={this._rowGetter} onRowClick={this._onRowSelect} width={1000} height={300} headerHeight={50} rowHeight={50}>
<Column label="Col 1" width={500} dataKey={0} />
<Column label="Col 2" width={500} dataKey={dataKey} align="center"
cellRenderer={(value) => <input type="checkbox" value={value} checked={this.state.selected.indexOf(value) >= 0} onChange={() => {}} />}/>
</Table>
</div>
);
},
_toggleAll:function(e){
var newSet = [];
if (e.target.checked) {
this.state.rows.forEach(function(columns){
newSet.push( columns[1] );
});
}
this.setState({
selected: newSet
});
},
_onRowSelect: function(e, index) {
var value = this.state.rows[index][dataKey];
var newSet = this.state.selected;
if ( newSet.indexOf(value) < 0 ) { // does not exist in list
newSet.push(value);
} else {
newSet.splice(newSet.indexOf(value), 1);
}
this.setState({
selected: newSet
});
},
_rowGetter: function(rowIndex){
return this.state.rows[rowIndex];
}
});
module.exports = TableTester;
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.