Created on 30 Jan 2021 ; Modified on 23 Feb 2022
This is the seventh part of an article about Flask, as follows:
This is the seventh part of a Flask project to show a single page in two different versions:
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.
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
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.
Ajax is not an easy task to implement. But it could be very useful when you have to handle big html pages.
Enjoy, ldfa