Created on 30 Jan 2021 ;    Modified on 23 Feb 2022

How create a minimal flask project (part 7: playing AJAX)


This is the seventh part of an article about Flask, as follows:

Objective

This is the seventh part of a Flask project to show a single page in two different versions:

  • single language;
  • two languages.

Here we show how use AJAX methodology to communicate between web browser and web server without reload enterly the current HTML page read by the user.

Methodology

Use of AJAX requests us a quite big work. So, instead of modify one of the two apps already developed, we are going to develop a new app.

What does this app do? We are going to implement an html page with a select box with a few nations. If we select one nation in the select box, we send a request for bound data to web server. Web server will answer our request sending a little group of related numbers that we'll show in current page, without loading a new one.

We call this new app ajacs. Our directory structure become:

▼ flask_single_page
    run.py
    babel.cfg
    ▼ instance
        ▼ log
    ▼ single_page
        __init__.py
        ▼ ajacs                 # + new app folder
            __init__.py
            views.py
            models.py
            ▼ static            # + static resources (images, css, javascript, ...)
                ▼ css
                    style.css
                ▼ js
                    ajacs.js
            ▼ templates         # + templates folder
                ajbase.html
                ajindex.html

        ▼ oneel
            __init__.py
            views.py
            ▼ static
                ▼ css
                    style.css
            ▼ templates
                base.html
                index.html
        ▼ twoels
            __init__.py
            views.py
            ▼ static
                ▼ css
                    style.css
            ▼ templates
                2lbase.html
                2lindex.html
    ▼ docs
    ▼ tests
    ▼ venv

Development

Let's start with the part we know better: the server side application.

We'll need the list of countries, and, given a country a set of related numbers. We are going to implement this using two classes in module ajacs/models.py

 1  class Nations(object):
 2      _nations = {'en': 'England',
 3                  'it': 'Italy',
 4                  'de': 'Germany',
 5      }
 6 
 7      @classmethod
 8      def get(cls, key=None, default=None):
 9          '''given nation id returns its name'''
10          return cls._nations.get(key, default)
11 
12      @classmethod
13      def keys(cls):
14          '''return all nation keys '''
15          return cls._nations.keys()
16 
17      @classmethod
18      def values(cls):
19          '''return all nation names '''
20          return cls._nations.values()
21 
22      @classmethod
23      def set(cls, key, val):
24          '''set a name
25 
26             WARN this is valid only in the current session
27          '''
28          cls._nations[key] = val
29 
30  class NationData(object):
31      _data = { 'en': [100, 200, 300],
32                'it': [10, 20, 30],
33                'de': [150, 250, 350],
34      }
35 
36      @classmethod
37      def get(cls, key=None, default=None):
38          '''given nation id returns its data'''
39          return cls._data.get(key, default)

Class Nations (@ line 1) can give us three nations. Each nation has a code and a name. The code is used as key to get country's name. Beware we do not instantiate the class. We use it straightforward: its methods are all class methods.

Similarly we use the class NationData (@ line 30). This class returns us a list of numeric values bound to the country code. We use as is also this class, without instantiation.

Now a glance to ajacs/views.py.

 1  # std libs import
 2  from datetime import datetime
 3 
 4  # 3rd parties libs import
 5  from flask import (Blueprint,
 6                     jsonify,
 7                     render_template,
 8                     current_app,
 9                     request)
10 
11  # project modules import
12  from single_page import sitemap
13  from single_page.ajacs import models
14 
15  # this app will respond to srv/aj/... URLs
16  ajacs = Blueprint('ajacs',
17                    __name__,
18                    static_folder='static',
19                    template_folder='templates',
20                    url_prefix='/ajacs')
21 
22  @ajacs.route('/_get_nation_data', methods=['POST',])  #! respond to post request
23  def get_nation_data():
24      code = request.json                               # get country code from JSON request
25      data = models.NationData.get(code)                # get bound data from models
26      return jsonify(data)                              # send JSON response to caller
27 
28 
29  @ajacs.route('/')                 # index URLs
30  def index():
31      current_app.logger.debug('> index')
32      nations = models.Nations
33      return render_template('ajindex.html',            # send the "usual" html page
34                             title='ajax page title',
35                             nations=nations,
36                             )
37  ...<CUT>...

Here the news is function get_nation_data (@ line 23) used to respond to POST requests on URL http://localhost:5000/ajacs/_get_nation_data with nation code in request body. This function call NationData class to get the country bound data, and send them to caller, building a JSON response.

To use our app we need to sligtly modify the application file single_page.__init__.py adding the ajacs blueprint:

 1  ...<CUT>...
 2 
 3  def create_app(test=False):
 4      '''create and configure the app'''
 5      app = Flask(__name__, instance_relative_config=True)
 6 
 7      # ensure the instance folder exists
 8      if not os.path.exists(app.instance_path):
 9          os.makedirs(app.instance_path)
10 
11      set_config(app)
12      if not test:
13          set_logger(app)
14 
15      from .oneel import views as views1
16      app.register_blueprint(views1.oneel)
17 
18      from .twoels import views as views2
19      app.register_blueprint(views2.twoels)
20 
21      from .ajacs import views as views3                     # +
22      app.register_blueprint(views3.ajacs)                   # +
23 
24      babel.init_app(app)
25      sitemap.init_app(app)
26 
27      @app.route('/sitemap')
28      def ep_sitemap():                            # endpoint for sitemap
29          return sitemap.sitemap(), 200, {'Content-Type': 'text/xml', }
30 
31      return app
32 
33  ...<CUT>...

