sexta-feira, 25 de março de 2016

Logged Names - a select box from the server in React

This message solves exercise 2 from this list of exercises.

We need a server that provides us the data to populate the select box. I will provide the complete server below, in Flask, but, for the sake of keeping the presentation simple, I will start by displaying the JSON response of the server:

[{"age": 21, "name": "Paulo"},
 {"age": 15, "name": "Rui"},
 {"age": 25, "name": "Sandra"},
 {"age": 23, "name": "Adelle"},
 {"age": 50, "name": "Bryan"},
 {"age": 40, "name": "Castro"},
 {"age": 16, "name": "Pinto"},
 {"age": 18, "name": "Saul"},
 {"age": 11, "name": "Camilo"},
 {"age": 9, "name": "Luis"},
 {"age": 21, "name": "Andre"}]

First, the HTML. It is quite simple, only the references to the scripts we will use (and in fact, won't), a div element named "mountNode" and a final reference to the script that will replace the former div and create the select box with all the data that we need.


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Here I see the logged names</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/react1/loggedNames.js"/>
  </body>
</html>

A tricky detail here is the location of the script loggedNames.js. You might need to change it, depending on the exact configuration. Let me show the arrangement I have in Eclipse:

The 'static' folder makes it simpler for the server to find and send the html and js files.

Our react script will be very simple:


var LoggedNames = React.createClass({
       loadLoggedPeopleFromServer: function() {
           $.ajax({
             url: this.props.url,
             dataType: 'json',
             cache: false,
             success: function(data) {
               this.setState({loggedPersons: data});
             }.bind(this),
             error: function(xhr, status, err) {
               console.error(this.props.url, status, err.toString());
             }.bind(this)
           });
       },
       getInitialState : function() {
              return {loggedPersons : []};
       },
       componentDidMount : function() {
              this.loadLoggedPeopleFromServer();
              this.timer = setInterval(this.loadLoggedPeopleFromServer, 1000);
       },
       componentWillUnmount : function() {
              clearInterval(this.timer);
       },
       render: function() {
              var htmlnames = this.state.loggedPersons.map(function(logged) {
                 return (<option key={logged.name}>{logged.name}, {logged.age}</option>);
           });

           return (
             <div>
             <select size="5">
                     {htmlnames}
             </select>
             </div>
            
           );
       }
});

ReactDOM.render(<LoggedNames url="/loggedNames" />, mountNode);

Let us start by the render function: it creates a select box with space for 5 elements (the remaining will be accessible by scrolling). Each <option> will be of the form name, age. We are assuming that names are unique, which is reasonable since these names correspond to logins. Hence, they may serve as a key to the <option> elements. This key makes rendering fast, in case the next list of names coming from the server changes. The idea is that react doesn't need to recreate components with the same key across different rendering operations. Here, we use the map function to create the list of <option>s; this could be replaced by a more common for. We will do it in a different exercise. The <option> element contains no value attribute, because we do not care for the option in this simple exercise.

What does the loadLoggedPeopleFromServer function do? This function uses jQuery to do a get request to the server for the logged persons. On success, this function changes the state of the LoggedNames component, thus causing the component to re-render. This should happen every second, because we installed the timer immediately after the LoggedNames component mounted to the virtual DOM. Only on unmount will the timer stop. In practice, a timer will refresh the list of names every second, from the moment when the component first enters the HTML page, until it leaves (which this component never does).

Finally, the getInitialState sets the state to an empty list of persons. This will change immediately after the component mounts, as the loadLoggedPeopleFromServer function will be invoked.

Before showing you the server, we need a text file with the names (names.txt - take a look at the figure with the structure of the files above):

Paulo 21
Rui 15
Sandra 25
Adelle 23
Bryan 50
Castro 40
Pinto 16
Saul 18
Camilo 11
Luis 9
Andre 21

The python server is here. It is overly complex, because I will use the same server in another example. Our entry point in this example is the /loggedNames path. Note that this server is not thread-safe and it is absolutely unprepared for production. Usually, we would have a database serving the names, instead of a text file, thus automatically solving the the concurrency problem:


'''
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])
        print(person)
        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)


Finally, the result, once the server is running:




Sem comentários:

Enviar um comentário