I'm trying to figure out how to edit comments in ReactJS. I have been following this tutorial.
There are several theories I have on the solution:
Using mutable state instead of immutable props.
Has to do with the CommentBox component which has the loadCommentsFromServer and handleCommentSubmit functions. The loadComments function fires an AJAX request, possibly to my comments.json file.
Here's the relevant code from the server.js file
var COMMENTS_FILE = path.join(__dirname, 'comments.json');
app.get('/api/comments', function(req, res) {
fs.readFile(COMMENTS_FILE, function(err, data) {
if (err) { /* Print error to console */ }
res.json(JSON.parse(data));
});
});
// This snippet of code is probably the most important
app.post('/api/comments', function(req, res) {
fs.readFile(COMMENTS_FILE, function(err, data) {
if (err) { /* Print error to console */ }
var comments = JSON.parse(data);
var newComment = {
id: Date.now(),
text: req.body.text,
};
comments.push(newComment);
fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) {
if (err) { /* Print error to console */ }
res.json(comments);
});
});
});
Here's my main script file where I generate react components
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
// Trying to change the value of the text box on edit
<p onChange={this.handleTextChange()}> {this.props.text} </p>
</div>
);
}
});
var CommentBox = React.createClass({
When the component is first created, we need to get JSON data from the server and update the state with the latest data. this.setState() allows for dynamic updates. The old array of comments is being replaced by the new one
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
This function passes data from the child components up to the parent components. It's supposed to submit to the server and refresh the list of comments
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
this.setState({data: comments});
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) {
return <Comment key={comment.id}>{comment.text}</Comment>
});
return <div className="commentList"> {commentNodes} </div>
);
}
});
Here a component is being created for filling out forms. this.state is used to save user's input as it's entered. I'm trying to accomplish this with the edit functionality.
var CommentForm = React.createClass({
getInitialState: function() {
return {text: ''};
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
// This is also probably an important function
handleSubmit: function(e) {
e.preventDefault();
var text = this.state.text.trim();
this.props.onCommentSubmit({text: text});
this.setState({text: ''});
},
The value property of the input elements will reflect the state of the component and attach onChange handlers to them
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" value={this.state.text} onChange={this.handleTextChange} />
<input type="submit" value="Post" />
</form>
);
}
});
Finally, I am rendering the CommentBox component. The url attribute fetches dynamic data from the server. The pollInterval reloads the page every 2 seconds.
ReactDOM.render(
<CommentBox url="/api/comments" pollInterval={2000} />,
document.getElementById('content')
);
Here were my thoughts on how to implement the edit functionality
setTimeout(function() {
$('.edit').on('click', function() {
$(this).prev().prop('contentEditable', 'true');
$(this).prev().focus();
});
},1000);
I had to use setTimeout because it would take some time before the component's file to be loaded. I would then listen for clicking on the edit button, and change the html5 contentEditable property to true.
The problem I have is in updating the changes to the JSON file once it's edited.
I'd also like to know if there's a more react way of accomplishing this onclick functionality
As you can see in my component's file, I added an onChange handler to the paragraph that renders the body of the text.
render: function() {
return (
<div className="comment">
<p onChange={this.handleTextChange()}> {this.props.text} </p>
</div>
);
}
I've searched the internet extensively for examples of editing functionality but couldn't find anything.
My goal was to make this code as readable as possible. I tried to trim down code that is not immediately relevant to the problem at hand. I have removed the following code:
Declaration of npm variables and app.use
The listening of the server
The author fields for the text form. We only need the text field
It's usually not a very good idea to have jQuery mucking with React components (though jQuery + React can play nice with each other for certain tasks); we are running a large scale React application and have spent many hours removing instances of this from our early days.
In terms of saving the comments, you need a new endpoint to handle that functionality, it should look almost exactly like app.post('/api/comments') except instead of getting data from reading the file, it should get data from req.body, which is the data posted to it. To keep the same url this.props.url you could set it up as a PATCH endpoint: app.patch('/api/comments' ...). I'll leave that implementation up to you. The React save functionality should happen like this: the Comment component should use state to manage it's...state. Clicking "Edit" should switch that state to have the contentEditable set to true, "Edit" become "Save", etc. The actual saving part should be defined in the parent component CommentBox and should be passed down to the Comment component. Here is a basic idea of the changes you should make to allow editing, it's 100% untested but hopefully helps out some.
// changes to Comment component
var Comment = React.createClass({
getInitialState: function() {
return {
contentEditable: false,
buttonText: 'Edit',
text: this.props.text
};
},
handleButton: function() {
var commentTag = this.refs.comment;
// if the component is currently editable and the text is different from the original, save it
if (this.state.contentEditable && commentTag.textContent != this.state.text) {
this.props.onUpdateComment(this.props.id, commentTag.textContent);
}
// invert current contentEditable state Save => Edit or Edit => Save
var editable = !this.state.contentEditable;
this.setState({
contentEditable: editable,
// update the state to reflect the edited text
text: commentTag.textContent,
// change button text based on editable state
buttonText: editable ? 'Save' : 'Edit'
});
},
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">{this.props.author}</h2>
<p ref="comment" contentEditable={this.state.contentEditable}>{this.state.text}</p>
<button onClick={this.handleButton}>{this.state.buttonText}</button>
</div>
);
}
});
// changes to CommentList
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) {
return <Comment onUpdateComment={this.props.onUpdateComment} {...comment} />
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
// changes to CommentBox
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.getJSON(this.props.url)
.then(function(newComments) {
this.setState({ data: newComments });
}.bind(this))
.fail(function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
});
},
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.post(this.props.url, comments)
.then(function(data) {
this.setState({ data: data });
}.bind(this))
.fail(function(xhr, status, err) {
this.setState({ data: comments });
console.error(this.props.url, status, err.toString());
}.bind(this));
},
onUpdateComment: function(id, comment) {
// clone state, we don't want to alter this directly
var newData = this.state.data.slice(0);
newData.forEach(function(item) {
if(item.id === id) {
item.text = comment;
}
});
$.ajax({
url: this.props.url,
dataType: 'json',
method: 'PATCH',
data: newData
}).then(function(data) {
this.setState({ data: data });
}.bind(this));
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
Related
I have an array of data being passed from one json file.
When I click on the list item it triggers a handleClick method that will get the new url and set it to the provided state. That state is then passed onto a child component that makes an ajax call using that link.
The problem is that the ajax call only loads once when the component is mounted and does not make anymore calls after that no matter how many times I click on it.
How would I get the child component to load a new url each time a different item is clicked?
Parent Component:
getInitialState: function() {
return {
gameUrl: ''
};
},
handleClick: function() {
this.setState({
gameUrl: gameUrl
});
},
render: function() {
var gameList = this.props.data.map(function(game) {
var homeTeamName = game.home_team_name;
var awayTeamName = game.away_team_name;
var gameUrl = game.game_directory+'/anotherFile.json';
console.log(homeTeamName+' vs '+awayTeamName+':'+ gameUrl);
var click = this.handleClick;
return (
<li key={game.location} className="list-group-item" onClick={click}>
{homeTeamName}
</li>
);
}, bind);
return (
<div>
<ul>
{gameList}
</ul>
<GameDetail url={this.state.gameUrl}/>
</div>
);
Child Component:
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({
data: data.data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
Implement componentWillReceiveProps method.
It will be called when the props have changed and when this is not an initial rendering.Then update the state depending on the existing and upcoming props.
componentWillReceiveProps: function(nextProps) {
this.setState({
// set something
});
}
Thanks to #WitVault managed to do it here is the revised section:
Instead of componentDidMount, I changed it to componentWillReceiveProps in the child component. Make sure the prop you're passing through is the same in the child and parent component. (i.e. in child component the prop is url, in the parent you're passing it to the same prop <GameDetail url={this.state.gameUrl}/>) Then you access it in the componentWillReceiveProps method via nextProps.url
componentWillReceiveProps: function(nextProps) {
// Access the url prop
var newUrl = nextProps.url;
$.ajax({
url: newUrl,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({
data: data.data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
I have three React components: the first is a container (NoteContainer component) which is responsible for rendering my objects (Note component) in the UI. The data is obtained as JSON via AJAX GET. The last component is a form (NoteForm) which creates new objects (via AJAX POST).
The response from the POST is only the JSON representation of the newly created object, not the JSON for all of the objects.
Should the NoteForm send the JSON response from creating a new object to the NoteContainer which would append it to its state.data and re-render , or should the NoteContainer request the full list of objects and update its state date entirely?
I would presume the first way is better since it does not require requesting data which is already present in the state of NoteContainer. However, I'm still not sure of the "best" way to handle this. Should I give NoteContainer another function, something like addNewNote, which would take the JSON data from the NoteForm and append it to state.data?
I'm new to React so I apologize if this is not a clear question. Here are my components:
var NoteContainer = React.createClass({
getInitialState: function(){
return {data: []};
},
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data){
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err){
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function(){
var noteNodes = this.state.data.map(function(note){
return (
<Note title={note.title} body={note.body} />
);
});
return (<div className='noteContainer'>{noteNodes}</div>);
}
});
var Note = React.createClass({
render: function(){
return (
<div className="note" >
<h1>{this.props.title}</h1>
<p>{this.props.body}</p>
</div>
);
}
});
var NoteForm = React.createClass({
getInitialState: function(){
return {'title': '', 'body': ''}
},
handleTitleChange: function(e){
this.setState({title: e.target.value});
},
handleBodyChange: function(e){
this.setState({body: e.target.value});
},
handleSubmit: function(e){
e.preventDefault();
var note = {
title: this.state.title,
body: this.state.body};
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: note,
success: function(data){
// Send data to NoteContainer?
}.bind(this),
error: function(xhr, status, err){
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function(){
return (
<form>
<input
type='text'
placeholder='Title'
value={this.state.title}
onChange={this.handleTitleChange} />
<textarea onChange={this.handleBodyChange}>
{this.state.body}
</textarea>
</form>
);
}
});
What #xCrZx is suggesting is that you pull the state outside of the individual components and have a one or more top-level stores to maintain state. The simplest (i.e. "vanilla") example of this would be if your NoteContainer was a parent of NoteForm. Then you could simply pass a callback from NoteContainer to NoteForm:
var NoteContainer = React.createClass({
createNote: function() {
...
},
render: function() {
return (
...
<NoteForm createNote={this.createNote}>
...
);
}
});
var NoteForm = React.createClass({
props: {
createNote: React.PropTypes.func.isRequired
},
render: function() {
return (
...
onClick={this.props.createNote}
...
);
}
});
However, that of course only works if the relationship actually exists. Now let's take a look at Reflux, where you create central stores (and actions to go with them) to keep data, and components "listen" to the stores.
var NoteActions = Reflux.createActins([
'createNote',
'getNotes'
]);
var NoteStore = Reflux.createStore({
listenables: [NoteActions],
init: {
// Notes is an empty array by default
this.notes = [];
},
getInitialState: function() {
return {
notes: this.notes
};
},
onCreateNote: function(noteFormData) { ...POST here, save note JSON to this.notes on success... },
onGetNotes: function() { ..GET here for the initial load...}
});
var NoteForm = React.createClass({
render: function() {
...
onClick={NoteActions.createNote(noteFormData)}
...
}
});
var NoteContainer = React.createClass({
mixins: [Reflux.connect(NoteStore)],
componentDidMount: function() {
NoteActions.getNotes();
},
render: function() {
return: function() {
.. same that you have now using this.state.notes
}
}
});
Hope this is starting to make sense. Highly recommend looking through the Reflux (or Redux, similar but different) examples.
The best approach is to keep all notes in global state and add new entities there one by one when needed. It can be achieved with help of global stores, like Redux or Reflux.
This is the java code that returns a List
#Override
public List get_News(String lang) {
Session session = this.sessionFactory.getCurrentSession();
Query query = session.createQuery("select n.text from tableName n where n.lang=:lang");
query.setParameter("lang", lang);
List result = query.list();
if(result != null){
if(result.iterator().hasNext()){
return result; //this is the result that is being returned
}
}
return null;
}
Following is the React JS code
var Comment = React.createClass({
displayName: "Comment",
loadCommentsFromServer: function loadCommentsFromServer() {
$.ajax({
url: "/abc/ages",
dataType: 'json',
cache: true,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
}.bind(this)
});
},
getInitialState: function(){
return {data: []};
},
componentDidMount: function componentDidMount() {
this.loadCommentsFromServer();
},
render: function render() {
return React.createElement("div",{ className: "comment" },
React.createElement("div", { }, this.state.data) //I want to iterate over this data
);
}
});
function RenderComment(){
ReactDOM.render(React.createElement("div", {}, React.createElement(Comment,{})),
document.getElementById('content')
);
}
As of now I am getting all text from database, but all rows get displayed as a single chunk of data. I want to iterate over the data and show as
text1
text2
text3
rather than
text1
text2
text3
Please help me.
If the data is an array, you could do something like this (not sure if you're using react or react native):
React:
render() {
var data = this.state.data.map((d) => {
return <p>{d}</p>
})
return <div>
{data}
</div>
}
React Native:
render() {
var data = this.state.data.map((d) => {
return <Text>{d}</Text>
})
return <View>
{data}
</View>
}
I'm using React with Rails 4.
I've got the following:
<%= react_component('Box',url: "blah", pollInterval: 2000) %>
and then my components:
var Box = React.createClass({
getInitialState: function () {
return {data: []};
},
loadStuffFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
console.log(data);
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadStuffFromServer();
setInterval(this.loadStuffFromServer, this.props.pollInterval);
},
render: function() {
return (
<div>
<Widget office="1" data="{this.state.data}"/>
</div>
);
}
});
var Widget = React.createClass({
render: function () {
return (
<div>
{this.props.data}
</div>
)
};
});
For the Box, I can see using React DevTools for Chrome that the state is being set to the JSON returned by the url. However for the Widget component, when I try to echo the props out it literally returns: {this.state.data}
So the props is being set, but to a string instead of the JSON array?
Any property inside quotes is a string:
<Widget office="1" data="{this.state.data}"/>
To use a JavaScript expression, use only the curlies:
<Widget office="1" data={this.state.data}/>
I have a tweet box component which has a getAllTweets function called on submit. The handle submit functionis supposed to capture the value of the field and pass it to thegetAllTweets functionso it the URL, so I can make a dynamic Ajax request. I am unable to save the variable containing thehandle` value and append it. Secondly, when I get the response I need to bind it to the TweetList Component. I can't get either to work..
var Tweet = React.createClass({
render: function() {
var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return (
<div className="tweet">
<h2 className="tweetAuthor">
{this.props.handle}
</h2>
<span dangerouslySetInnerHTML={{__html: rawMarkup}} />
</div>
);
}
});
var TweetBox = React.createClass({
getAllTweets: function(handle) {
var handle = this.state.data;
$.ajax({
url: this.props.url + handle,
dataType: 'json',
type: 'POST',
data: tweet,
success: function(data) {
console.log(data);
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
render: function() {
return (
<div className="tweetBox">
<h1>Tweets</h1>
<TweetList data={this.state.data} />
<TweetForm onTweetSubmit={this.getAllTweets} />
</div>
);
}
});
var TweetList = React.createClass({
render: function() {
var tweetNodes = this.props.data.map(function(tweet, index) {
return (
<Tweet handle={tweet.handle} key={index}>
{tweet.message}
</Tweet>
);
});
return (
<div className="tweetList">
{tweetNodes}
</div>
);
}
});
var TweetForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var handle = React.findDOMNode(this.refs.handle).value.trim();
if (!handle) {
return;
}
this.props.onTweetSubmit({handle: handle, message: message});
React.findDOMNode(this.refs.handle).value = '';
},
render: function() {
return (
<form className="tweetForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="handle" />
<input type="submit" value="Post" />
</form>
);
}
});
React.render(
<TweetBox url="http://localhost:4000/api/"/>,
document.getElementById('content')
);
First of all, you forget to declare the message variable in your TweetForm component.
this.props.onTweetSubmit({handle: handle, message: message});
In your getAllTweets function, you pass in a handle variable and create another new handle variable again (var handle = this.state.data). That's why you can not get the tweet account name.
Don't use the same name to multiple variables in a method.