Ok, in views.py we are ready with our control logic, as well as in models.py we have our data. Now the ajacs/templates/ajbase.html:

 1  <!doctype html>
 2  <head>
 3  <title>{% block title %}{% endblock %} - Ajacs</title>
 4  <link rel="stylesheet" href="{{ url_for('ajacs.static', filename='css/style.css') }}">
 5  </head>
 6  <body>
 7  <section class="content">
 8    <header>
 9      {% block header %}{% endblock %}
10    </header>
11    {% for message in get_flashed_messages() %}
12      <div class="flash">{{ message }}</div>
13    {% endfor %}
14    {% block content %}{% endblock %}
15  </section>
16 
17  <!-- loading base javascripts -->
18  <script type=text/javascript>$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
19  </script>
20 
21 
22  {% block javas %}{% endblock %}
23  </body>

Respect other base.html we know, we see @ line 18 the javascript variable $SCRIPT_ROOT for reference in javascript code. And @ line 22 there is a block where to load javascript programs.

The ajacs/templates/ajindex.html template builds upon ajbase.html as follows:

 1  {% extends 'ajbase.html' %}
 2  {% block header %}
 3    <h1>{% block title %}{{title}}{% endblock %}</h1>
 4  {% endblock %}
 5 
 6  {% block content %}
 7 
 8      <select name="nation" id="nation" >
 9           <option value="" >Please select a country</option>
10           {% for nid in nations.keys() %}
11               <option value="{{nid}}">{{nations.get(nid)}}</option>
12           {% endfor %}
13      </select>
14      <p></p>
15      <p name="nation_values" id="nation_values"></p>
16 
17  {% endblock %}
18 
19  {% block javas %}
20 
21      <script src="{{ url_for('ajacs.static', filename='js/ajacs.js') }}"></script>
22 
23  {% endblock %}

Here in content we build the select box (@ lines from 8 to 13), naming it with id nation.

Then we make an empty paragraph (@ line 15), naming it with id nation_values.

And last, (@ line 21) we load the needed javascript to work in web browser side. It's the following file ajacs/static/js/ajacs.js:

 1  // Display a debug message in console
 2  function debug(msg) {
 3      console.log(msg);
 4  }
 5 
 6  // peek up the voice indicated by the user in the "nation" selection
 7  //   ... and post it to web server
 8  function chk_nation(){
 9      debug('> chk_nation');
10      var select_box = document.getElementById("nation");
11      var selected_value = select_box.options[select_box.selectedIndex].value;
12      postJSON($SCRIPT_ROOT+'_get_nation_data',
13               selected_value,
14               set_nation_values);
15  }
16 
17  // get response from web server, parse it and inject it in "txt_par"
18  function set_nation_values(r){
19      debug('> set_nation_values(' + r +')');
20      v = parse_response(r);
21      var txt_par = document.getElementById('nation_values');
22      txt_par.innerHTML = v;
23  }
24 
25  // parsing server response
26  function parse_response(response){
27      debug('>parse_request(' + response + ')')
28      if (response.readyState === 4 && response.status === 200) {
29          var type = response.getResponseHeader("Content-Type");    // Get the type of the response
30          if (type.indexOf("xml") !== -1 && response.responseXML)   // Check type
31              return response.responseXML;                          //   Document response
32          else if (type === "application/json")
33              return JSON.parse(response.responseText);             //   JSON response
34          else
35              return response.responseText;
36      }
37  }
38 
39  // posting data to web server as JSON
40  function postJSON(url, data, callback) {
41      debug('> postJSON(' + url + ', ' + data + ', ' + callback.name + ')')
42      var request = new XMLHttpRequest();
43      request.open("POST", url);                        // POST to the specified url
44      request.onreadystatechange = function() {         // Simple event handler
45          if (request.readyState === 4 && callback)     // When response is complete ...
46              callback(request);                        // ... call the callback.
47      };
48      request.setRequestHeader("Content-Type", "application/json");
49      request.send(JSON.stringify(data));
50  }
51 
52  // hook chk_nation to "nation" select
53  document.getElementById("nation").onchange = chk_nation;

Well, about me this is the hardest part. I'm not used writing with javascript: Python it's too comfortable :-) But when we need to work on the web browser side, javascipt is the de facto standard.

Entry point is function chk_nation (@ line 8). This function is triggered when select box with id nation is changed from user ( hook of this function and select box is @ line 53).

chk_nation get the selected countruy code (@ line 11) and exec a POST of a JSON request using function postJSON (@ line 12).

postJSON is the true AJAX function: it prepares the request (@ lines from 42 to 48), using as callback the function in parameter callback (@ line 46). Then send the request (@ line 49).

The receiving part is handled from function set_nation_values. This receives a response, parse it, and set the text of paragraph nation_values with the parsed value.

Conclusion

Ajax is not an easy task to implement. But it could be very useful when you have to handle big html pages.

Enjoy, ldfa