NOTE: I improved the final solution I show here in this other message: Still REACT-ifying: improving the services API.
We want to create two lists of
persons, such that the user can change persons from one list to the other, as
we show in this figure:
The
list of persons should come from a file on the server’s disk. Furthermore, if
this list changes, both web lists might change as well. Names that have
disappeared should also leave the lists, while new names in the text file
should show up on the left side list.
This video shows what I want:
My first approach was to do this HTML with two different forms, one per button, with empty divs for each of the select boxes, to be rendered in React.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Move the names from one box to the
other</title>
<!-- Not present in the
tutorial. Just for basic styling. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
</head>
<body>
<table>
<tr>
<td>
<h1>Add:</h1>
</td>
<td />
<td>
<h1>Remove:</h1>
</td>
</tr>
<tr>
<td>
<div id="fromNode" />
</td>
<td>
<form id="addform" action="/add"
method="post">
<input name="add" value="-->"
type="submit" /> <br />
</form>
<form id="subform" action="/sub"
method="post">
<input name="remove" value="<--"
type="submit" />
</form>
</td>
<td>
<div id="toNode" />
</td>
</tr>
</table>
<script type="text/babel" src="/static/react2/selectpeople.js" />
</body>
</html>
|
The select boxes are relatively straightforward to do in React and I did them here for exercise 2 of the same list. In the render() function, we go through the list of persons, whose names should be in the box, and dynamically build the select component. A difference here is that I use a simple for instead of the map function, just for the sake of exemplifying. I don't go into details regarding the PeopleList component, because this is pretty much the same of exercise 2.
var PeopleList = React.createClass({
newdata:
function(data) {
this.setState({people: data});
},
loadPersonsFromServer:
function() {
$.ajax({
url:
this.props.url,
dataType:
'json',
cache:
false,
success:
function(data) {
this.newdata(data);
}.bind(this),
error:
function(xhr, status,
err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState
: function() {
return {people : []};
},
componentDidMount
: function() {
this.loadPersonsFromServer();
this.timer = setInterval(this.loadPersonsFromServer, 10000);
},
componentWillUnmount
: function() {
clearInterval(this.timer);
},
render
: function() {
var theoptions = [];
for (var i = 0; i < this.state.people.length; i++) {
var person = this.state.people[i];
theoptions.push(<option
value={person.name} key={person.name}>{person.name},
{person.age}</option>);
}
return (
<div>
<select
id={this.props.name} value={this.props.value} size="5">
{theoptions}
</select>
</div>
);
},
})
var fromelement = ReactDOM.render(<PeopleList url="http://localhost:5000/peopleToSelect" name="from" />, fromNode);
var toelement = ReactDOM.render(<PeopleList url="http://localhost:5000/peopleSelected" name="to" />, toNode);
$( "#addform" ).submit(function( event ) {
// Stop form from submitting normally
event.preventDefault();
// Get some values from elements on the page:
var $form = $( this ),
fromval
= $("#from").val(),
url
= $form.attr( "action" );
// Send the data using post
var posting = $.post( url, { from: fromval } );
// Put the results in a div
posting.done(function( data ) {
fromelement.newdata(data[0]);
toelement.newdata(data[1]);
});
});
$( "#subform" ).submit(function( event ) {
// Stop form from submitting normally
event.preventDefault();
// Get some values from elements on the page:
var $form = $( this ),
toval
= $("#to").val(),
url
= $form.attr( "action" );
// Send the data using post
var posting = $.post( url, { to: toval } );
// Put the results in a div
posting.done(function( data ) {
console.log(fromelement);
fromelement.newdata(data[0]);
toelement.newdata(data[1]);
});
});
|
The real complication with this approach starts in the line $( "#addform" )... Since forms don't include the select boxes, this approach implies that when the user pushes a button form, we need to intercept the action, pick a name from the box, add the name to the form and do the post operation by hand. Note that I manually invoke the newdata method in the left and right boxes in the anonymous function registered in posting.done(). This ensures that the boxes will be re-rendered once the new data arrives from the server.
This works, but is so ugly! After some thought, I decided that this seems to be an un-React-ive way of solving the problem. Hence, I ended up solving the problem in a second improved way.
This starts with a simpler HTML file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Move the names from one box to the
other</title>
<!-- Not present in the
tutorial. Just for basic styling. -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
</head>
<body>
<div id="mountNode" />
<script type="text/babel" src="/static/react4/selectpeople4.js" />
</body>
</html>
|
Now, the jQuery free solution in React:
var Table =
React.createClass({
getInitialState
: function() {
return {people : [[], []], toadd : null, tosub : null};
},
gotResponse
: function(data) {
this.setState({people : data})
},
loadPeopleFromServer
: function() {
var theurl = this.props.baseurl + '/allPeople';
console.log('loadPeopleFromServer. url = ' + theurl);
$.ajax({
url:
theurl,
dataType:
'json',
cache:
false,
success:
this.gotResponse,
error:
function(xhr, status,
err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
postToServer
: function(url,
personstate) {
console.log('Posting to ' + url + '. Data : ' + personstate);
var posting = $.post( url, personstate
);
posting.done(this.gotResponse);
},
add
: function() {
this.postToServer(this.props.baseurl + '/add',
{from : this.state.toadd});
this.setState({toadd : null});
},
sub
: function() {
this.postToServer(this.props.baseurl + '/sub', {to : this.state.tosub});
this.setState({tosub : null});
},
changeAdd
: function(person) {
this.setState({toadd : person})
},
changeSub
: function(person) {
this.setState({tosub : person})
},
componentDidMount
: function() {
this.loadPeopleFromServer();
this.timer = setInterval(this.loadPeopleFromServer, 10000);
},
componentWillUnmount
: function() {
clearInterval(this.timer);
},
render
: function() {
return (
<table>
<tbody>
<tr>
<td>
<h1>Add:</h1>
</td>
<td
/>
<td>
<h1>Remove:</h1>
</td>
</tr>
<tr>
<td>
<PeopleList
peoplelist={this.state.people[0]} change={this.changeAdd} value={this.state.toadd} name="from"/>
</td>
<td>
<SelectButton
handleclick={this.add} title="-->"/>
<br/>
<SelectButton
handleclick={this.sub} title="<--"/>
</td>
<td>
<PeopleList
peoplelist={this.state.people[1]} change={this.changeSub} value={this.state.tosub} name="to"/>
</td>
</tr>
</tbody>
</table>
);
}
});
var SelectButton = React.createClass({
render
: function() {
return (<button onClick={this.props.handleclick} type="button">{this.props.title}</button>);
}
});
var PeopleList = React.createClass({
change
: function(event) {
this.props.change(event.target.value);
},
render
: function() {
var theoptions = [];
for (var i = 0; i < this.props.peoplelist.length; i++) {
var person = this.props.peoplelist[i];
theoptions.push(<option
value={person.name} key={person.name}>{person.name},
{person.age}</option>);
}
return (
<div>
<select
onChange={this.change} value={this.props.value} size="5">
{theoptions}
</select>
</div>
);
},
})
ReactDOM.render(<Table
baseurl="http://localhost:5000"/>, mountNode);
|
The top-level component, the Table, owns two PeopleLists (left and right) and two SelectButtons (--> and <--). This top component gives them the data they should show using properties. Whenever something happens in one of this lower level components (a button is pushed, or a name is selected), the component will invoke methods in the parent Table component, using callbacks, according to the principle of parent-child communication in react.
The four callbacks provided by the parent Table change the state (toadd and tosub), thus forcing a re-render of Table (and of children, if necessary). The callbacks from the SelectButton components will invoke a post operation on the server. On success, this operation will set the new list of people, which, has we can see in getInitialState() is a list of two lists. The first one is for the left select box, the second one for the right select box. Each time we press a button, we end up with new lists for both sides. However, since we use the key attribute in the render function of the PeopleList component, this will require minimal reconstruction of components from one rendering to the next, as most of them stay unchanged.
The server is still missing, but before that, we need to show the structure of files in the project. In this case I have:
react0 and react1 are solutions to the other exercises, while react3 was another solution that fell in disfavor. This leaves us with react2 and react4 that I showed in this message.
The server code is:
'''
Created on 16/03/2016
@author: filipius
'''
from flask import Flask, request, Response
import json
app = Flask(__name__)
listofpeople = [[], []]
@app.route('/')
def hello_world():
return 'Hello World!'
def readpeople(filename):
with open(filename, 'rt', encoding='utf-8') as f:
result = []
lines = f.readlines()
for l in lines:
array = l.strip().split()
name, age = array[0], int(array[1])
result.append({'name' : name, 'age': age})
return result
@app.route('/loggedNames')
def getLoggedName():
print('Called
loggedNames')
return json.dumps(readpeople('names.txt'))
def intersection(l1,
l2):
return [val for val in l1 if val in l2]
def subtract(l1,
l2):
return [val for val in l1 if val not in l2]
def updateListOfPeople():
newlistofpeople = readpeople('names.txt')
listofpeople[0] =
subtract(newlistofpeople, listofpeople[1])
listofpeople[1] =
intersection(listofpeople[1], newlistofpeople)
@app.route('/peopleToSelect')
def getListToSelect():
updateListOfPeople()
return
Response(response=json.dumps(listofpeople[0]), status=200, mimetype="application/json")
@app.route('/peopleSelected')
def getListSelected():
updateListOfPeople()
return
Response(response=json.dumps(listofpeople[1]), status=200, mimetype="application/json")
@app.route('/allPeople')
def getCompleteList():
updateListOfPeople()
return
Response(response=json.dumps(listofpeople), status=200, mimetype="application/json")
def persontolist(name,
peoplist):
for p in peoplist:
if (p['name'] == name):
return p
raise ValueError('No person called ' + name + ' exists')
@app.route('/add', methods = ['POST'])
def add():
print('Not
thread-safe! Please correct me!')
#add pressed
if (request.form['from'] != ''):
person = persontolist(request.form['from'], listofpeople[0])
listofpeople[0].remove(person)
listofpeople[1].append(person)
return
Response(response=json.dumps(listofpeople), status=200, mimetype="application/json")
@app.route('/sub', methods = ['POST'])
def sub():
print('Not
thread-safe! Please correct me!')
#sub pressed
if (request.form['to'] != ''):
person = persontolist(request.form['to'], listofpeople[1])
listofpeople[1].remove(person)
listofpeople[0].append(person)
return
Response(response=json.dumps(listofpeople), status=200, mimetype="application/json")
@app.route('/list.html')
def getListHtml():
return app.send_static_file('react1/loggednames.html')
@app.route('/select2.html')
def getSelectHtml2():
return app.send_static_file('react2/selectpeople2.html')
@app.route('/select3.html')
def getSelectHtml3():
return app.send_static_file('react3/selectpeople3.html')
@app.route('/select4.html')
def getSelectHtml4():
return app.send_static_file('react4/selectpeople4.html')
if __name__ == '__main__':
app.run(debug=True, port=5000)
|
Note that a race condition might emerge here: for example, when we pick a name and try to add it to the second box (or remove it from the second box), the name might have already left the list of names in the server, due to the periodic call of the /allPeople service. The server should handle this situation gracefully, which does not seem to be the case. However, I believe that this is a consequence of an oversimplified server. Solving it would complicate this example too much.
As I said before, consider this other message, for slight improvements: Still REACT-ifying: improving the services API.
Good luck on this example!
Sem comentários:
Enviar um comentário