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;
Related
I want to delete a row from html table on button click using ReactJS. The problem is that on clicking delete button always last row is being deleted instead of the row that is clicked. Please tell what is the issue with my code?
code:
var RecordsComponent = React.createClass({
getInitialState: function() {
return {
rows: ['row1', 'row2', 'row3'],
newValue: "new value"
}
},
render : function() {
return (
<div>
<table>
<tbody>
{this.state.rows.map((r) => (
<tr>
<td>{r}</td>
<td>
<button onClick={this.deleteRow}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
<input trype="text" id={"newVal"} onChange={this.updateNewValue}></input>
<button id="addBtn" onClick={this.addRow}>ADD</button>
</div>
);
},
updateNewValue: function(component) {
this.setState({
newValue: component.target.value
});
},
addRow : function() {
var rows = this.state.rows
rows.push(this.state.newValue)
this.setState({rows: rows})
},
deleteRow : function(record) {
var index = -1;
var clength = this.state.rows.length
for( var i = 0; i < clength; i++ ) {
if( this.state.rows[i].value === record.target.value ) {
index = i;
break;
}
}
var rows = this.state.rows
rows.splice( index, 1 )
this.setState( {rows: rows} );
}
});
React.render(<RecordsComponent/>, document.getElementById('display'))
You need pass a param on func deleteRow
<button onClick={() => this.deleteRow(r)}>Delete</button>
I refactor a litte bit of your func
deleteRow : function(record) {
this.setState({
rows: this.state.rows.filter(r => r !== record)
});
}
In deleteRow you are looking for the index of the row that matches record.target.value (record is actually an event), but event.target.value is blank so the index remains -1, and rows.splice(-1, 1) deletes the last element.
You should pass the row data itself, like:
<button onClick={e => this.deleteRow(r)}>Delete</button>
I'm looking to implement a table in ReactJS with the following features:
initially empty
rows are dynamically added and removed
when there are no rows, an empty state (e.g. a box saying "Table empty") should be displayed
when a row is removed, there should be a fade out transition
when the first row is added, there should be no fade out transition on the empty state
I came up with two approaches using ReactCSSTransitionGroup.
1. Wrap only rows into ReactCSSTransitionGroup
Codepen: https://codepen.io/skyshell/pen/OpVwYK
Here, the table body is rendered in:
renderTBodyContent: function() {
var items = this.state.items;
if (items.length === 0) {
return (
<tbody><tr><td colSpan="2">TABLE EMPTY</td></tr></tbody>
);
}
const rows = this.state.items.map(function(name) {
return (
<tr key={name}>
<td>{name[0]}</td>
<td>{name[1]}</td>
</tr>
);
});
return (
<ReactCSSTransitionGroup
component="tbody"
transitionName="example"
transitionEnter={false}
transitionLeave={true}>
{rows}
</ReactCSSTransitionGroup>
);}
The issue is that the last row to be removed does not get the fade out transition before disappearing since the ReactCSSTransitionGroup is not rendered when item.length === 0.
2. Wrap table body into ReactCSSTransitionGroup
Codepen: https://codepen.io/skyshell/pen/RpbKVb
Here, the entire renderTBodyContent method is wrapped into ReactCSSTransitionGroup within the render method:
<ReactCSSTransitionGroup
component="tbody"
transitionName="example"
transitionEnter={false}
transitionLeave={true}>
{this.renderTBodyContent()}
</ReactCSSTransitionGroup>
And the RenderTBody method looks like:
renderTBodyContent: function() {
var items = this.state.items;
if (items.length === 0) {
return (
<tr><td colSpan="2">TABLE EMPTY</td></tr>
);
}
const rows = this.state.items.map(function(name) {
return (
<tr key={name}>
<td>{name[0]}</td>
<td>{name[1]}</td>
</tr>
);
});
return rows;}
The issue is that the empty state gets animated too.
Any suggestions on how to obtain the desired behaviour?
Thanks!
Thank you realseanp for your pointers. Using the low level API and TweenMax instead of CSS transitions, I came up with the following solution. First, introduce a Row component:
var Row = React.createClass({
componentWillLeave: function(callback) {
var el = React.findDOMNode(this);
TweenMax.fromTo(el, 1, {opacity: 1}, {opacity: 0, onComplete: callback})
},
componentDidLeave: function() {
this.props.checkTableContent();
},
render: function() {
const name = this.props.name;
return (
<tr>
<td>{name[0]}</td>
<td>{name[1]}</td>
</tr>
);
}
});
Then populate the table based on an isEmpty flag:
var Table = React.createClass({
getInitialState: function() {
return {
items: [],
isEmpty: true
};
},
addRow: function() {
var items = this.state.items;
var firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
var lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
items.push([firstName, lastName]);
this.setState({items: items, isEmpty: false});
},
removeLastRow: function() {
var items = this.state.items;
if (items.length != 0) {
items.splice(-1, 1);
this.setState({items: items});
}
},
checkTableContent: function() {
if (this.state.items.length > 0) {
this.setState({isEmpty: false});
}
else {
this.setState({isEmpty: true});
this.forceUpdate();
}
},
renderTBodyContent: function() {
if (this.state.isEmpty) {
return (
<tr><td colSpan="2">TABLE EMPTY</td></tr>
);
}
var that = this;
const rows = this.state.items.map(function(name) {
return <Row
key={name}
name={name}
checkTableContent={that.checkTableContent} />;
});
return rows;
},
render: function() {
return (
<div>
<button onClick={this.addRow}>Add row</button>
<button onClick={this.removeLastRow}>Remove row</button>
<table>
<thead>
<tr>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<ReactTransitionGroup
component="tbody"
transitionName="example"
transitionEnter={false}
transitionLeave={true}>
{this.renderTBodyContent()}
</ReactTransitionGroup>
</table>
</div>
);
}
});
Codepen: https://codepen.io/skyshell/pen/yMYMmv
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});
},
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 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.